Add olm agentVersion to separate the library from the agent

This commit is contained in:
Owen
2026-10-22 14:59:33 -04:00
parent 8b509da19f
commit 290f1f2ff0
14 changed files with 107 additions and 46 deletions
+1
View File
@@ -1488,6 +1488,7 @@ export const olms = pgTable(
dateCreated: varchar("dateCreated").notNull(), dateCreated: varchar("dateCreated").notNull(),
version: text("version"), version: text("version"),
agent: text("agent"), agent: text("agent"),
agentVersion: text("agentVersion"),
name: varchar("name"), name: varchar("name"),
clientId: integer("clientId").references(() => clients.clientId, { clientId: integer("clientId").references(() => clients.clientId, {
// we will switch this depending on the current org it wants to connect to // we will switch this depending on the current org it wants to connect to
+1
View File
@@ -843,6 +843,7 @@ export const olms = sqliteTable(
dateCreated: text("dateCreated").notNull(), dateCreated: text("dateCreated").notNull(),
version: text("version"), version: text("version"),
agent: text("agent"), agent: text("agent"),
agentVersion: text("agentVersion"),
name: text("name"), name: text("name"),
clientId: integer("clientId").references(() => clients.clientId, { clientId: integer("clientId").references(() => clients.clientId, {
// we will switch this depending on the current org it wants to connect to // we will switch this depending on the current org it wants to connect to
+39 -3
View File
@@ -1,16 +1,20 @@
import semver from "semver"; 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( export function canCompress(
clientVersion: string | null | undefined, clientVersion: string | null | undefined,
type: "newt" | "olm" agent: "newt" | "olm"
): boolean { ): boolean {
try { try {
if (!clientVersion) return false; if (!clientVersion) return false;
// check if it is a valid semver // check if it is a valid semver
if (!semver.valid(clientVersion)) return false; if (!semver.valid(clientVersion)) return false;
if (type === "newt") { if (agent === "newt") {
return semver.gte(clientVersion, "1.10.3"); return semver.gte(clientVersion, "1.10.3");
} else if (type === "olm") { } else if (agent === "olm") {
return semver.gte(clientVersion, "1.4.3"); return semver.gte(clientVersion, "1.4.3");
} }
return false; return false;
@@ -23,6 +27,7 @@ export function canCompress(
// sync message's `certs` array, instead of requiring each target to carry // 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 // its own inline `tlsCert`/`tlsKey` PEM data. Bump the version floor here to
// match whatever release first ships the newt-side support. // match whatever release first ships the newt-side support.
// NEWT
export function supportsCertReferences( export function supportsCertReferences(
clientVersion: string | null | undefined clientVersion: string | null | undefined
): boolean { ): boolean {
@@ -38,6 +43,7 @@ export function supportsCertReferences(
// Whether this newt client understands newt/wg/client-identities/add, used to // Whether this newt client understands newt/wg/client-identities/add, used to
// set Remote-* headers on requests through its HTTP proxy. Bump the version // set Remote-* headers on requests through its HTTP proxy. Bump the version
// floor here to match whatever release first ships the newt-side support. // floor here to match whatever release first ships the newt-side support.
// NEWT
export function supportsClientIdentities( export function supportsClientIdentities(
clientVersion: string | null | undefined clientVersion: string | null | undefined
): boolean { ): boolean {
@@ -49,3 +55,33 @@ export function supportsClientIdentities(
return false; 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;
}
}
+2
View File
@@ -228,6 +228,7 @@ export type GetClientResponse = NonNullable<
olmId: string | null; olmId: string | null;
agent: string | null; agent: string | null;
olmVersion: string | null; olmVersion: string | null;
agentVersion: string | null;
userEmail: string | null; userEmail: string | null;
userName: string | null; userName: string | null;
userUsername: string | null; userUsername: string | null;
@@ -421,6 +422,7 @@ export async function getClient(
olmId: client.olms ? client.olms.olmId : null, olmId: client.olms ? client.olms.olmId : null,
agent: client.olms?.agent || null, agent: client.olms?.agent || null,
olmVersion: client.olms?.version || null, olmVersion: client.olms?.version || null,
agentVersion: client.olms?.agentVersion || null,
userEmail: client.user?.email ?? null, userEmail: client.user?.email ?? null,
userName: client.user?.name ?? null, userName: client.user?.name ?? null,
userUsername: client.user?.username ?? null, userUsername: client.user?.username ?? null,
+1
View File
@@ -187,6 +187,7 @@ function queryClientsBase() {
userEmail: users.email, userEmail: users.email,
niceId: clients.niceId, niceId: clients.niceId,
agent: olms.agent, agent: olms.agent,
agentVersion: olms.agentVersion,
approvalState: clients.approvalState, approvalState: clients.approvalState,
olmArchived: olms.archived, olmArchived: olms.archived,
archived: clients.archived, archived: clients.archived,
+1
View File
@@ -172,6 +172,7 @@ function queryUserDevicesBase() {
idpVariant: idpOidcConfig.variant, idpVariant: idpOidcConfig.variant,
niceId: clients.niceId, niceId: clients.niceId,
agent: olms.agent, agent: olms.agent,
agentVersion: olms.agentVersion,
approvalState: clients.approvalState, approvalState: clients.approvalState,
olmArchived: olms.archived, olmArchived: olms.archived,
archived: clients.archived, archived: clients.archived,
+6 -12
View File
@@ -6,12 +6,12 @@ import {
SubnetProxyTarget, SubnetProxyTarget,
SubnetProxyTargetV2 SubnetProxyTargetV2
} from "@server/lib/ip"; } from "@server/lib/ip";
import { canCompress } from "@server/lib/clientVersionChecks"; import {
canCompress,
supportsTargetsV2
} from "@server/lib/clientVersionChecks";
import logger from "@server/logger"; import logger from "@server/logger";
import { eq, inArray } from "drizzle-orm"; import { eq, inArray } from "drizzle-orm";
import semver from "semver";
const NEWT_V2_TARGETS_VERSION = ">=1.10.3";
export async function convertTargetsIfNecessary( export async function convertTargetsIfNecessary(
newtId: string, newtId: string,
@@ -27,10 +27,7 @@ export async function convertTargetsIfNecessary(
} }
// check the semver // check the semver
if ( if (newt.version && !supportsTargetsV2(newt.version)) {
newt.version &&
!semver.satisfies(newt.version, NEWT_V2_TARGETS_VERSION)
) {
logger.debug( logger.debug(
`addTargets Newt version ${newt.version} does not support targets v2 falling back` `addTargets Newt version ${newt.version} does not support targets v2 falling back`
); );
@@ -167,10 +164,7 @@ export async function updateTargets(
} }
// check the semver // check the semver
if ( if (newt.version && !supportsTargetsV2(newt.version)) {
newt.version &&
!semver.satisfies(newt.version, NEWT_V2_TARGETS_VERSION)
) {
logger.debug( logger.debug(
`addTargets Newt version ${newt.version} does not support targets v2 falling back` `addTargets Newt version ${newt.version} does not support targets v2 falling back`
); );
+2 -6
View File
@@ -5,10 +5,9 @@ import { Newt } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import { sendNewtSyncMessage } from "./sync"; import { sendNewtSyncMessage } from "./sync";
import semver from "semver";
import { recordSitePing } from "./pingAccumulator"; import { recordSitePing } from "./pingAccumulator";
import { supportsSync } from "@server/lib/clientVersionChecks";
const NEWT_SUPPORTS_SYNC_VERSION = ">=1.14.0";
const PONG = { const PONG = {
message: { message: {
type: "pong", type: "pong",
@@ -50,10 +49,7 @@ export const handleNewtPingMessage: MessageHandler = async (context) => {
// cross-region latency to the database. // cross-region latency to the database.
recordSitePing(newt.siteId); recordSitePing(newt.siteId);
if ( if (newt.version && !supportsSync(newt.version)) {
newt.version &&
!semver.satisfies(newt.version, NEWT_SUPPORTS_SYNC_VERSION)
) {
// Newt does not support the sync message so not checking - stop here - // Newt does not support the sync message so not checking - stop here -
return PONG; return PONG;
} }
@@ -55,6 +55,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
relay, relay,
olmVersion, olmVersion,
olmAgent, olmAgent,
agentVersion,
orgId, orgId,
userToken, userToken,
pingResults, pingResults,
@@ -92,9 +93,17 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
await handleFingerprintInsertion(olm, fingerprint, postures); 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 ( if (
(olmVersion && olm.version !== olmVersion) || (olmVersion && olm.version !== olmVersion) ||
(olmAgent && olm.agent !== olmAgent) || (olmAgent && olm.agent !== olmAgent) ||
(reportedAgentVersion && olm.agentVersion !== reportedAgentVersion) ||
olm.archived olm.archived
) { ) {
await db await db
@@ -102,6 +111,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
.set({ .set({
version: olmVersion, version: olmVersion,
agent: olmAgent, agent: olmAgent,
agentVersion: reportedAgentVersion,
archived: false archived: false
}) })
.where(eq(olms.olmId, olm.olmId)); .where(eq(olms.olmId, olm.olmId));
@@ -89,6 +89,7 @@ export default async function ClientsPage(props: ClientsPageProps) {
orgId: params.orgId, orgId: params.orgId,
online: client.online, online: client.online,
olmVersion: client.olmVersion || undefined, olmVersion: client.olmVersion || undefined,
agentVersion: client.agentVersion || undefined,
olmUpdateAvailable: client.olmUpdateAvailable || false, olmUpdateAvailable: client.olmUpdateAvailable || false,
userId: client.userId, userId: client.userId,
username: client.username, username: client.username,
@@ -92,6 +92,7 @@ export default async function ClientsPage(props: ClientsPageProps) {
orgId: params.orgId, orgId: params.orgId,
online: client.online, online: client.online,
olmVersion: client.olmVersion || undefined, olmVersion: client.olmVersion || undefined,
agentVersion: client.agentVersion || undefined,
olmUpdateAvailable: Boolean(client.olmUpdateAvailable), olmUpdateAvailable: Boolean(client.olmUpdateAvailable),
userId: client.userId, userId: client.userId,
username: client.username, username: client.username,
+12 -11
View File
@@ -44,26 +44,29 @@ export default function SiteInfoCard({}: ClientInfoCardProps) {
const data = useQuery(productUpdatesQueries.latestVersion(true)); const data = useQuery(productUpdatesQueries.latestVersion(true));
const latestPlatformVersions = data.data?.data; 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; let updateAvailable = false;
if (client.agent && client.olmVersion && latestPlatformVersions) { if (client.agent && agentVersion && latestPlatformVersions) {
const agent = agentVersionMap[ const agent = agentVersionMap[
client.agent client.agent
] as keyof LatestVersionResponse; ] as keyof LatestVersionResponse;
if (agent in latestPlatformVersions) { if (agent in latestPlatformVersions) {
const agentVersion = latestPlatformVersions[agent]; const latestAgentVersion = latestPlatformVersions[agent];
updateAvailable = Boolean( updateAvailable = Boolean(
semver.valid(client.olmVersion) && semver.valid(agentVersion) &&
semver.lt(client.olmVersion, agentVersion.latestVersion) semver.lt(agentVersion, latestAgentVersion.latestVersion)
); );
} }
} }
const cols = const cols =
2 + 2 + (userDisplayName ? 1 : 0) + (client.agent && agentVersion ? 1 : 0);
(userDisplayName ? 1 : 0) +
(client.agent && client.olmVersion ? 1 : 0);
return ( return (
<Alert> <Alert>
@@ -73,15 +76,13 @@ export default function SiteInfoCard({}: ClientInfoCardProps) {
<InfoSectionTitle>{t("name")}</InfoSectionTitle> <InfoSectionTitle>{t("name")}</InfoSectionTitle>
<InfoSectionContent>{client.name}</InfoSectionContent> <InfoSectionContent>{client.name}</InfoSectionContent>
</InfoSection> </InfoSection>
{client.agent && client.olmVersion ? ( {client.agent && agentVersion ? (
<InfoSection> <InfoSection>
<InfoSectionTitle>{t("agent")}</InfoSectionTitle> <InfoSectionTitle>{t("agent")}</InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span> <span>
{client.agent + {client.agent + " v" + agentVersion}
" v" +
client.olmVersion}
</span> </span>
{updateAvailable && ( {updateAvailable && (
<InfoPopup <InfoPopup
+15 -7
View File
@@ -56,6 +56,7 @@ export type ClientRow = {
orgId: string; orgId: string;
online: boolean; online: boolean;
olmVersion?: string; olmVersion?: string;
agentVersion?: string;
olmUpdateAvailable: boolean; olmUpdateAvailable: boolean;
userId: string | null; userId: string | null;
username: string | null; username: string | null;
@@ -397,10 +398,16 @@ export default function MachineClientsTable({
"Olm CLI": "olm" "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; let updateAvailable = false;
if ( if (
originalRow.olmVersion && displayedVersion &&
originalRow.agent && originalRow.agent &&
latestPlatformVersions latestPlatformVersions
) { ) {
@@ -409,13 +416,14 @@ export default function MachineClientsTable({
] as keyof LatestVersionResponse; ] as keyof LatestVersionResponse;
if (agent in latestPlatformVersions) { if (agent in latestPlatformVersions) {
const agentVersion = latestPlatformVersions[agent]; const latestAgentVersion =
latestPlatformVersions[agent];
updateAvailable = Boolean( updateAvailable = Boolean(
semver.valid(originalRow.olmVersion) && semver.valid(displayedVersion) &&
semver.lt( semver.lt(
originalRow.olmVersion, displayedVersion,
agentVersion.latestVersion latestAgentVersion.latestVersion
) )
); );
} }
@@ -423,11 +431,11 @@ export default function MachineClientsTable({
return ( return (
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
{originalRow.agent && originalRow.olmVersion ? ( {originalRow.agent && displayedVersion ? (
<Badge variant="secondary"> <Badge variant="secondary">
{originalRow.agent + {originalRow.agent +
" v" + " v" +
originalRow.olmVersion} displayedVersion}
</Badge> </Badge>
) : ( ) : (
"-" "-"
+15 -7
View File
@@ -55,6 +55,7 @@ export type ClientRow = {
orgId: string; orgId: string;
online: boolean; online: boolean;
olmVersion?: string; olmVersion?: string;
agentVersion?: string;
olmUpdateAvailable: boolean; olmUpdateAvailable: boolean;
userId: string | null; userId: string | null;
username: string | null; username: string | null;
@@ -578,10 +579,16 @@ export default function UserDevicesTable({
"Olm CLI": "olm" "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; let updateAvailable = false;
if ( if (
originalRow.olmVersion && displayedVersion &&
originalRow.agent && originalRow.agent &&
latestPlatformVersions latestPlatformVersions
) { ) {
@@ -590,13 +597,14 @@ export default function UserDevicesTable({
] as keyof LatestVersionResponse; ] as keyof LatestVersionResponse;
if (agent in latestPlatformVersions) { if (agent in latestPlatformVersions) {
const agentVersion = latestPlatformVersions[agent]; const latestAgentVersion =
latestPlatformVersions[agent];
updateAvailable = Boolean( updateAvailable = Boolean(
semver.valid(originalRow.olmVersion) && semver.valid(displayedVersion) &&
semver.lt( semver.lt(
originalRow.olmVersion, displayedVersion,
agentVersion.latestVersion latestAgentVersion.latestVersion
) )
); );
} }
@@ -604,11 +612,11 @@ export default function UserDevicesTable({
return ( return (
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
{originalRow.agent && originalRow.olmVersion ? ( {originalRow.agent && displayedVersion ? (
<Badge variant="secondary"> <Badge variant="secondary">
{originalRow.agent + {originalRow.agent +
" v" + " v" +
originalRow.olmVersion} displayedVersion}
</Badge> </Badge>
) : ( ) : (
"-" "-"