mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-22 15:22:12 +00:00
Compare commits
7 Commits
dependabot
...
queue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42983a3add | ||
|
|
ad3eddbc08 | ||
|
|
9c1ecc3796 | ||
|
|
64595f5875 | ||
|
|
b66140bfd2 | ||
|
|
45158ff45b | ||
|
|
06fb3685e4 |
@@ -11,7 +11,7 @@ import {
|
|||||||
primaryKey,
|
primaryKey,
|
||||||
uniqueIndex
|
uniqueIndex
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel, sql } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
domains,
|
domains,
|
||||||
orgs,
|
orgs,
|
||||||
@@ -207,17 +207,28 @@ export const remoteExitNodeSessions = pgTable("remoteExitNodeSession", {
|
|||||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const loginPage = pgTable("loginPage", {
|
export const loginPage = pgTable(
|
||||||
|
"loginPage",
|
||||||
|
{
|
||||||
loginPageId: serial("loginPageId").primaryKey(),
|
loginPageId: serial("loginPageId").primaryKey(),
|
||||||
subdomain: varchar("subdomain"),
|
subdomain: varchar("subdomain"),
|
||||||
fullDomain: varchar("fullDomain"),
|
fullDomain: varchar("fullDomain"),
|
||||||
exitNodeId: integer("exitNodeId").references(() => exitNodes.exitNodeId, {
|
exitNodeId: integer("exitNodeId").references(
|
||||||
|
() => exitNodes.exitNodeId,
|
||||||
|
{
|
||||||
onDelete: "set null"
|
onDelete: "set null"
|
||||||
}),
|
}
|
||||||
|
),
|
||||||
domainId: varchar("domainId").references(() => domains.domainId, {
|
domainId: varchar("domainId").references(() => domains.domainId, {
|
||||||
onDelete: "set null"
|
onDelete: "set null"
|
||||||
})
|
})
|
||||||
});
|
},
|
||||||
|
(t) => [
|
||||||
|
index("idx_loginpage_fulldomain")
|
||||||
|
.on(t.fullDomain)
|
||||||
|
.where(sql`${t.fullDomain} IS NOT NULL`)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
export const loginPageOrg = pgTable("loginPageOrg", {
|
export const loginPageOrg = pgTable("loginPageOrg", {
|
||||||
loginPageId: integer("loginPageId")
|
loginPageId: integer("loginPageId")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel, sql } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
bigint,
|
bigint,
|
||||||
boolean,
|
boolean,
|
||||||
@@ -82,7 +82,9 @@ export const orgDomains = pgTable("orgDomains", {
|
|||||||
.references(() => domains.domainId, { onDelete: "cascade" })
|
.references(() => domains.domainId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const sites = pgTable("sites", {
|
export const sites = pgTable(
|
||||||
|
"sites",
|
||||||
|
{
|
||||||
siteId: serial("siteId").primaryKey(),
|
siteId: serial("siteId").primaryKey(),
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
.references(() => orgs.orgId, {
|
.references(() => orgs.orgId, {
|
||||||
@@ -107,17 +109,32 @@ export const sites = pgTable("sites", {
|
|||||||
publicKey: varchar("publicKey"),
|
publicKey: varchar("publicKey"),
|
||||||
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
||||||
listenPort: integer("listenPort"),
|
listenPort: integer("listenPort"),
|
||||||
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
|
dockerSocketEnabled: boolean("dockerSocketEnabled")
|
||||||
autoUpdateEnabled: boolean("autoUpdateEnabled").notNull().default(false),
|
.notNull()
|
||||||
|
.default(true),
|
||||||
|
autoUpdateEnabled: boolean("autoUpdateEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
autoUpdateOverrideOrg: boolean("autoUpdateOverrideOrg")
|
autoUpdateOverrideOrg: boolean("autoUpdateOverrideOrg")
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
status: varchar("status")
|
status: varchar("status")
|
||||||
.$type<"pending" | "approved">()
|
.$type<"pending" | "approved">()
|
||||||
.default("approved")
|
.default("approved")
|
||||||
});
|
},
|
||||||
|
(t) => [
|
||||||
|
index("idx_sites_exitnodeid").on(t.exitNodeId),
|
||||||
|
index("idx_sites_exitnode_type_siteid").on(
|
||||||
|
t.exitNodeId,
|
||||||
|
t.type,
|
||||||
|
t.siteId
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
export const resources = pgTable("resources", {
|
export const resources = pgTable(
|
||||||
|
"resources",
|
||||||
|
{
|
||||||
resourceId: serial("resourceId").primaryKey(),
|
resourceId: serial("resourceId").primaryKey(),
|
||||||
resourcePolicyId: integer("resourcePolicyId").references(
|
resourcePolicyId: integer("resourcePolicyId").references(
|
||||||
() => resourcePolicies.resourcePolicyId,
|
() => resourcePolicies.resourcePolicyId,
|
||||||
@@ -182,7 +199,13 @@ export const resources = pgTable("resources", {
|
|||||||
.$type<"site" | "remote" | "native">()
|
.$type<"site" | "remote" | "native">()
|
||||||
.default("site"),
|
.default("site"),
|
||||||
authDaemonPort: integer("authDaemonPort").default(22123)
|
authDaemonPort: integer("authDaemonPort").default(22123)
|
||||||
});
|
},
|
||||||
|
(t) => [
|
||||||
|
index("idx_resources_fulldomain")
|
||||||
|
.on(t.fullDomain)
|
||||||
|
.where(sql`${t.fullDomain} IS NOT NULL`)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
export const labels = pgTable("labels", {
|
export const labels = pgTable("labels", {
|
||||||
labelId: serial("labelId").primaryKey(),
|
labelId: serial("labelId").primaryKey(),
|
||||||
@@ -267,7 +290,9 @@ export const clientLabels = pgTable(
|
|||||||
(t) => [unique("client_label_uniq").on(t.clientId, t.labelId)]
|
(t) => [unique("client_label_uniq").on(t.clientId, t.labelId)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const targets = pgTable("targets", {
|
export const targets = pgTable(
|
||||||
|
"targets",
|
||||||
|
{
|
||||||
targetId: serial("targetId").primaryKey(),
|
targetId: serial("targetId").primaryKey(),
|
||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.references(() => resources.resourceId, {
|
.references(() => resources.resourceId, {
|
||||||
@@ -294,9 +319,18 @@ export const targets = pgTable("targets", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.default("http"),
|
.default("http"),
|
||||||
authToken: varchar("authToken")
|
authToken: varchar("authToken")
|
||||||
});
|
},
|
||||||
|
(t) => [
|
||||||
|
index("idx_targets_resourceid_siteid").on(t.resourceId, t.siteId),
|
||||||
|
index("idx_targets_site_enabled_priority_target_resource")
|
||||||
|
.on(t.siteId, t.priority.desc(), t.targetId, t.resourceId)
|
||||||
|
.where(sql`${t.enabled} = true`)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
export const targetHealthCheck = pgTable("targetHealthCheck", {
|
export const targetHealthCheck = pgTable(
|
||||||
|
"targetHealthCheck",
|
||||||
|
{
|
||||||
targetHealthCheckId: serial("targetHealthCheckId").primaryKey(),
|
targetHealthCheckId: serial("targetHealthCheckId").primaryKey(),
|
||||||
targetId: integer("targetId").references(() => targets.targetId, {
|
targetId: integer("targetId").references(() => targets.targetId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
@@ -331,7 +365,9 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
|
|||||||
hcTlsServerName: text("hcTlsServerName"),
|
hcTlsServerName: text("hcTlsServerName"),
|
||||||
hcHealthyThreshold: integer("hcHealthyThreshold").default(1),
|
hcHealthyThreshold: integer("hcHealthyThreshold").default(1),
|
||||||
hcUnhealthyThreshold: integer("hcUnhealthyThreshold").default(1)
|
hcUnhealthyThreshold: integer("hcUnhealthyThreshold").default(1)
|
||||||
});
|
},
|
||||||
|
(t) => [index("idx_targethealthcheck_targetid").on(t.targetId)]
|
||||||
|
);
|
||||||
|
|
||||||
export const exitNodes = pgTable("exitNodes", {
|
export const exitNodes = pgTable("exitNodes", {
|
||||||
exitNodeId: serial("exitNodeId").primaryKey(),
|
exitNodeId: serial("exitNodeId").primaryKey(),
|
||||||
@@ -406,7 +442,9 @@ export const networks = pgTable("networks", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const siteNetworks = pgTable("siteNetworks", {
|
export const siteNetworks = pgTable(
|
||||||
|
"siteNetworks",
|
||||||
|
{
|
||||||
siteId: integer("siteId")
|
siteId: integer("siteId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => sites.siteId, {
|
.references(() => sites.siteId, {
|
||||||
@@ -415,34 +453,63 @@ export const siteNetworks = pgTable("siteNetworks", {
|
|||||||
networkId: integer("networkId")
|
networkId: integer("networkId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => networks.networkId, { onDelete: "cascade" })
|
.references(() => networks.networkId, { onDelete: "cascade" })
|
||||||
});
|
},
|
||||||
|
(t) => [
|
||||||
|
index("idx_sitenetworks_siteid").on(t.siteId),
|
||||||
|
index("idx_sitenetworks_networkid").on(t.networkId)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
export const clientSiteResources = pgTable("clientSiteResources", {
|
export const clientSiteResources = pgTable(
|
||||||
|
"clientSiteResources",
|
||||||
|
{
|
||||||
clientId: integer("clientId")
|
clientId: integer("clientId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => clients.clientId, { onDelete: "cascade" }),
|
.references(() => clients.clientId, { onDelete: "cascade" }),
|
||||||
siteResourceId: integer("siteResourceId")
|
siteResourceId: integer("siteResourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
.references(() => siteResources.siteResourceId, {
|
||||||
});
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
index("idx_clientsiteresources_clientid").on(t.clientId),
|
||||||
|
index("idx_clientsiteresources_siteresourceid").on(t.siteResourceId)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
export const roleSiteResources = pgTable("roleSiteResources", {
|
export const roleSiteResources = pgTable(
|
||||||
|
"roleSiteResources",
|
||||||
|
{
|
||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => roles.roleId, { onDelete: "cascade" }),
|
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||||
siteResourceId: integer("siteResourceId")
|
siteResourceId: integer("siteResourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
.references(() => siteResources.siteResourceId, {
|
||||||
});
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
(t) => [index("idx_rolesiteresources_siteresourceid").on(t.siteResourceId)]
|
||||||
|
);
|
||||||
|
|
||||||
export const userSiteResources = pgTable("userSiteResources", {
|
export const userSiteResources = pgTable(
|
||||||
|
"userSiteResources",
|
||||||
|
{
|
||||||
userId: varchar("userId")
|
userId: varchar("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, { onDelete: "cascade" }),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
siteResourceId: integer("siteResourceId")
|
siteResourceId: integer("siteResourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => siteResources.siteResourceId, { onDelete: "cascade" })
|
.references(() => siteResources.siteResourceId, {
|
||||||
});
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
index("idx_usersiteresources_userid").on(t.userId),
|
||||||
|
index("idx_usersiteresources_siteresourceid").on(t.siteResourceId)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
export const users = pgTable("user", {
|
export const users = pgTable("user", {
|
||||||
userId: varchar("id").primaryKey(),
|
userId: varchar("id").primaryKey(),
|
||||||
@@ -467,7 +534,9 @@ export const users = pgTable("user", {
|
|||||||
locale: varchar("locale")
|
locale: varchar("locale")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const newts = pgTable("newt", {
|
export const newts = pgTable(
|
||||||
|
"newt",
|
||||||
|
{
|
||||||
newtId: varchar("id").primaryKey(),
|
newtId: varchar("id").primaryKey(),
|
||||||
secretHash: varchar("secretHash").notNull(),
|
secretHash: varchar("secretHash").notNull(),
|
||||||
dateCreated: varchar("dateCreated").notNull(),
|
dateCreated: varchar("dateCreated").notNull(),
|
||||||
@@ -475,7 +544,9 @@ export const newts = pgTable("newt", {
|
|||||||
siteId: integer("siteId").references(() => sites.siteId, {
|
siteId: integer("siteId").references(() => sites.siteId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
});
|
},
|
||||||
|
(t) => [index("idx_newt_siteid").on(t.siteId)]
|
||||||
|
);
|
||||||
|
|
||||||
export const twoFactorBackupCodes = pgTable("twoFactorBackupCodes", {
|
export const twoFactorBackupCodes = pgTable("twoFactorBackupCodes", {
|
||||||
codeId: serial("id").primaryKey(),
|
codeId: serial("id").primaryKey(),
|
||||||
@@ -576,7 +647,9 @@ export const userOrgRoles = pgTable(
|
|||||||
(t) => [unique().on(t.userId, t.orgId, t.roleId)]
|
(t) => [unique().on(t.userId, t.orgId, t.roleId)]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const roleActions = pgTable("roleActions", {
|
export const roleActions = pgTable(
|
||||||
|
"roleActions",
|
||||||
|
{
|
||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => roles.roleId, { onDelete: "cascade" }),
|
.references(() => roles.roleId, { onDelete: "cascade" }),
|
||||||
@@ -586,9 +659,19 @@ export const roleActions = pgTable("roleActions", {
|
|||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => orgs.orgId, { onDelete: "cascade" })
|
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||||
});
|
},
|
||||||
|
(t) => [
|
||||||
|
index("idx_roleActions_roleId_orgId_actionId").on(
|
||||||
|
t.roleId,
|
||||||
|
t.orgId,
|
||||||
|
t.actionId
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
export const userActions = pgTable("userActions", {
|
export const userActions = pgTable(
|
||||||
|
"userActions",
|
||||||
|
{
|
||||||
userId: varchar("userId")
|
userId: varchar("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.userId, { onDelete: "cascade" }),
|
.references(() => users.userId, { onDelete: "cascade" }),
|
||||||
@@ -598,7 +681,15 @@ export const userActions = pgTable("userActions", {
|
|||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => orgs.orgId, { onDelete: "cascade" })
|
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||||
});
|
},
|
||||||
|
(t) => [
|
||||||
|
index("idx_userActions_userId_orgId_actionId").on(
|
||||||
|
t.userId,
|
||||||
|
t.orgId,
|
||||||
|
t.actionId
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
export const roleSites = pgTable("roleSites", {
|
export const roleSites = pgTable("roleSites", {
|
||||||
roleId: integer("roleId")
|
roleId: integer("roleId")
|
||||||
@@ -1004,7 +1095,9 @@ export const idpOrg = pgTable("idpOrg", {
|
|||||||
orgMapping: varchar("orgMapping")
|
orgMapping: varchar("orgMapping")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clients = pgTable("clients", {
|
export const clients = pgTable(
|
||||||
|
"clients",
|
||||||
|
{
|
||||||
clientId: serial("clientId").primaryKey(),
|
clientId: serial("clientId").primaryKey(),
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
.references(() => orgs.orgId, {
|
.references(() => orgs.orgId, {
|
||||||
@@ -1037,7 +1130,9 @@ export const clients = pgTable("clients", {
|
|||||||
approvalState: varchar("approvalState").$type<
|
approvalState: varchar("approvalState").$type<
|
||||||
"pending" | "approved" | "denied"
|
"pending" | "approved" | "denied"
|
||||||
>()
|
>()
|
||||||
});
|
},
|
||||||
|
(t) => [index("idx_clients_userid").on(t.userId)]
|
||||||
|
);
|
||||||
|
|
||||||
export const clientSitesAssociationsCache = pgTable(
|
export const clientSitesAssociationsCache = pgTable(
|
||||||
"clientSitesAssociationsCache",
|
"clientSitesAssociationsCache",
|
||||||
@@ -1049,7 +1144,11 @@ export const clientSitesAssociationsCache = pgTable(
|
|||||||
isJitMode: boolean("isJitMode").notNull().default(false),
|
isJitMode: boolean("isJitMode").notNull().default(false),
|
||||||
endpoint: varchar("endpoint"),
|
endpoint: varchar("endpoint"),
|
||||||
publicKey: varchar("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
publicKey: varchar("publicKey") // this will act as the session's public key for hole punching so we can track when it changes
|
||||||
}
|
},
|
||||||
|
(t) => [
|
||||||
|
primaryKey({ columns: [t.clientId, t.siteId] }),
|
||||||
|
index("idx_clientsitesassociationscache_siteid").on(t.siteId)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const clientSiteResourcesAssociationsCache = pgTable(
|
export const clientSiteResourcesAssociationsCache = pgTable(
|
||||||
@@ -1058,7 +1157,14 @@ export const clientSiteResourcesAssociationsCache = pgTable(
|
|||||||
clientId: integer("clientId") // not a foreign key here so after its deleted the rebuild function can delete it and send the message
|
clientId: integer("clientId") // not a foreign key here so after its deleted the rebuild function can delete it and send the message
|
||||||
.notNull(),
|
.notNull(),
|
||||||
siteResourceId: integer("siteResourceId").notNull()
|
siteResourceId: integer("siteResourceId").notNull()
|
||||||
}
|
},
|
||||||
|
(t) => [
|
||||||
|
primaryKey({ columns: [t.clientId, t.siteResourceId] }),
|
||||||
|
index("idx_clientSiteResourcesAssociationsCache_siteResourceId").on(
|
||||||
|
t.siteResourceId,
|
||||||
|
t.clientId
|
||||||
|
)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const clientPostureSnapshots = pgTable("clientPostureSnapshots", {
|
export const clientPostureSnapshots = pgTable("clientPostureSnapshots", {
|
||||||
@@ -1071,7 +1177,9 @@ export const clientPostureSnapshots = pgTable("clientPostureSnapshots", {
|
|||||||
collectedAt: integer("collectedAt").notNull()
|
collectedAt: integer("collectedAt").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const olms = pgTable("olms", {
|
export const olms = pgTable(
|
||||||
|
"olms",
|
||||||
|
{
|
||||||
olmId: varchar("id").primaryKey(),
|
olmId: varchar("id").primaryKey(),
|
||||||
secretHash: varchar("secretHash").notNull(),
|
secretHash: varchar("secretHash").notNull(),
|
||||||
dateCreated: varchar("dateCreated").notNull(),
|
dateCreated: varchar("dateCreated").notNull(),
|
||||||
@@ -1087,7 +1195,9 @@ export const olms = pgTable("olms", {
|
|||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
archived: boolean("archived").notNull().default(false)
|
archived: boolean("archived").notNull().default(false)
|
||||||
});
|
},
|
||||||
|
(t) => [index("idx_olms_clientid").on(t.clientId)]
|
||||||
|
);
|
||||||
|
|
||||||
export const currentFingerprint = pgTable("currentFingerprint", {
|
export const currentFingerprint = pgTable("currentFingerprint", {
|
||||||
fingerprintId: serial("id").primaryKey(),
|
fingerprintId: serial("id").primaryKey(),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import license from "#dynamic/license/license";
|
|||||||
import { initLogCleanupInterval } from "@server/lib/cleanupLogs";
|
import { initLogCleanupInterval } from "@server/lib/cleanupLogs";
|
||||||
import { initAcmeCertSync } from "#dynamic/lib/acmeCertSync";
|
import { initAcmeCertSync } from "#dynamic/lib/acmeCertSync";
|
||||||
import { fetchServerIp } from "@server/lib/serverIpService";
|
import { fetchServerIp } from "@server/lib/serverIpService";
|
||||||
|
import { startRebuildQueueProcessor } from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
async function startServers() {
|
async function startServers() {
|
||||||
await setHostMeta();
|
await setHostMeta();
|
||||||
@@ -41,6 +42,7 @@ async function startServers() {
|
|||||||
|
|
||||||
initLogCleanupInterval();
|
initLogCleanupInterval();
|
||||||
initAcmeCertSync();
|
initAcmeCertSync();
|
||||||
|
startRebuildQueueProcessor();
|
||||||
|
|
||||||
// Start all servers
|
// Start all servers
|
||||||
const apiServer = createApiServer();
|
const apiServer = createApiServer();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
exitNodes,
|
exitNodes,
|
||||||
newts,
|
newts,
|
||||||
olms,
|
olms,
|
||||||
|
primaryDb,
|
||||||
roleSiteResources,
|
roleSiteResources,
|
||||||
Site,
|
Site,
|
||||||
SiteResource,
|
SiteResource,
|
||||||
@@ -20,10 +21,10 @@ import {
|
|||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { and, count, eq, inArray, ne } from "drizzle-orm";
|
import { and, count, eq, inArray, ne } from "drizzle-orm";
|
||||||
|
|
||||||
import { deletePeer as newtDeletePeer } from "@server/routers/newt/peers";
|
import { deletePeersBatch as newtDeletePeersBatch } from "@server/routers/newt/peers";
|
||||||
import {
|
import {
|
||||||
initPeerAddHandshake,
|
initPeerAddHandshakeBatch,
|
||||||
deletePeer as olmDeletePeer
|
deletePeersBatch as olmDeletePeersBatch
|
||||||
} from "@server/routers/olm/peers";
|
} from "@server/routers/olm/peers";
|
||||||
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
@@ -34,12 +35,13 @@ import {
|
|||||||
parseEndpoint
|
parseEndpoint
|
||||||
} from "@server/lib/ip";
|
} from "@server/lib/ip";
|
||||||
import {
|
import {
|
||||||
addPeerData,
|
addPeerDataBatch,
|
||||||
addTargets as addSubnetProxyTargets,
|
addTargetsBatch as addSubnetProxyTargetsBatch,
|
||||||
removePeerData,
|
removePeerDataBatch,
|
||||||
removeTargets as removeSubnetProxyTargets
|
removeTargetsBatch as removeSubnetProxyTargetsBatch
|
||||||
} from "@server/routers/client/targets";
|
} from "@server/routers/client/targets";
|
||||||
import { lockManager } from "#dynamic/lib/lock";
|
import { lockManager } from "#dynamic/lib/lock";
|
||||||
|
import { rebuildQueue } from "#dynamic/lib/rebuildQueue";
|
||||||
|
|
||||||
// TTL for rebuild-association locks. These functions can fan out into many
|
// TTL for rebuild-association locks. These functions can fan out into many
|
||||||
// peer/proxy updates, so give them a generous window.
|
// peer/proxy updates, so give them a generous window.
|
||||||
@@ -167,11 +169,32 @@ export async function rebuildClientAssociationsFromSiteResource(
|
|||||||
subnet: string | null;
|
subnet: string | null;
|
||||||
}[];
|
}[];
|
||||||
}> {
|
}> {
|
||||||
|
try {
|
||||||
return await lockManager.withLock(
|
return await lockManager.withLock(
|
||||||
`rebuild-client-associations:site-resource:${siteResource.siteResourceId}`,
|
`rebuild-client-associations:site-resource:${siteResource.siteResourceId}`,
|
||||||
() => rebuildClientAssociationsFromSiteResourceImpl(siteResource, trx),
|
() =>
|
||||||
|
rebuildClientAssociationsFromSiteResourceImpl(
|
||||||
|
siteResource,
|
||||||
|
trx
|
||||||
|
),
|
||||||
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
|
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
|
||||||
);
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (
|
||||||
|
typeof err?.message === "string" &&
|
||||||
|
err.message.startsWith("Failed to acquire lock")
|
||||||
|
) {
|
||||||
|
logger.warn(
|
||||||
|
`rebuildClientAssociations: could not acquire lock for site resource ${siteResource.siteResourceId}, queuing for deferred processing`
|
||||||
|
);
|
||||||
|
await rebuildQueue.enqueue({
|
||||||
|
type: "site-resource",
|
||||||
|
id: siteResource.siteResourceId
|
||||||
|
});
|
||||||
|
return { mergedAllClients: [] };
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rebuildClientAssociationsFromSiteResourceImpl(
|
async function rebuildClientAssociationsFromSiteResourceImpl(
|
||||||
@@ -536,6 +559,28 @@ async function handleMessagesForSiteClients(
|
|||||||
const newtJobs: Promise<any>[] = [];
|
const newtJobs: Promise<any>[] = [];
|
||||||
const olmJobs: Promise<any>[] = [];
|
const olmJobs: Promise<any>[] = [];
|
||||||
const exitNodeJobs: Promise<any>[] = [];
|
const exitNodeJobs: Promise<any>[] = [];
|
||||||
|
const newtPeerDeletes: {
|
||||||
|
siteId: number;
|
||||||
|
publicKey: string;
|
||||||
|
newtId: string;
|
||||||
|
}[] = [];
|
||||||
|
const olmPeerDeletes: {
|
||||||
|
clientId: number;
|
||||||
|
siteId: number;
|
||||||
|
publicKey: string;
|
||||||
|
olmId: string;
|
||||||
|
}[] = [];
|
||||||
|
const olmPeerAddHandshakes: {
|
||||||
|
clientId: number;
|
||||||
|
peer: {
|
||||||
|
siteId: number;
|
||||||
|
exitNode: {
|
||||||
|
publicKey: string;
|
||||||
|
endpoint: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
olmId: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
// Combine all clients that need processing (those being added or removed)
|
// Combine all clients that need processing (those being added or removed)
|
||||||
const clientsToProcess = new Map<
|
const clientsToProcess = new Map<
|
||||||
@@ -584,6 +629,21 @@ async function handleMessagesForSiteClients(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Batch-fetch all olm IDs for the clients we need to process
|
||||||
|
const clientIdsToProcess = Array.from(clientsToProcess.keys());
|
||||||
|
const olmRows =
|
||||||
|
clientIdsToProcess.length > 0
|
||||||
|
? await trx
|
||||||
|
.select({ olmId: olms.olmId, clientId: olms.clientId })
|
||||||
|
.from(olms)
|
||||||
|
.where(inArray(olms.clientId, clientIdsToProcess))
|
||||||
|
: [];
|
||||||
|
const olmByClientId = new Map<number, string>(
|
||||||
|
olmRows
|
||||||
|
.filter((r) => r.clientId !== null)
|
||||||
|
.map((r) => [r.clientId as number, r.olmId])
|
||||||
|
);
|
||||||
|
|
||||||
for (const client of clientsToProcess.values()) {
|
for (const client of clientsToProcess.values()) {
|
||||||
// UPDATE THE NEWT
|
// UPDATE THE NEWT
|
||||||
if (!client.subnet || !client.pubKey) {
|
if (!client.subnet || !client.pubKey) {
|
||||||
@@ -600,14 +660,8 @@ async function handleMessagesForSiteClients(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [olm] = await trx
|
const olmId = olmByClientId.get(client.clientId);
|
||||||
.select({
|
if (!olmId) {
|
||||||
olmId: olms.olmId
|
|
||||||
})
|
|
||||||
.from(olms)
|
|
||||||
.where(eq(olms.clientId, client.clientId))
|
|
||||||
.limit(1);
|
|
||||||
if (!olm) {
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Olm not found for client ${client.clientId} so cannot add/delete peers`
|
`Olm not found for client ${client.clientId} so cannot add/delete peers`
|
||||||
);
|
);
|
||||||
@@ -615,15 +669,17 @@ async function handleMessagesForSiteClients(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isDelete) {
|
if (isDelete) {
|
||||||
newtJobs.push(newtDeletePeer(siteId, client.pubKey, newt.newtId));
|
newtPeerDeletes.push({
|
||||||
olmJobs.push(
|
|
||||||
olmDeletePeer(
|
|
||||||
client.clientId,
|
|
||||||
siteId,
|
siteId,
|
||||||
site.publicKey,
|
publicKey: client.pubKey,
|
||||||
olm.olmId
|
newtId: newt.newtId
|
||||||
)
|
});
|
||||||
);
|
olmPeerDeletes.push({
|
||||||
|
clientId: client.clientId,
|
||||||
|
siteId,
|
||||||
|
publicKey: site.publicKey,
|
||||||
|
olmId
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAdd) {
|
if (isAdd) {
|
||||||
@@ -635,23 +691,34 @@ async function handleMessagesForSiteClients(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await initPeerAddHandshake(
|
olmPeerAddHandshakes.push({
|
||||||
// this will kick off the add peer process for the client
|
clientId: client.clientId,
|
||||||
client.clientId,
|
peer: {
|
||||||
{
|
|
||||||
siteId,
|
siteId,
|
||||||
exitNode: {
|
exitNode: {
|
||||||
publicKey: exitNode.publicKey,
|
publicKey: exitNode.publicKey,
|
||||||
endpoint: exitNode.endpoint
|
endpoint: exitNode.endpoint
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
olm.olmId
|
olmId
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exitNodeJobs.push(updateClientSiteDestinations(client, trx));
|
exitNodeJobs.push(updateClientSiteDestinations(client, trx));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newtPeerDeletes.length > 0) {
|
||||||
|
newtJobs.push(newtDeletePeersBatch(newtPeerDeletes));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (olmPeerDeletes.length > 0) {
|
||||||
|
olmJobs.push(olmDeletePeersBatch(olmPeerDeletes));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (olmPeerAddHandshakes.length > 0) {
|
||||||
|
olmJobs.push(initPeerAddHandshakeBatch(olmPeerAddHandshakes));
|
||||||
|
}
|
||||||
|
|
||||||
Promise.all(exitNodeJobs).catch((error) => {
|
Promise.all(exitNodeJobs).catch((error) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
`rebuildClientAssociations: Error updating client site destinations for site ${site.siteId}:`,
|
`rebuildClientAssociations: Error updating client site destinations for site ${site.siteId}:`,
|
||||||
@@ -844,26 +911,30 @@ async function handleSubnetProxyTargetUpdates(
|
|||||||
|
|
||||||
if (targetsToAdd) {
|
if (targetsToAdd) {
|
||||||
proxyJobs.push(
|
proxyJobs.push(
|
||||||
addSubnetProxyTargets(
|
addSubnetProxyTargetsBatch([
|
||||||
newt.newtId,
|
{
|
||||||
targetsToAdd,
|
newtId: newt.newtId,
|
||||||
newt.version
|
targets: targetsToAdd,
|
||||||
)
|
version: newt.version
|
||||||
|
}
|
||||||
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const client of addedClients) {
|
|
||||||
olmJobs.push(
|
olmJobs.push(
|
||||||
addPeerData(
|
addPeerDataBatch(
|
||||||
client.clientId,
|
addedClients.map((client) => ({
|
||||||
|
clientId: client.clientId,
|
||||||
siteId,
|
siteId,
|
||||||
generateRemoteSubnets([siteResource]),
|
remoteSubnets: generateRemoteSubnets([
|
||||||
generateAliasConfig([siteResource])
|
siteResource
|
||||||
|
]),
|
||||||
|
aliases: generateAliasConfig([siteResource])
|
||||||
|
}))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// here we use the existingSiteResource from BEFORE we updated the destination so we dont need to worry about updating destinations here
|
// here we use the existingSiteResource from BEFORE we updated the destination so we dont need to worry about updating destinations here
|
||||||
|
|
||||||
@@ -881,14 +952,23 @@ async function handleSubnetProxyTargetUpdates(
|
|||||||
|
|
||||||
if (targetsToRemove) {
|
if (targetsToRemove) {
|
||||||
proxyJobs.push(
|
proxyJobs.push(
|
||||||
removeSubnetProxyTargets(
|
removeSubnetProxyTargetsBatch([
|
||||||
newt.newtId,
|
{
|
||||||
targetsToRemove,
|
newtId: newt.newtId,
|
||||||
newt.version
|
targets: targetsToRemove,
|
||||||
)
|
version: newt.version
|
||||||
|
}
|
||||||
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const peerDataRemovals: {
|
||||||
|
clientId: number;
|
||||||
|
siteId: number;
|
||||||
|
remoteSubnets: string[];
|
||||||
|
aliases: ReturnType<typeof generateAliasConfig>;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
for (const client of removedClients) {
|
for (const client of removedClients) {
|
||||||
if (!siteResource.destination) {
|
if (!siteResource.destination) {
|
||||||
continue;
|
continue;
|
||||||
@@ -936,14 +1016,16 @@ async function handleSubnetProxyTargetUpdates(
|
|||||||
? []
|
? []
|
||||||
: generateRemoteSubnets([siteResource]);
|
: generateRemoteSubnets([siteResource]);
|
||||||
|
|
||||||
olmJobs.push(
|
peerDataRemovals.push({
|
||||||
removePeerData(
|
clientId: client.clientId,
|
||||||
client.clientId,
|
|
||||||
siteId,
|
siteId,
|
||||||
remoteSubnetsToRemove,
|
remoteSubnets: remoteSubnetsToRemove,
|
||||||
generateAliasConfig([siteResource])
|
aliases: generateAliasConfig([siteResource])
|
||||||
)
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
|
if (peerDataRemovals.length > 0) {
|
||||||
|
olmJobs.push(removePeerDataBatch(peerDataRemovals));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -956,11 +1038,28 @@ export async function rebuildClientAssociationsFromClient(
|
|||||||
client: Client,
|
client: Client,
|
||||||
trx: Transaction | typeof db = db
|
trx: Transaction | typeof db = db
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
try {
|
||||||
return await lockManager.withLock(
|
return await lockManager.withLock(
|
||||||
`rebuild-client-associations:client:${client.clientId}`,
|
`rebuild-client-associations:client:${client.clientId}`,
|
||||||
() => rebuildClientAssociationsFromClientImpl(client, trx),
|
() => rebuildClientAssociationsFromClientImpl(client, trx),
|
||||||
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
|
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
|
||||||
);
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (
|
||||||
|
typeof err?.message === "string" &&
|
||||||
|
err.message.startsWith("Failed to acquire lock")
|
||||||
|
) {
|
||||||
|
logger.warn(
|
||||||
|
`rebuildClientAssociations: could not acquire lock for client ${client.clientId}, queuing for deferred processing`
|
||||||
|
);
|
||||||
|
await rebuildQueue.enqueue({
|
||||||
|
type: "client",
|
||||||
|
id: client.clientId
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rebuildClientAssociationsFromClientImpl(
|
async function rebuildClientAssociationsFromClientImpl(
|
||||||
@@ -1237,6 +1336,28 @@ async function handleMessagesForClientSites(
|
|||||||
const newtJobs: Promise<any>[] = [];
|
const newtJobs: Promise<any>[] = [];
|
||||||
const olmJobs: Promise<any>[] = [];
|
const olmJobs: Promise<any>[] = [];
|
||||||
const exitNodeJobs: Promise<any>[] = [];
|
const exitNodeJobs: Promise<any>[] = [];
|
||||||
|
const newtPeerDeletes: {
|
||||||
|
siteId: number;
|
||||||
|
publicKey: string;
|
||||||
|
newtId: string;
|
||||||
|
}[] = [];
|
||||||
|
const olmPeerDeletes: {
|
||||||
|
clientId: number;
|
||||||
|
siteId: number;
|
||||||
|
publicKey: string;
|
||||||
|
olmId: string;
|
||||||
|
}[] = [];
|
||||||
|
const olmPeerAddHandshakes: {
|
||||||
|
clientId: number;
|
||||||
|
peer: {
|
||||||
|
siteId: number;
|
||||||
|
exitNode: {
|
||||||
|
publicKey: string;
|
||||||
|
endpoint: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
olmId: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
const totalSitesOnClient = await trx
|
const totalSitesOnClient = await trx
|
||||||
.select({ count: count(clientSitesAssociationsCache.siteId) })
|
.select({ count: count(clientSitesAssociationsCache.siteId) })
|
||||||
@@ -1268,19 +1389,19 @@ async function handleMessagesForClientSites(
|
|||||||
|
|
||||||
if (isRemove) {
|
if (isRemove) {
|
||||||
// Remove peer from newt
|
// Remove peer from newt
|
||||||
newtJobs.push(
|
newtPeerDeletes.push({
|
||||||
newtDeletePeer(site.siteId, client.pubKey, newt.newtId)
|
siteId: site.siteId,
|
||||||
);
|
publicKey: client.pubKey,
|
||||||
|
newtId: newt.newtId
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
// Remove peer from olm
|
// Remove peer from olm
|
||||||
olmJobs.push(
|
olmPeerDeletes.push({
|
||||||
olmDeletePeer(
|
clientId: client.clientId,
|
||||||
client.clientId,
|
siteId: site.siteId,
|
||||||
site.siteId,
|
publicKey: site.publicKey,
|
||||||
site.publicKey,
|
|
||||||
olmId
|
olmId
|
||||||
)
|
});
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// if the error includes not found then its just because the olm does not exist anymore or yet and its fine if we dont send
|
// if the error includes not found then its just because the olm does not exist anymore or yet and its fine if we dont send
|
||||||
if (
|
if (
|
||||||
@@ -1312,10 +1433,9 @@ async function handleMessagesForClientSites(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await initPeerAddHandshake(
|
olmPeerAddHandshakes.push({
|
||||||
// this will kick off the add peer process for the client
|
clientId: client.clientId,
|
||||||
client.clientId,
|
peer: {
|
||||||
{
|
|
||||||
siteId: site.siteId,
|
siteId: site.siteId,
|
||||||
exitNode: {
|
exitNode: {
|
||||||
publicKey: exitNode.publicKey,
|
publicKey: exitNode.publicKey,
|
||||||
@@ -1323,7 +1443,7 @@ async function handleMessagesForClientSites(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
olmId
|
olmId
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update exit node destinations
|
// Update exit node destinations
|
||||||
@@ -1339,6 +1459,18 @@ async function handleMessagesForClientSites(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newtPeerDeletes.length > 0) {
|
||||||
|
newtJobs.push(newtDeletePeersBatch(newtPeerDeletes));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (olmPeerDeletes.length > 0) {
|
||||||
|
olmJobs.push(olmDeletePeersBatch(olmPeerDeletes));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (olmPeerAddHandshakes.length > 0) {
|
||||||
|
olmJobs.push(initPeerAddHandshakeBatch(olmPeerAddHandshakes));
|
||||||
|
}
|
||||||
|
|
||||||
Promise.all(exitNodeJobs).catch((error) => {
|
Promise.all(exitNodeJobs).catch((error) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
`rebuildClientAssociations: Error updating client site destinations for client ${client.clientId}:`,
|
`rebuildClientAssociations: Error updating client site destinations for client ${client.clientId}:`,
|
||||||
@@ -1437,6 +1569,20 @@ async function handleMessagesForClientResources(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const targetsToAddBatch: {
|
||||||
|
newtId: string;
|
||||||
|
targets: NonNullable<
|
||||||
|
Awaited<ReturnType<typeof generateSubnetProxyTargetV2>>
|
||||||
|
>;
|
||||||
|
version: string | null;
|
||||||
|
}[] = [];
|
||||||
|
const peerDataAdds: {
|
||||||
|
clientId: number;
|
||||||
|
siteId: number;
|
||||||
|
remoteSubnets: string[];
|
||||||
|
aliases: ReturnType<typeof generateAliasConfig>;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
for (const resource of resources) {
|
for (const resource of resources) {
|
||||||
const targets = await generateSubnetProxyTargetV2(resource, [
|
const targets = await generateSubnetProxyTargetV2(resource, [
|
||||||
{
|
{
|
||||||
@@ -1447,25 +1593,21 @@ async function handleMessagesForClientResources(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (targets) {
|
if (targets) {
|
||||||
proxyJobs.push(
|
targetsToAddBatch.push({
|
||||||
addSubnetProxyTargets(
|
newtId: newt.newtId,
|
||||||
newt.newtId,
|
|
||||||
targets,
|
targets,
|
||||||
newt.version
|
version: newt.version
|
||||||
)
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Add peer data to olm
|
// Add peer data to olm
|
||||||
olmJobs.push(
|
peerDataAdds.push({
|
||||||
addPeerData(
|
clientId: client.clientId,
|
||||||
client.clientId,
|
|
||||||
siteId,
|
siteId,
|
||||||
generateRemoteSubnets([resource]),
|
remoteSubnets: generateRemoteSubnets([resource]),
|
||||||
generateAliasConfig([resource])
|
aliases: generateAliasConfig([resource])
|
||||||
)
|
});
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// if the error includes not found then its just because the olm does not exist anymore or yet and its fine if we dont send
|
// if the error includes not found then its just because the olm does not exist anymore or yet and its fine if we dont send
|
||||||
if (
|
if (
|
||||||
@@ -1480,6 +1622,14 @@ async function handleMessagesForClientResources(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (targetsToAddBatch.length > 0) {
|
||||||
|
proxyJobs.push(addSubnetProxyTargetsBatch(targetsToAddBatch));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (peerDataAdds.length > 0) {
|
||||||
|
olmJobs.push(addPeerDataBatch(peerDataAdds));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1546,6 +1696,20 @@ async function handleMessagesForClientResources(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const targetsToRemoveBatch: {
|
||||||
|
newtId: string;
|
||||||
|
targets: NonNullable<
|
||||||
|
Awaited<ReturnType<typeof generateSubnetProxyTargetV2>>
|
||||||
|
>;
|
||||||
|
version: string | null;
|
||||||
|
}[] = [];
|
||||||
|
const peerDataRemovals: {
|
||||||
|
clientId: number;
|
||||||
|
siteId: number;
|
||||||
|
remoteSubnets: string[];
|
||||||
|
aliases: ReturnType<typeof generateAliasConfig>;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
for (const resource of resources) {
|
for (const resource of resources) {
|
||||||
const targets = await generateSubnetProxyTargetV2(resource, [
|
const targets = await generateSubnetProxyTargetV2(resource, [
|
||||||
{
|
{
|
||||||
@@ -1556,13 +1720,11 @@ async function handleMessagesForClientResources(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (targets) {
|
if (targets) {
|
||||||
proxyJobs.push(
|
targetsToRemoveBatch.push({
|
||||||
removeSubnetProxyTargets(
|
newtId: newt.newtId,
|
||||||
newt.newtId,
|
|
||||||
targets,
|
targets,
|
||||||
newt.version
|
version: newt.version
|
||||||
)
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1613,14 +1775,12 @@ async function handleMessagesForClientResources(
|
|||||||
: generateRemoteSubnets([resource]);
|
: generateRemoteSubnets([resource]);
|
||||||
|
|
||||||
// Remove peer data from olm
|
// Remove peer data from olm
|
||||||
olmJobs.push(
|
peerDataRemovals.push({
|
||||||
removePeerData(
|
clientId: client.clientId,
|
||||||
client.clientId,
|
|
||||||
siteId,
|
siteId,
|
||||||
remoteSubnetsToRemove,
|
remoteSubnets: remoteSubnetsToRemove,
|
||||||
generateAliasConfig([resource])
|
aliases: generateAliasConfig([resource])
|
||||||
)
|
});
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// if the error includes not found then its just because the olm does not exist anymore or yet and its fine if we dont send
|
// if the error includes not found then its just because the olm does not exist anymore or yet and its fine if we dont send
|
||||||
if (
|
if (
|
||||||
@@ -1635,6 +1795,16 @@ async function handleMessagesForClientResources(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (targetsToRemoveBatch.length > 0) {
|
||||||
|
proxyJobs.push(
|
||||||
|
removeSubnetProxyTargetsBatch(targetsToRemoveBatch)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (peerDataRemovals.length > 0) {
|
||||||
|
olmJobs.push(removePeerDataBatch(peerDataRemovals));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1888,7 +2058,15 @@ export async function cleanupSiteAssociations(
|
|||||||
for (const client of allClients) {
|
for (const client of allClients) {
|
||||||
// Tell each olm to drop the site's WireGuard peer.
|
// Tell each olm to drop the site's WireGuard peer.
|
||||||
if (site.publicKey) {
|
if (site.publicKey) {
|
||||||
jobs.push(olmDeletePeer(client.clientId, siteId, site.publicKey));
|
jobs.push(
|
||||||
|
olmDeletePeersBatch([
|
||||||
|
{
|
||||||
|
clientId: client.clientId,
|
||||||
|
siteId,
|
||||||
|
publicKey: site.publicKey
|
||||||
|
}
|
||||||
|
])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recompute and push updated relay destinations (now excluding this site).
|
// Recompute and push updated relay destinations (now excluding this site).
|
||||||
@@ -1906,3 +2084,47 @@ export async function cleanupSiteAssociations(
|
|||||||
|
|
||||||
logger.debug(`cleanupSiteAssociations: DONE siteId=${siteId}`);
|
logger.debug(`cleanupSiteAssociations: DONE siteId=${siteId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the background rebuild queue processor. This should be called once
|
||||||
|
* during server startup. Only one server instance at a time will actively
|
||||||
|
* consume the queue (enforced via a distributed Redis lock); all other
|
||||||
|
* instances will poll and wait until the lock becomes available.
|
||||||
|
*/
|
||||||
|
export function startRebuildQueueProcessor(): void {
|
||||||
|
rebuildQueue.startProcessing({
|
||||||
|
onSiteResource: async (siteResourceId: number) => {
|
||||||
|
const [siteResource] = await primaryDb
|
||||||
|
.select()
|
||||||
|
.from(siteResources)
|
||||||
|
.where(eq(siteResources.siteResourceId, siteResourceId));
|
||||||
|
|
||||||
|
if (!siteResource) {
|
||||||
|
logger.warn(
|
||||||
|
`Rebuild queue: site resource ${siteResourceId} not found, skipping`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await rebuildClientAssociationsFromSiteResource(
|
||||||
|
siteResource,
|
||||||
|
primaryDb
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onClient: async (clientId: number) => {
|
||||||
|
const [client] = await primaryDb
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.clientId, clientId));
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
logger.warn(
|
||||||
|
`Rebuild queue: client ${clientId} not found, skipping`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await rebuildClientAssociationsFromClient(client, primaryDb);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
23
server/lib/rebuildQueue.ts
Normal file
23
server/lib/rebuildQueue.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export type RebuildJobType = "site-resource" | "client";
|
||||||
|
|
||||||
|
export interface RebuildJob {
|
||||||
|
type: RebuildJobType;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RebuildJobHandlers {
|
||||||
|
onSiteResource(siteResourceId: number): Promise<void>;
|
||||||
|
onClient(clientId: number): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RebuildQueueManager {
|
||||||
|
enqueue(job: RebuildJob): Promise<void>;
|
||||||
|
startProcessing(handlers: RebuildJobHandlers): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoopRebuildQueue implements RebuildQueueManager {
|
||||||
|
async enqueue(_job: RebuildJob): Promise<void> {}
|
||||||
|
startProcessing(_handlers: RebuildJobHandlers): void {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rebuildQueue: RebuildQueueManager = new NoopRebuildQueue();
|
||||||
198
server/private/lib/rebuildQueue.ts
Normal file
198
server/private/lib/rebuildQueue.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { redis } from "#private/lib/redis";
|
||||||
|
import { lockManager } from "#dynamic/lib/lock";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
export type RebuildJobType = "site-resource" | "client";
|
||||||
|
|
||||||
|
export interface RebuildJob {
|
||||||
|
type: RebuildJobType;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RebuildJobHandlers {
|
||||||
|
onSiteResource(siteResourceId: number): Promise<void>;
|
||||||
|
onClient(clientId: number): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redis list holding pending rebuild jobs (RPUSH to enqueue, LPOP to dequeue — FIFO order).
|
||||||
|
const QUEUE_KEY = "rebuild-client-associations:queue";
|
||||||
|
const QUEUED_SET_KEY = "rebuild-client-associations:queued";
|
||||||
|
|
||||||
|
// Distributed lock that serialises queue consumption to a single server instance
|
||||||
|
// at a time. TTL is generous enough to cover a full batch of expensive rebuilds.
|
||||||
|
const PROCESSOR_LOCK_KEY = "rebuild-client-associations:processor";
|
||||||
|
|
||||||
|
// Each rebuild can take up to REBUILD_ASSOCIATIONS_LOCK_TTL_MS (120 s) per
|
||||||
|
// resource. Allow BATCH_SIZE resources per processor-lock acquisition, plus a
|
||||||
|
// small buffer.
|
||||||
|
const BATCH_SIZE = 5;
|
||||||
|
const PROCESSOR_LOCK_TTL_MS = 120000 * BATCH_SIZE + 30000; // ~630 s
|
||||||
|
|
||||||
|
const POLL_INTERVAL_MS = 500;
|
||||||
|
|
||||||
|
class RedisRebuildQueue {
|
||||||
|
private processingStarted = false;
|
||||||
|
|
||||||
|
async enqueue(job: RebuildJob): Promise<void> {
|
||||||
|
if (!redis || redis.status !== "ready") {
|
||||||
|
logger.warn(
|
||||||
|
`Rebuild queue: Redis not available — rebuild for ${job.type}:${job.id} will not be retried`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dedupeKey = `${job.type}:${job.id}`;
|
||||||
|
const added = await redis.sadd(QUEUED_SET_KEY, dedupeKey);
|
||||||
|
if (added === 0) {
|
||||||
|
logger.debug(
|
||||||
|
`Rebuild queue: skipped duplicate queued job ${job.type}:${job.id}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await redis.rpush(QUEUE_KEY, JSON.stringify(job));
|
||||||
|
logger.debug(
|
||||||
|
`Rebuild queue: enqueued ${job.type}:${job.id} (queue position: tail)`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
await redis
|
||||||
|
.srem(QUEUED_SET_KEY, `${job.type}:${job.id}`)
|
||||||
|
.catch((cleanupErr) =>
|
||||||
|
logger.warn(
|
||||||
|
`Rebuild queue: failed to cleanup dedupe key for ${job.type}:${job.id} after enqueue failure:`,
|
||||||
|
cleanupErr
|
||||||
|
)
|
||||||
|
);
|
||||||
|
logger.error(
|
||||||
|
`Rebuild queue: failed to enqueue ${job.type}:${job.id}:`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startProcessing(handlers: RebuildJobHandlers): void {
|
||||||
|
if (this.processingStarted) return;
|
||||||
|
this.processingStarted = true;
|
||||||
|
|
||||||
|
this.processLoop(handlers).catch((err) => {
|
||||||
|
logger.error("Rebuild queue processor loop crashed:", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info("Rebuild queue processor started");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processLoop(handlers: RebuildJobHandlers): Promise<void> {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
await this.tryProcessBatch(handlers);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
"Rebuild queue: unhandled error in process loop:",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, POLL_INTERVAL_MS)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async tryProcessBatch(handlers: RebuildJobHandlers): Promise<void> {
|
||||||
|
if (!redis || redis.status !== "ready") return;
|
||||||
|
|
||||||
|
// Peek before acquiring the processor lock to avoid unnecessary Redis
|
||||||
|
// round-trips and lock contention when the queue is idle.
|
||||||
|
const queueLength = await redis.llen(QUEUE_KEY).catch(() => 0);
|
||||||
|
if (queueLength === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await lockManager.withLock(
|
||||||
|
PROCESSOR_LOCK_KEY,
|
||||||
|
async () => {
|
||||||
|
for (let i = 0; i < BATCH_SIZE; i++) {
|
||||||
|
if (!redis || redis.status !== "ready") break;
|
||||||
|
|
||||||
|
const payload = await redis.lpop(QUEUE_KEY);
|
||||||
|
if (payload === null) break; // queue drained
|
||||||
|
|
||||||
|
let job: RebuildJob;
|
||||||
|
try {
|
||||||
|
job = JSON.parse(payload) as RebuildJob;
|
||||||
|
} catch {
|
||||||
|
logger.error(
|
||||||
|
`Rebuild queue: could not parse job payload, discarding: ${payload}`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from dedupe set once dequeued so the same job
|
||||||
|
// can be re-queued while this one is in progress.
|
||||||
|
await redis
|
||||||
|
.srem(QUEUED_SET_KEY, `${job.type}:${job.id}`)
|
||||||
|
.catch((cleanupErr) =>
|
||||||
|
logger.warn(
|
||||||
|
`Rebuild queue: failed to remove dedupe key for ${job.type}:${job.id} on dequeue:`,
|
||||||
|
cleanupErr
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Rebuild queue: processing ${job.type}:${job.id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (job.type === "site-resource") {
|
||||||
|
await handlers.onSiteResource(job.id);
|
||||||
|
} else if (job.type === "client") {
|
||||||
|
await handlers.onClient(job.id);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
`Rebuild queue: unknown job type "${(job as any).type}", discarding`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Rebuild queue: completed ${job.type}:${job.id}`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
`Rebuild queue: job ${job.type}:${job.id} threw an error:`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PROCESSOR_LOCK_TTL_MS
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (
|
||||||
|
typeof err?.message === "string" &&
|
||||||
|
err.message.startsWith("Failed to acquire lock")
|
||||||
|
) {
|
||||||
|
// Another server instance currently holds the processor lock and
|
||||||
|
// is consuming the queue — nothing to do this cycle.
|
||||||
|
logger.debug(
|
||||||
|
"Rebuild queue: processor lock held by another instance, skipping this cycle"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rebuildQueue: RedisRebuildQueue = new RedisRebuildQueue();
|
||||||
@@ -38,6 +38,7 @@ import { messageHandlers } from "@server/routers/ws/messageHandlers";
|
|||||||
import { messageHandlers as privateMessageHandlers } from "#private/routers/ws/messageHandlers";
|
import { messageHandlers as privateMessageHandlers } from "#private/routers/ws/messageHandlers";
|
||||||
import {
|
import {
|
||||||
AuthenticatedWebSocket,
|
AuthenticatedWebSocket,
|
||||||
|
BatchSendMessage,
|
||||||
ClientType,
|
ClientType,
|
||||||
WSMessage,
|
WSMessage,
|
||||||
TokenPayload,
|
TokenPayload,
|
||||||
@@ -187,6 +188,8 @@ const wss: WebSocketServer = new WebSocketServer({ noServer: true });
|
|||||||
// Generate unique node ID for this instance
|
// Generate unique node ID for this instance
|
||||||
const NODE_ID = uuidv4();
|
const NODE_ID = uuidv4();
|
||||||
const REDIS_CHANNEL = "websocket_messages";
|
const REDIS_CHANNEL = "websocket_messages";
|
||||||
|
const REDIS_DIRECT_BATCH_SIZE = 250;
|
||||||
|
const REDIS_DIRECT_FLUSH_INTERVAL_MS = 10;
|
||||||
|
|
||||||
// Client tracking map (local to this node)
|
// Client tracking map (local to this node)
|
||||||
const connectedClients: Map<string, AuthenticatedWebSocket[]> = new Map();
|
const connectedClients: Map<string, AuthenticatedWebSocket[]> = new Map();
|
||||||
@@ -197,6 +200,15 @@ const clientConfigVersions: Map<string, number> = new Map();
|
|||||||
// Recovery tracking
|
// Recovery tracking
|
||||||
let isRedisRecoveryInProgress = false;
|
let isRedisRecoveryInProgress = false;
|
||||||
|
|
||||||
|
interface RedisDirectBatchEntry {
|
||||||
|
targetClientId: string;
|
||||||
|
message: WSMessage;
|
||||||
|
resolve: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pendingRedisDirectMessages: RedisDirectBatchEntry[] = [];
|
||||||
|
let redisDirectFlushTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
// Helper to get map key
|
// Helper to get map key
|
||||||
const getClientMapKey = (clientId: string) => clientId;
|
const getClientMapKey = (clientId: string) => clientId;
|
||||||
|
|
||||||
@@ -207,6 +219,78 @@ const getNodeConnectionsKey = (nodeId: string, clientId: string) =>
|
|||||||
const getConfigVersionKey = (clientId: string) =>
|
const getConfigVersionKey = (clientId: string) =>
|
||||||
`ws:configVersion:${clientId}`;
|
`ws:configVersion:${clientId}`;
|
||||||
|
|
||||||
|
const clearRedisDirectFlushTimer = (): void => {
|
||||||
|
if (redisDirectFlushTimer) {
|
||||||
|
clearTimeout(redisDirectFlushTimer);
|
||||||
|
redisDirectFlushTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const publishDirectBatch = async (
|
||||||
|
entries: RedisDirectBatchEntry[]
|
||||||
|
): Promise<void> => {
|
||||||
|
const redisMessage: RedisMessage = {
|
||||||
|
type: "direct-batch",
|
||||||
|
messages: entries.map((entry) => ({
|
||||||
|
targetClientId: entry.targetClientId,
|
||||||
|
message: entry.message
|
||||||
|
})),
|
||||||
|
fromNodeId: NODE_ID
|
||||||
|
};
|
||||||
|
|
||||||
|
await redisManager.publish(REDIS_CHANNEL, JSON.stringify(redisMessage));
|
||||||
|
};
|
||||||
|
|
||||||
|
const flushPendingRedisDirectMessages = async (): Promise<void> => {
|
||||||
|
clearRedisDirectFlushTimer();
|
||||||
|
|
||||||
|
if (pendingRedisDirectMessages.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = pendingRedisDirectMessages;
|
||||||
|
pendingRedisDirectMessages = [];
|
||||||
|
|
||||||
|
if (!redisManager.isRedisEnabled()) {
|
||||||
|
entries.forEach((entry) => entry.resolve());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < entries.length; i += REDIS_DIRECT_BATCH_SIZE) {
|
||||||
|
const batch = entries.slice(i, i + REDIS_DIRECT_BATCH_SIZE);
|
||||||
|
try {
|
||||||
|
await publishDirectBatch(batch);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"Failed to send batched direct messages via Redis, messages may be lost:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
batch.forEach((entry) => entry.resolve());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const enqueueRedisDirectMessage = async (
|
||||||
|
targetClientId: string,
|
||||||
|
message: WSMessage
|
||||||
|
): Promise<void> => {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
pendingRedisDirectMessages.push({ targetClientId, message, resolve });
|
||||||
|
|
||||||
|
if (pendingRedisDirectMessages.length >= REDIS_DIRECT_BATCH_SIZE) {
|
||||||
|
void flushPendingRedisDirectMessages();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!redisDirectFlushTimer) {
|
||||||
|
redisDirectFlushTimer = setTimeout(() => {
|
||||||
|
void flushPendingRedisDirectMessages();
|
||||||
|
}, REDIS_DIRECT_FLUSH_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize Redis subscription for cross-node messaging
|
// Initialize Redis subscription for cross-node messaging
|
||||||
const initializeRedisSubscription = async (): Promise<void> => {
|
const initializeRedisSubscription = async (): Promise<void> => {
|
||||||
if (!redisManager.isRedisEnabled()) return;
|
if (!redisManager.isRedisEnabled()) return;
|
||||||
@@ -227,7 +311,16 @@ const initializeRedisSubscription = async (): Promise<void> => {
|
|||||||
// Send to specific client on this node
|
// Send to specific client on this node
|
||||||
await sendToClientLocal(
|
await sendToClientLocal(
|
||||||
redisMessage.targetClientId,
|
redisMessage.targetClientId,
|
||||||
redisMessage.message
|
redisMessage.message,
|
||||||
|
{},
|
||||||
|
redisMessage.message.configVersion
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
redisMessage.type === "direct-batch" &&
|
||||||
|
redisMessage.messages
|
||||||
|
) {
|
||||||
|
await sendRedisDirectBatchToLocalClients(
|
||||||
|
redisMessage.messages
|
||||||
);
|
);
|
||||||
} else if (redisMessage.type === "broadcast") {
|
} else if (redisMessage.type === "broadcast") {
|
||||||
// Broadcast to all clients on this node except excluded
|
// Broadcast to all clients on this node except excluded
|
||||||
@@ -503,7 +596,8 @@ const incrementClientConfigVersion = async (
|
|||||||
const sendToClientLocal = async (
|
const sendToClientLocal = async (
|
||||||
clientId: string,
|
clientId: string,
|
||||||
message: WSMessage,
|
message: WSMessage,
|
||||||
options: SendMessageOptions = {}
|
options: SendMessageOptions = {},
|
||||||
|
preResolvedConfigVersion?: number
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
const mapKey = getClientMapKey(clientId);
|
const mapKey = getClientMapKey(clientId);
|
||||||
const clients = connectedClients.get(mapKey);
|
const clients = connectedClients.get(mapKey);
|
||||||
@@ -512,7 +606,8 @@ const sendToClientLocal = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle config version
|
// Handle config version
|
||||||
const configVersion = await getClientConfigVersion(clientId);
|
const configVersion =
|
||||||
|
preResolvedConfigVersion ?? (await getClientConfigVersion(clientId));
|
||||||
|
|
||||||
// Add config version to message
|
// Add config version to message
|
||||||
const messageWithVersion = {
|
const messageWithVersion = {
|
||||||
@@ -545,6 +640,20 @@ const sendToClientLocal = async (
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sendRedisDirectBatchToLocalClients = async (
|
||||||
|
entries: { targetClientId: string; message: WSMessage }[]
|
||||||
|
): Promise<void> => {
|
||||||
|
const jobs = entries.map((entry) =>
|
||||||
|
sendToClientLocal(
|
||||||
|
entry.targetClientId,
|
||||||
|
entry.message,
|
||||||
|
{},
|
||||||
|
entry.message.configVersion
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await Promise.all(jobs);
|
||||||
|
};
|
||||||
|
|
||||||
const broadcastToAllExceptLocal = async (
|
const broadcastToAllExceptLocal = async (
|
||||||
message: WSMessage,
|
message: WSMessage,
|
||||||
excludeClientId?: string,
|
excludeClientId?: string,
|
||||||
@@ -607,23 +716,13 @@ const sendToClient = async (
|
|||||||
// Only send via Redis if the client is not connected locally and Redis is enabled
|
// Only send via Redis if the client is not connected locally and Redis is enabled
|
||||||
if (!localSent && redisManager.isRedisEnabled()) {
|
if (!localSent && redisManager.isRedisEnabled()) {
|
||||||
try {
|
try {
|
||||||
const redisMessage: RedisMessage = {
|
await enqueueRedisDirectMessage(clientId, {
|
||||||
type: "direct",
|
|
||||||
targetClientId: clientId,
|
|
||||||
message: {
|
|
||||||
...message,
|
...message,
|
||||||
configVersion
|
configVersion
|
||||||
},
|
});
|
||||||
fromNodeId: NODE_ID
|
|
||||||
};
|
|
||||||
|
|
||||||
await redisManager.publish(
|
|
||||||
REDIS_CHANNEL,
|
|
||||||
JSON.stringify(redisMessage)
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
"Failed to send message via Redis, message may be lost:",
|
"Failed to queue batched direct message for Redis delivery, message may be lost:",
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
// Continue execution - local delivery already attempted
|
// Continue execution - local delivery already attempted
|
||||||
@@ -638,6 +737,76 @@ const sendToClient = async (
|
|||||||
return localSent;
|
return localSent;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sendToClientsBatch = async (
|
||||||
|
entries: BatchSendMessage[]
|
||||||
|
): Promise<void> => {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteEntries: { targetClientId: string; message: WSMessage }[] = [];
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const options = entry.options || {};
|
||||||
|
const { clientId, message } = entry;
|
||||||
|
|
||||||
|
let configVersion = await getClientConfigVersion(clientId);
|
||||||
|
if (options.incrementConfigVersion) {
|
||||||
|
configVersion = await incrementClientConfigVersion(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`sendToClientsBatch: Message type ${message.type} queued for clientId ${clientId} (new configVersion: ${configVersion})`
|
||||||
|
);
|
||||||
|
|
||||||
|
const localSent = await sendToClientLocal(
|
||||||
|
clientId,
|
||||||
|
message,
|
||||||
|
options,
|
||||||
|
configVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!localSent && redisManager.isRedisEnabled()) {
|
||||||
|
remoteEntries.push({
|
||||||
|
targetClientId: clientId,
|
||||||
|
message: {
|
||||||
|
...message,
|
||||||
|
configVersion
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (!localSent && !redisManager.isRedisEnabled()) {
|
||||||
|
logger.debug(
|
||||||
|
`Could not deliver batch message to ${clientId} - not connected locally and Redis unavailable`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!redisManager.isRedisEnabled() || remoteEntries.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < remoteEntries.length; i += REDIS_DIRECT_BATCH_SIZE) {
|
||||||
|
const messages = remoteEntries.slice(i, i + REDIS_DIRECT_BATCH_SIZE);
|
||||||
|
try {
|
||||||
|
const redisMessage: RedisMessage = {
|
||||||
|
type: "direct-batch",
|
||||||
|
messages,
|
||||||
|
fromNodeId: NODE_ID
|
||||||
|
};
|
||||||
|
|
||||||
|
await redisManager.publish(
|
||||||
|
REDIS_CHANNEL,
|
||||||
|
JSON.stringify(redisMessage)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"Failed to send explicit direct batch via Redis, messages may be lost:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const broadcastToAllExcept = async (
|
const broadcastToAllExcept = async (
|
||||||
message: WSMessage,
|
message: WSMessage,
|
||||||
excludeClientId?: string,
|
excludeClientId?: string,
|
||||||
@@ -1109,6 +1278,8 @@ const disconnectClient = async (clientId: string): Promise<boolean> => {
|
|||||||
// Cleanup function for graceful shutdown
|
// Cleanup function for graceful shutdown
|
||||||
const cleanup = async (): Promise<void> => {
|
const cleanup = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
await flushPendingRedisDirectMessages();
|
||||||
|
|
||||||
// Close all WebSocket connections
|
// Close all WebSocket connections
|
||||||
connectedClients.forEach((clients) => {
|
connectedClients.forEach((clients) => {
|
||||||
clients.forEach((client) => {
|
clients.forEach((client) => {
|
||||||
@@ -1139,6 +1310,7 @@ export {
|
|||||||
router,
|
router,
|
||||||
handleWSUpgrade,
|
handleWSUpgrade,
|
||||||
sendToClient,
|
sendToClient,
|
||||||
|
sendToClientsBatch,
|
||||||
broadcastToAllExcept,
|
broadcastToAllExcept,
|
||||||
connectedClients,
|
connectedClients,
|
||||||
hasActiveConnections,
|
hasActiveConnections,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { sendToClient } from "#dynamic/routers/ws";
|
import { sendToClient, sendToClientsBatch } from "#dynamic/routers/ws";
|
||||||
import { db, newts, olms } from "@server/db";
|
import { db, newts, olms } from "@server/db";
|
||||||
import {
|
import {
|
||||||
Alias,
|
Alias,
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "@server/lib/ip";
|
} from "@server/lib/ip";
|
||||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, inArray } from "drizzle-orm";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
|
|
||||||
const NEWT_V2_TARGETS_VERSION = ">=1.10.3";
|
const NEWT_V2_TARGETS_VERSION = ">=1.10.3";
|
||||||
@@ -59,6 +59,42 @@ export async function addTargets(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function addTargetsBatch(
|
||||||
|
entries: {
|
||||||
|
newtId: string;
|
||||||
|
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[];
|
||||||
|
version?: string | null;
|
||||||
|
}[]
|
||||||
|
) {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = await Promise.all(
|
||||||
|
entries.map(async (entry) => ({
|
||||||
|
...entry,
|
||||||
|
targets: await convertTargetsIfNecessary(
|
||||||
|
entry.newtId,
|
||||||
|
entry.targets
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
await sendToClientsBatch(
|
||||||
|
resolved.map((entry) => ({
|
||||||
|
clientId: entry.newtId,
|
||||||
|
message: {
|
||||||
|
type: `newt/wg/targets/add`,
|
||||||
|
data: entry.targets
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
incrementConfigVersion: true,
|
||||||
|
compress: canCompress(entry.version, "newt")
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function removeTargets(
|
export async function removeTargets(
|
||||||
newtId: string,
|
newtId: string,
|
||||||
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[],
|
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[],
|
||||||
@@ -76,6 +112,42 @@ export async function removeTargets(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function removeTargetsBatch(
|
||||||
|
entries: {
|
||||||
|
newtId: string;
|
||||||
|
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[];
|
||||||
|
version?: string | null;
|
||||||
|
}[]
|
||||||
|
) {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = await Promise.all(
|
||||||
|
entries.map(async (entry) => ({
|
||||||
|
...entry,
|
||||||
|
targets: await convertTargetsIfNecessary(
|
||||||
|
entry.newtId,
|
||||||
|
entry.targets
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
await sendToClientsBatch(
|
||||||
|
resolved.map((entry) => ({
|
||||||
|
clientId: entry.newtId,
|
||||||
|
message: {
|
||||||
|
type: `newt/wg/targets/remove`,
|
||||||
|
data: entry.targets
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
incrementConfigVersion: true,
|
||||||
|
compress: canCompress(entry.version, "newt")
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateTargets(
|
export async function updateTargets(
|
||||||
newtId: string,
|
newtId: string,
|
||||||
targets: {
|
targets: {
|
||||||
@@ -201,6 +273,171 @@ export async function removePeerData(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolveOlmTargets = async (
|
||||||
|
entries: {
|
||||||
|
clientId: number;
|
||||||
|
olmId?: string;
|
||||||
|
version?: string | null;
|
||||||
|
}[]
|
||||||
|
) => {
|
||||||
|
const unresolvedClientIds = entries
|
||||||
|
.filter((entry) => !entry.olmId)
|
||||||
|
.map((entry) => entry.clientId);
|
||||||
|
|
||||||
|
const olmMap = new Map<number, { olmId: string; version: string | null }>();
|
||||||
|
|
||||||
|
if (unresolvedClientIds.length > 0) {
|
||||||
|
const olmRows = await db
|
||||||
|
.select({
|
||||||
|
clientId: olms.clientId,
|
||||||
|
olmId: olms.olmId,
|
||||||
|
version: olms.version
|
||||||
|
})
|
||||||
|
.from(olms)
|
||||||
|
.where(inArray(olms.clientId, unresolvedClientIds));
|
||||||
|
|
||||||
|
for (const row of olmRows) {
|
||||||
|
if (row.clientId !== null) {
|
||||||
|
olmMap.set(row.clientId, {
|
||||||
|
olmId: row.olmId,
|
||||||
|
version: row.version
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
.map((entry) => {
|
||||||
|
if (entry.olmId) {
|
||||||
|
return {
|
||||||
|
clientId: entry.clientId,
|
||||||
|
olmId: entry.olmId,
|
||||||
|
version: entry.version
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = olmMap.get(entry.clientId);
|
||||||
|
if (!resolved) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
clientId: entry.clientId,
|
||||||
|
olmId: resolved.olmId,
|
||||||
|
version: entry.version ?? resolved.version
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((entry) => entry !== null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function addPeerDataBatch(
|
||||||
|
entries: {
|
||||||
|
clientId: number;
|
||||||
|
siteId: number;
|
||||||
|
remoteSubnets: string[];
|
||||||
|
aliases: Alias[];
|
||||||
|
olmId?: string;
|
||||||
|
version?: string | null;
|
||||||
|
}[]
|
||||||
|
) {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedTargets = await resolveOlmTargets(entries);
|
||||||
|
|
||||||
|
if (resolvedTargets.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloads = entries
|
||||||
|
.map((entry) => {
|
||||||
|
const resolved = resolvedTargets.find(
|
||||||
|
(target) => target.clientId === entry.clientId
|
||||||
|
);
|
||||||
|
if (!resolved) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
clientId: resolved.olmId,
|
||||||
|
message: {
|
||||||
|
type: `olm/wg/peer/data/add`,
|
||||||
|
data: {
|
||||||
|
siteId: entry.siteId,
|
||||||
|
remoteSubnets: entry.remoteSubnets,
|
||||||
|
aliases: entry.aliases
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
incrementConfigVersion: true,
|
||||||
|
compress: canCompress(resolved.version, "olm")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((entry) => entry !== null);
|
||||||
|
|
||||||
|
if (payloads.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendToClientsBatch(payloads);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removePeerDataBatch(
|
||||||
|
entries: {
|
||||||
|
clientId: number;
|
||||||
|
siteId: number;
|
||||||
|
remoteSubnets: string[];
|
||||||
|
aliases: Alias[];
|
||||||
|
olmId?: string;
|
||||||
|
version?: string | null;
|
||||||
|
}[]
|
||||||
|
) {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedTargets = await resolveOlmTargets(entries);
|
||||||
|
|
||||||
|
if (resolvedTargets.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloads = entries
|
||||||
|
.map((entry) => {
|
||||||
|
const resolved = resolvedTargets.find(
|
||||||
|
(target) => target.clientId === entry.clientId
|
||||||
|
);
|
||||||
|
if (!resolved) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
clientId: resolved.olmId,
|
||||||
|
message: {
|
||||||
|
type: `olm/wg/peer/data/remove`,
|
||||||
|
data: {
|
||||||
|
siteId: entry.siteId,
|
||||||
|
remoteSubnets: entry.remoteSubnets,
|
||||||
|
aliases: entry.aliases
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
incrementConfigVersion: true,
|
||||||
|
compress: canCompress(resolved.version, "olm")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((entry) => entry !== null);
|
||||||
|
|
||||||
|
if (payloads.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendToClientsBatch(payloads);
|
||||||
|
}
|
||||||
|
|
||||||
export async function updatePeerData(
|
export async function updatePeerData(
|
||||||
clientId: number,
|
clientId: number,
|
||||||
siteId: number,
|
siteId: number,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { db, Site } from "@server/db";
|
import { db, Site } from "@server/db";
|
||||||
import { newts, sites } from "@server/db";
|
import { newts, sites } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { sendToClient } from "#dynamic/routers/ws";
|
import { sendToClient, sendToClientsBatch } from "#dynamic/routers/ws";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
|
||||||
export async function addPeer(
|
export async function addPeer(
|
||||||
@@ -36,10 +36,14 @@ export async function addPeer(
|
|||||||
newtId = newt.newtId;
|
newtId = newt.newtId;
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendToClient(newtId, {
|
await sendToClient(
|
||||||
|
newtId,
|
||||||
|
{
|
||||||
type: "newt/wg/peer/add",
|
type: "newt/wg/peer/add",
|
||||||
data: peer
|
data: peer
|
||||||
}, { incrementConfigVersion: true }).catch((error) => {
|
},
|
||||||
|
{ incrementConfigVersion: true }
|
||||||
|
).catch((error) => {
|
||||||
logger.warn(`Error sending message:`, error);
|
logger.warn(`Error sending message:`, error);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,12 +80,16 @@ export async function deletePeer(
|
|||||||
newtId = newt.newtId;
|
newtId = newt.newtId;
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendToClient(newtId, {
|
await sendToClient(
|
||||||
|
newtId,
|
||||||
|
{
|
||||||
type: "newt/wg/peer/remove",
|
type: "newt/wg/peer/remove",
|
||||||
data: {
|
data: {
|
||||||
publicKey
|
publicKey
|
||||||
}
|
}
|
||||||
}, { incrementConfigVersion: true }).catch((error) => {
|
},
|
||||||
|
{ incrementConfigVersion: true }
|
||||||
|
).catch((error) => {
|
||||||
logger.warn(`Error sending message:`, error);
|
logger.warn(`Error sending message:`, error);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,6 +98,35 @@ export async function deletePeer(
|
|||||||
return site;
|
return site;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deletePeersBatch(
|
||||||
|
peers: {
|
||||||
|
siteId: number;
|
||||||
|
publicKey: string;
|
||||||
|
newtId: string;
|
||||||
|
}[]
|
||||||
|
) {
|
||||||
|
if (peers.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendToClientsBatch(
|
||||||
|
peers.map((peer) => ({
|
||||||
|
clientId: peer.newtId,
|
||||||
|
message: {
|
||||||
|
type: "newt/wg/peer/remove",
|
||||||
|
data: {
|
||||||
|
publicKey: peer.publicKey
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: { incrementConfigVersion: true }
|
||||||
|
}))
|
||||||
|
).catch((error) => {
|
||||||
|
logger.warn(`Error sending batched newt peer removals:`, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Deleted ${peers.length} peer(s) from newts (batch)`);
|
||||||
|
}
|
||||||
|
|
||||||
export async function updatePeer(
|
export async function updatePeer(
|
||||||
siteId: number,
|
siteId: number,
|
||||||
publicKey: string,
|
publicKey: string,
|
||||||
@@ -122,13 +159,17 @@ export async function updatePeer(
|
|||||||
newtId = newt.newtId;
|
newtId = newt.newtId;
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendToClient(newtId, {
|
await sendToClient(
|
||||||
|
newtId,
|
||||||
|
{
|
||||||
type: "newt/wg/peer/update",
|
type: "newt/wg/peer/update",
|
||||||
data: {
|
data: {
|
||||||
publicKey,
|
publicKey,
|
||||||
...peer
|
...peer
|
||||||
}
|
}
|
||||||
}, { incrementConfigVersion: true }).catch((error) => {
|
},
|
||||||
|
{ incrementConfigVersion: true }
|
||||||
|
).catch((error) => {
|
||||||
logger.warn(`Error sending message:`, error);
|
logger.warn(`Error sending message:`, error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { sendToClient } from "#dynamic/routers/ws";
|
import { sendToClient, sendToClientsBatch } from "#dynamic/routers/ws";
|
||||||
import { clientSitesAssociationsCache, db, olms } from "@server/db";
|
import { clientSitesAssociationsCache, db, olms } from "@server/db";
|
||||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
import { Alias } from "yaml";
|
import { Alias } from "yaml";
|
||||||
|
|
||||||
export async function addPeer(
|
export async function addPeer(
|
||||||
@@ -205,3 +205,150 @@ export async function initPeerAddHandshake(
|
|||||||
`Initiated peer add handshake for site ${peer.siteId} to olm ${olmId}`
|
`Initiated peer add handshake for site ${peer.siteId} to olm ${olmId}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deletePeersBatch(
|
||||||
|
peers: {
|
||||||
|
clientId: number;
|
||||||
|
siteId: number;
|
||||||
|
publicKey: string;
|
||||||
|
olmId?: string;
|
||||||
|
version?: string | null;
|
||||||
|
}[]
|
||||||
|
) {
|
||||||
|
if (peers.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unresolvedClientIds = peers
|
||||||
|
.filter((peer) => !peer.olmId)
|
||||||
|
.map((peer) => peer.clientId);
|
||||||
|
|
||||||
|
const olmByClientId = new Map<
|
||||||
|
number,
|
||||||
|
{ olmId: string; version: string | null }
|
||||||
|
>();
|
||||||
|
|
||||||
|
if (unresolvedClientIds.length > 0) {
|
||||||
|
const olmRows = await db
|
||||||
|
.select({
|
||||||
|
clientId: olms.clientId,
|
||||||
|
olmId: olms.olmId,
|
||||||
|
version: olms.version
|
||||||
|
})
|
||||||
|
.from(olms)
|
||||||
|
.where(inArray(olms.clientId, unresolvedClientIds));
|
||||||
|
|
||||||
|
for (const row of olmRows) {
|
||||||
|
if (row.clientId !== null) {
|
||||||
|
olmByClientId.set(row.clientId, {
|
||||||
|
olmId: row.olmId,
|
||||||
|
version: row.version
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchPayloads = peers
|
||||||
|
.map((peer) => {
|
||||||
|
const resolved = peer.olmId
|
||||||
|
? { olmId: peer.olmId, version: peer.version ?? null }
|
||||||
|
: olmByClientId.get(peer.clientId);
|
||||||
|
if (!resolved) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
clientId: resolved.olmId,
|
||||||
|
message: {
|
||||||
|
type: "olm/wg/peer/remove",
|
||||||
|
data: {
|
||||||
|
publicKey: peer.publicKey,
|
||||||
|
siteId: peer.siteId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
incrementConfigVersion: true,
|
||||||
|
compress: canCompress(
|
||||||
|
peer.version ?? resolved.version,
|
||||||
|
"olm"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((payload) => payload !== null);
|
||||||
|
|
||||||
|
if (batchPayloads.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendToClientsBatch(batchPayloads).catch((error) => {
|
||||||
|
logger.warn(`Error sending batched olm peer removals:`, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Deleted ${batchPayloads.length} peer(s) from olms (batch)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initPeerAddHandshakeBatch(
|
||||||
|
handshakes: {
|
||||||
|
clientId: number;
|
||||||
|
peer: {
|
||||||
|
siteId: number;
|
||||||
|
exitNode: {
|
||||||
|
publicKey: string;
|
||||||
|
endpoint: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
olmId: string;
|
||||||
|
chainId?: string;
|
||||||
|
}[]
|
||||||
|
) {
|
||||||
|
if (handshakes.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendToClientsBatch(
|
||||||
|
handshakes.map((item) => ({
|
||||||
|
clientId: item.olmId,
|
||||||
|
message: {
|
||||||
|
type: "olm/wg/peer/holepunch/site/add",
|
||||||
|
data: {
|
||||||
|
siteId: item.peer.siteId,
|
||||||
|
exitNode: {
|
||||||
|
publicKey: item.peer.exitNode.publicKey,
|
||||||
|
relayPort:
|
||||||
|
config.getRawConfig().gerbil.clients_start_port,
|
||||||
|
endpoint: item.peer.exitNode.endpoint
|
||||||
|
},
|
||||||
|
chainId: item.chainId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: { incrementConfigVersion: true }
|
||||||
|
}))
|
||||||
|
).catch((error) => {
|
||||||
|
logger.warn(`Error sending batched olm handshakes:`, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
handshakes.map((item) =>
|
||||||
|
db
|
||||||
|
.update(clientSitesAssociationsCache)
|
||||||
|
.set({ isJitMode: false })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
clientSitesAssociationsCache.clientId,
|
||||||
|
item.clientId
|
||||||
|
),
|
||||||
|
eq(
|
||||||
|
clientSitesAssociationsCache.siteId,
|
||||||
|
item.peer.siteId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Initiated ${handshakes.length} peer add handshake(s) to olms (batch)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,12 +76,32 @@ export interface SendMessageOptions {
|
|||||||
compress?: boolean;
|
compress?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redis message type for cross-node communication
|
export interface BatchSendMessage {
|
||||||
export interface RedisMessage {
|
clientId: string;
|
||||||
type: "direct" | "broadcast";
|
message: WSMessage;
|
||||||
targetClientId?: string;
|
options?: SendMessageOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redis message types for cross-node communication
|
||||||
|
export type RedisMessage =
|
||||||
|
| {
|
||||||
|
type: "direct";
|
||||||
|
targetClientId: string;
|
||||||
|
message: WSMessage;
|
||||||
|
fromNodeId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "direct-batch";
|
||||||
|
messages: {
|
||||||
|
targetClientId: string;
|
||||||
|
message: WSMessage;
|
||||||
|
}[];
|
||||||
|
fromNodeId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "broadcast";
|
||||||
excludeClientId?: string;
|
excludeClientId?: string;
|
||||||
message: WSMessage;
|
message: WSMessage;
|
||||||
fromNodeId: string;
|
fromNodeId: string;
|
||||||
options?: SendMessageOptions;
|
options?: SendMessageOptions;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ import {
|
|||||||
WebSocketRequest,
|
WebSocketRequest,
|
||||||
WSMessage,
|
WSMessage,
|
||||||
AuthenticatedWebSocket,
|
AuthenticatedWebSocket,
|
||||||
SendMessageOptions
|
SendMessageOptions,
|
||||||
|
BatchSendMessage
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { validateSessionToken } from "@server/auth/sessions/app";
|
import { validateSessionToken } from "@server/auth/sessions/app";
|
||||||
|
|
||||||
@@ -212,6 +213,20 @@ const sendToClient = async (
|
|||||||
return localSent;
|
return localSent;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sendToClientsBatch = async (
|
||||||
|
entries: BatchSendMessage[]
|
||||||
|
): Promise<void> => {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
entries.map((entry) =>
|
||||||
|
sendToClient(entry.clientId, entry.message, entry.options)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const broadcastToAllExcept = async (
|
const broadcastToAllExcept = async (
|
||||||
message: WSMessage,
|
message: WSMessage,
|
||||||
excludeClientId?: string,
|
excludeClientId?: string,
|
||||||
@@ -552,6 +567,7 @@ export {
|
|||||||
router,
|
router,
|
||||||
handleWSUpgrade,
|
handleWSUpgrade,
|
||||||
sendToClient,
|
sendToClient,
|
||||||
|
sendToClientsBatch,
|
||||||
broadcastToAllExcept,
|
broadcastToAllExcept,
|
||||||
connectedClients,
|
connectedClients,
|
||||||
hasActiveConnections,
|
hasActiveConnections,
|
||||||
|
|||||||
Reference in New Issue
Block a user