diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index aff7cc70e..c27303e99 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -1488,6 +1488,7 @@ export const olms = pgTable( dateCreated: varchar("dateCreated").notNull(), version: text("version"), agent: text("agent"), + agentVersion: text("agentVersion"), name: varchar("name"), clientId: integer("clientId").references(() => clients.clientId, { // we will switch this depending on the current org it wants to connect to diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 2cd3494e5..07e14a25d 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -843,6 +843,7 @@ export const olms = sqliteTable( dateCreated: text("dateCreated").notNull(), version: text("version"), agent: text("agent"), + agentVersion: text("agentVersion"), name: text("name"), clientId: integer("clientId").references(() => clients.clientId, { // we will switch this depending on the current org it wants to connect to diff --git a/server/lib/clientVersionChecks.ts b/server/lib/clientVersionChecks.ts index 2fd70e736..0f9168e47 100644 --- a/server/lib/clientVersionChecks.ts +++ b/server/lib/clientVersionChecks.ts @@ -1,16 +1,20 @@ import semver from "semver"; +// NEWT AND OLM +// Pass the newt/olm library version (newts.version / olms.version), not +// agentVersion: the agent may be an app embedding the library (the CLI, +// Pangolin Windows, ...) whose version is unrelated to the protocol support. export function canCompress( clientVersion: string | null | undefined, - type: "newt" | "olm" + agent: "newt" | "olm" ): boolean { try { if (!clientVersion) return false; // check if it is a valid semver if (!semver.valid(clientVersion)) return false; - if (type === "newt") { + if (agent === "newt") { return semver.gte(clientVersion, "1.10.3"); - } else if (type === "olm") { + } else if (agent === "olm") { return semver.gte(clientVersion, "1.4.3"); } return false; @@ -23,6 +27,7 @@ export function canCompress( // sync message's `certs` array, instead of requiring each target to carry // its own inline `tlsCert`/`tlsKey` PEM data. Bump the version floor here to // match whatever release first ships the newt-side support. +// NEWT export function supportsCertReferences( clientVersion: string | null | undefined ): boolean { @@ -38,6 +43,7 @@ export function supportsCertReferences( // Whether this newt client understands newt/wg/client-identities/add, used to // set Remote-* headers on requests through its HTTP proxy. Bump the version // floor here to match whatever release first ships the newt-side support. +// NEWT export function supportsClientIdentities( clientVersion: string | null | undefined ): boolean { @@ -49,3 +55,33 @@ export function supportsClientIdentities( return false; } } + +// Whether this newt client understands the v2 subnet proxy target format. +// Older clients need targets converted back to v1 before they are sent. +// NEWT +export function supportsTargetsV2( + clientVersion: string | null | undefined +): boolean { + try { + if (!clientVersion) return false; + if (!semver.valid(clientVersion)) return false; + return semver.gte(clientVersion, "1.10.3"); + } catch { + return false; + } +} + +// Whether this newt client understands the sync message sent when its config +// version falls behind the server's. +// NEWT +export function supportsSync( + clientVersion: string | null | undefined +): boolean { + try { + if (!clientVersion) return false; + if (!semver.valid(clientVersion)) return false; + return semver.gte(clientVersion, "1.14.0"); + } catch { + return false; + } +} diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index 491a014b7..d803b0126 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -228,6 +228,7 @@ export type GetClientResponse = NonNullable< olmId: string | null; agent: string | null; olmVersion: string | null; + agentVersion: string | null; userEmail: string | null; userName: string | null; userUsername: string | null; @@ -421,6 +422,7 @@ export async function getClient( olmId: client.olms ? client.olms.olmId : null, agent: client.olms?.agent || null, olmVersion: client.olms?.version || null, + agentVersion: client.olms?.agentVersion || null, userEmail: client.user?.email ?? null, userName: client.user?.name ?? null, userUsername: client.user?.username ?? null, diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts index 01448a80f..33ae377df 100644 --- a/server/routers/client/listClients.ts +++ b/server/routers/client/listClients.ts @@ -187,6 +187,7 @@ function queryClientsBase() { userEmail: users.email, niceId: clients.niceId, agent: olms.agent, + agentVersion: olms.agentVersion, approvalState: clients.approvalState, olmArchived: olms.archived, archived: clients.archived, diff --git a/server/routers/client/listUserDevices.ts b/server/routers/client/listUserDevices.ts index cc006565f..41d89a1bb 100644 --- a/server/routers/client/listUserDevices.ts +++ b/server/routers/client/listUserDevices.ts @@ -172,6 +172,7 @@ function queryUserDevicesBase() { idpVariant: idpOidcConfig.variant, niceId: clients.niceId, agent: olms.agent, + agentVersion: olms.agentVersion, approvalState: clients.approvalState, olmArchived: olms.archived, archived: clients.archived, diff --git a/server/routers/client/targets.ts b/server/routers/client/targets.ts index 543293a10..74b43c34f 100644 --- a/server/routers/client/targets.ts +++ b/server/routers/client/targets.ts @@ -6,12 +6,12 @@ import { SubnetProxyTarget, SubnetProxyTargetV2 } from "@server/lib/ip"; -import { canCompress } from "@server/lib/clientVersionChecks"; +import { + canCompress, + supportsTargetsV2 +} from "@server/lib/clientVersionChecks"; import logger from "@server/logger"; import { eq, inArray } from "drizzle-orm"; -import semver from "semver"; - -const NEWT_V2_TARGETS_VERSION = ">=1.10.3"; export async function convertTargetsIfNecessary( newtId: string, @@ -27,10 +27,7 @@ export async function convertTargetsIfNecessary( } // check the semver - if ( - newt.version && - !semver.satisfies(newt.version, NEWT_V2_TARGETS_VERSION) - ) { + if (newt.version && !supportsTargetsV2(newt.version)) { logger.debug( `addTargets Newt version ${newt.version} does not support targets v2 falling back` ); @@ -167,10 +164,7 @@ export async function updateTargets( } // check the semver - if ( - newt.version && - !semver.satisfies(newt.version, NEWT_V2_TARGETS_VERSION) - ) { + if (newt.version && !supportsTargetsV2(newt.version)) { logger.debug( `addTargets Newt version ${newt.version} does not support targets v2 falling back` ); diff --git a/server/routers/newt/handleNewtPingMessage.ts b/server/routers/newt/handleNewtPingMessage.ts index 13052dcc1..b0536669d 100644 --- a/server/routers/newt/handleNewtPingMessage.ts +++ b/server/routers/newt/handleNewtPingMessage.ts @@ -5,10 +5,9 @@ import { Newt } from "@server/db"; import { eq } from "drizzle-orm"; import logger from "@server/logger"; import { sendNewtSyncMessage } from "./sync"; -import semver from "semver"; import { recordSitePing } from "./pingAccumulator"; +import { supportsSync } from "@server/lib/clientVersionChecks"; -const NEWT_SUPPORTS_SYNC_VERSION = ">=1.14.0"; const PONG = { message: { type: "pong", @@ -50,10 +49,7 @@ export const handleNewtPingMessage: MessageHandler = async (context) => { // cross-region latency to the database. recordSitePing(newt.siteId); - if ( - newt.version && - !semver.satisfies(newt.version, NEWT_SUPPORTS_SYNC_VERSION) - ) { + if (newt.version && !supportsSync(newt.version)) { // Newt does not support the sync message so not checking - stop here - return PONG; } diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 13bf44070..7af781726 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -55,6 +55,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { relay, olmVersion, olmAgent, + agentVersion, orgId, userToken, pingResults, @@ -92,9 +93,17 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { await handleFingerprintInsertion(olm, fingerprint, postures); } + // Olm clients that predate agentVersion report the version of the app + // embedding olm (Pangolin Windows, the CLI, ...) in olmVersion, so it + // doubles as the agent version. Newer clients report the olm library + // version in olmVersion and the app version in agentVersion. + const reportedAgentVersion = + agentVersion === undefined ? olmVersion : agentVersion; + if ( (olmVersion && olm.version !== olmVersion) || (olmAgent && olm.agent !== olmAgent) || + (reportedAgentVersion && olm.agentVersion !== reportedAgentVersion) || olm.archived ) { await db @@ -102,6 +111,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { .set({ version: olmVersion, agent: olmAgent, + agentVersion: reportedAgentVersion, archived: false }) .where(eq(olms.olmId, olm.olmId)); diff --git a/src/app/[orgId]/settings/clients/machine/page.tsx b/src/app/[orgId]/settings/clients/machine/page.tsx index 4e8f57da5..ac023e77e 100644 --- a/src/app/[orgId]/settings/clients/machine/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/page.tsx @@ -89,6 +89,7 @@ export default async function ClientsPage(props: ClientsPageProps) { orgId: params.orgId, online: client.online, olmVersion: client.olmVersion || undefined, + agentVersion: client.agentVersion || undefined, olmUpdateAvailable: client.olmUpdateAvailable || false, userId: client.userId, username: client.username, diff --git a/src/app/[orgId]/settings/clients/user/page.tsx b/src/app/[orgId]/settings/clients/user/page.tsx index 5cc904fee..ad99728a6 100644 --- a/src/app/[orgId]/settings/clients/user/page.tsx +++ b/src/app/[orgId]/settings/clients/user/page.tsx @@ -92,6 +92,7 @@ export default async function ClientsPage(props: ClientsPageProps) { orgId: params.orgId, online: client.online, olmVersion: client.olmVersion || undefined, + agentVersion: client.agentVersion || undefined, olmUpdateAvailable: Boolean(client.olmUpdateAvailable), userId: client.userId, username: client.username, diff --git a/src/components/ClientInfoCard.tsx b/src/components/ClientInfoCard.tsx index cfd004e62..4a3ea50ac 100644 --- a/src/components/ClientInfoCard.tsx +++ b/src/components/ClientInfoCard.tsx @@ -44,26 +44,29 @@ export default function SiteInfoCard({}: ClientInfoCardProps) { const data = useQuery(productUpdatesQueries.latestVersion(true)); const latestPlatformVersions = data.data?.data; + // agentVersion was added after olmVersion, and older olm clients report + // the app version in olmVersion, so fall back to it for clients that have + // not re-registered yet. + const agentVersion = client.agentVersion ?? client.olmVersion; + let updateAvailable = false; - if (client.agent && client.olmVersion && latestPlatformVersions) { + if (client.agent && agentVersion && latestPlatformVersions) { const agent = agentVersionMap[ client.agent ] as keyof LatestVersionResponse; if (agent in latestPlatformVersions) { - const agentVersion = latestPlatformVersions[agent]; + const latestAgentVersion = latestPlatformVersions[agent]; updateAvailable = Boolean( - semver.valid(client.olmVersion) && - semver.lt(client.olmVersion, agentVersion.latestVersion) + semver.valid(agentVersion) && + semver.lt(agentVersion, latestAgentVersion.latestVersion) ); } } const cols = - 2 + - (userDisplayName ? 1 : 0) + - (client.agent && client.olmVersion ? 1 : 0); + 2 + (userDisplayName ? 1 : 0) + (client.agent && agentVersion ? 1 : 0); return ( @@ -73,15 +76,13 @@ export default function SiteInfoCard({}: ClientInfoCardProps) { {t("name")} {client.name} - {client.agent && client.olmVersion ? ( + {client.agent && agentVersion ? ( {t("agent")}
- {client.agent + - " v" + - client.olmVersion} + {client.agent + " v" + agentVersion} {updateAvailable && ( - {originalRow.agent && originalRow.olmVersion ? ( + {originalRow.agent && displayedVersion ? ( {originalRow.agent + " v" + - originalRow.olmVersion} + displayedVersion} ) : ( "-" diff --git a/src/components/UserDevicesTable.tsx b/src/components/UserDevicesTable.tsx index 957df9218..713fd7f98 100644 --- a/src/components/UserDevicesTable.tsx +++ b/src/components/UserDevicesTable.tsx @@ -55,6 +55,7 @@ export type ClientRow = { orgId: string; online: boolean; olmVersion?: string; + agentVersion?: string; olmUpdateAvailable: boolean; userId: string | null; username: string | null; @@ -578,10 +579,16 @@ export default function UserDevicesTable({ "Olm CLI": "olm" }; + // agentVersion was added after olmVersion, and older olm + // clients report the app version in olmVersion, so fall + // back to it for rows that have not re-registered yet. + const displayedVersion = + originalRow.agentVersion ?? originalRow.olmVersion; + let updateAvailable = false; if ( - originalRow.olmVersion && + displayedVersion && originalRow.agent && latestPlatformVersions ) { @@ -590,13 +597,14 @@ export default function UserDevicesTable({ ] as keyof LatestVersionResponse; if (agent in latestPlatformVersions) { - const agentVersion = latestPlatformVersions[agent]; + const latestAgentVersion = + latestPlatformVersions[agent]; updateAvailable = Boolean( - semver.valid(originalRow.olmVersion) && + semver.valid(displayedVersion) && semver.lt( - originalRow.olmVersion, - agentVersion.latestVersion + displayedVersion, + latestAgentVersion.latestVersion ) ); } @@ -604,11 +612,11 @@ export default function UserDevicesTable({ return (
- {originalRow.agent && originalRow.olmVersion ? ( + {originalRow.agent && displayedVersion ? ( {originalRow.agent + " v" + - originalRow.olmVersion} + displayedVersion} ) : ( "-"