From 7b564e7ba94c00d06cdecc8abe9cb50b65c1d7ad Mon Sep 17 00:00:00 2001 From: mikhailm-coder Date: Fri, 31 Jul 2026 15:01:55 +0200 Subject: [PATCH 1/4] feat(client-core): update bundled meshcentral core module with x-machine-id headers Refreshes the ARTIFACTORY-served CoreModule.js snapshot from the meshagent hotfix/machine-id-header branch: in openFrameMode the core now stamps x-machine-id (read from the shared OpenFrame machine_id file) and an Authorization bearer token on tunnel, download, and self-update requests. On agent binaries that predate the openFrameMode JS property the new code is a silent no-op, so the core can roll out ahead of the binaries. Co-Authored-By: Claude Fable 5 --- .../main/resources/meshcentral-core-module | 58 +++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/openframe-client-core/src/main/resources/meshcentral-core-module b/openframe-client-core/src/main/resources/meshcentral-core-module index 18cbcf954..2dcf1f781 100644 --- a/openframe-client-core/src/main/resources/meshcentral-core-module +++ b/openframe-client-core/src/main/resources/meshcentral-core-module @@ -719,6 +719,43 @@ var http = require('http'); var net = require('net'); var fs = require('fs'); var rtc = require('ILibWebRTC'); + +// OpenFrame: Read machine ID from shared location +var openframeMachineId = null; +function getOpenFrameMachineId() { + if (openframeMachineId != null) return openframeMachineId; + try { + var machineIdPath = (process.platform == 'win32') + ? (process.env['ProgramData'] + '\\OpenFrame\\machine_id') + : ((process.platform == 'darwin') + ? '/Library/Application Support/OpenFrame/machine_id' + : '/var/lib/openframe/machine_id'); + openframeMachineId = fs.readFileSync(machineIdPath).toString().trim(); + } catch (ex) { openframeMachineId = null; } + return openframeMachineId; +} + +// OpenFrame: Add x-machine-id and Authorization headers to request options (only in openFrameMode) +function addOpenFrameHeaders(options) { + // Only add headers if running in OpenFrame mode + if (!mesh.openFrameMode) return options; + + if (!options.headers) options.headers = {}; + + // Add x-machine-id header + var machineId = getOpenFrameMachineId(); + if (machineId) { + options.headers['x-machine-id'] = machineId; + } + + // Add Authorization header with JWT token + var token = mesh.authToken(); + if (token) { + options.headers['Authorization'] = 'Bearer ' + token; + } + + return options; +} var amt = null; var processManager = require('process-manager'); var wifiScannerLib = null; @@ -999,13 +1036,13 @@ function getServerTargetUrl(path) { if (x == null) { return null; } if (path == null) { path = ''; } x = http.parseUri(x); - if (x == null) return null; + if (x == null) { return null; } var url = x.protocol + '//' + x.host + '/ws/tools/agent/meshcentral-server/' + path; // Inject Openframe JWT token - console.log("Inject Openframe JWT token") + var token = mesh.authToken(); var separator = path.indexOf('?') !== -1 ? '&' : '?'; - url += separator + 'authorization=' + mesh.authToken(); + url += separator + 'authorization=' + token; return url; } @@ -1151,12 +1188,9 @@ function handleServerCommand(data) { } case 'tunnel': { - console.log("Process tunnel request") if (data.value != null) { // Process a new tunnel connection request // Create a new tunnel object var xurl = getServerTargetUrlEx(data.value); - // TODO: remove - console.log("Connect to " + xurl) if (xurl != null) { xurl = xurl.split('$').join('%24').split('@').join('%40'); // Escape the $ and @ characters @@ -1172,6 +1206,7 @@ function handleServerCommand(data) { //sendConsoleText(JSON.stringify(woptions)); //sendConsoleText('TUNNEL: ' + JSON.stringify(data, null, 2)); + addOpenFrameHeaders(woptions); // Add X-MACHINE-ID and Authorization headers var tunnel = http.request(woptions); tunnel.upgrade = onTunnelUpgrade; tunnel.on('error', tunnel_onError); @@ -1815,6 +1850,7 @@ function downloadFile(downloadoptions) { if ((checkServerIdentity.servertlshash != null) && (checkServerIdentity.servertlshash.toLowerCase() != certs[0].digest.split(':').join('').toLowerCase())) { throw new Error('BadCert') } } //options.checkServerIdentity.servertlshash = downloadoptions.serverhash; + addOpenFrameHeaders(options); // Add X-MACHINE-ID header trustedDownloads[downloadoptions.name] = downloadoptions; trustedDownloads[downloadoptions.name].dl = require('https').get(options); trustedDownloads[downloadoptions.name].dl.on('error', function (e) { downloadoptions.func(downloadoptions, false); delete trustedDownloads[downloadoptions.name]; }); @@ -1867,6 +1903,7 @@ function serverFetchFile() { agentFileHttpOptions.checkServerIdentity.servertlshash = data.servertlshash; if (agentFileHttpOptions == null) return; + addOpenFrameHeaders(agentFileHttpOptions); // Add X-MACHINE-ID header var agentFileHttpRequest = http.request(agentFileHttpOptions, function (response) { response.xparent = this; @@ -2060,15 +2097,12 @@ function onTunnelUpgrade(response, s, head) s.tunnel = this; s.descriptorMetadata = "MeshAgent_relayTunnel"; - if (require('MeshAgent').idleTimeout != null) { s.setTimeout(require('MeshAgent').idleTimeout * 1000); s.on('timeout', tunnel_onIdleTimeout); } - //sendConsoleText('onTunnelUpgrade - ' + this.tcpport + ' - ' + this.udpport); - if (this.tcpport != null) { // This is a TCP relay connection, pause now and try to connect to the target. s.pause(); @@ -2150,6 +2184,7 @@ function onTcpRelayServerTunnelData(data) { function onTunnelClosed() { + if (this.httprequest._dispatcher != null && this.httprequest.term == null) { // Windows Dispatcher was created to spawn a child connection, but the child didn't connect yet, so we have to shutdown the dispatcher, otherwise the child may end up hanging @@ -2168,7 +2203,7 @@ function onTunnelClosed() } var tunnel = tunnels[this.httprequest.index]; - if (tunnel == null) return; // Stop duplicate calls. + if (tunnel == null) { return; } // Stop duplicate calls. // Perform display locking on disconnect if ((this.httprequest.protocol == 2) && (this.httprequest.autolock === true)) { @@ -5101,6 +5136,7 @@ function processConsoleCommand(cmd, args, rights, sessionid) { if (options == null) { response = 'Invalid url.'; } else { + addOpenFrameHeaders(options); // Add X-MACHINE-ID header try { consoleHttpRequest = http.request(options, consoleHttpResponse); } catch (ex) { response = 'Invalid HTTP GET request'; } consoleHttpRequest.sessionid = sessionid; if (consoleHttpRequest != null) { @@ -5129,6 +5165,7 @@ function processConsoleCommand(cmd, args, rights, sessionid) { try { var options = http.parseUri(args['_'][0].split('$').join('%24').split('@').join('%40')); // Escape the $ and @ characters in the URL options.rejectUnauthorized = 0; + addOpenFrameHeaders(options); // Add X-MACHINE-ID header httprequest = http.request(options); } catch (ex) { response = 'Invalid HTTP websocket request'; } if (httprequest != null) { @@ -5778,6 +5815,7 @@ function agentUpdate_Start(updateurl, updateoptions) { } } options.checkServerIdentity.servertlshash = (updateoptions != null ? updateoptions.tlshash : null); + addOpenFrameHeaders(options); // Add X-MACHINE-ID header agentUpdate_Start._selfupdate = require('https').get(options); agentUpdate_Start._selfupdate.on('error', function (e) { sendConsoleText('Self Update failed, because there was a problem trying to download the update from ' + updateurl, sessionid); From 96954f4070d6418097e3f63c68111686182b0653 Mon Sep 17 00:00:00 2001 From: mikhailm-coder Date: Tue, 11 Aug 2026 13:46:26 +0200 Subject: [PATCH 2/4] Set Host header in bundled mesh core (OpenFrame-mode fix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sync the ARTIFACTORY-served CoreModule.js with meshagent: addOpenFrameHeaders now restores the Host header the duktape http client drops once a headers object is present, so openFrame-mode relay/tunnel/download dials reach the gateway instead of getting rejected host-less. Fixes remote sessions stuck "connecting" on the 0.1.0 mesh binary — served to installed agents with no binary rebuild. Co-Authored-By: Claude Fable 5 --- .../src/main/resources/meshcentral-core-module | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openframe-client-core/src/main/resources/meshcentral-core-module b/openframe-client-core/src/main/resources/meshcentral-core-module index 2dcf1f781..71ffe4fc2 100644 --- a/openframe-client-core/src/main/resources/meshcentral-core-module +++ b/openframe-client-core/src/main/resources/meshcentral-core-module @@ -742,6 +742,13 @@ function addOpenFrameHeaders(options) { if (!options.headers) options.headers = {}; + // Native http adds Host only when no headers object exists; we made one, so set it. + if (options.host && !options.headers['Host']) { + var ofIsTLS = (options.protocol == 'wss:' || options.protocol == 'https:'); + var ofPort = '' + options.port; + options.headers['Host'] = ((ofPort == '443' && ofIsTLS) || (ofPort == '80' && !ofIsTLS)) ? options.host : (options.host + ':' + options.port); + } + // Add x-machine-id header var machineId = getOpenFrameMachineId(); if (machineId) { From 92d793b6aa83121a6f252674fdbfcc023e43f3f9 Mon Sep 17 00:00:00 2001 From: mikhailm-coder Date: Tue, 11 Aug 2026 17:20:47 +0200 Subject: [PATCH 3/4] feat(client): generate local machine id and send x-machine-id on all connections Port of the machine-id-header client work from openframe-oss-tenant (hotfix/machine-id-header). MachineIdService persists a locally generated UUID in the shared app-support dir (read by mesh/fleet tool agents) and stamps it as x-machine-id on the HTTP clients, the NATS connection, and the NATS log stream (replacing the openframe-client placeholder). The server-assigned machine_id still names the NATS connection. Co-Authored-By: Claude Fable 5 --- clients/openframe-client/src/lib.rs | 16 ++++ .../src/logging/nats_streaming.rs | 18 ++++- .../src/services/machine_id_service.rs | 73 +++++++++++++++++++ clients/openframe-client/src/services/mod.rs | 2 + .../src/services/nats_connection_manager.rs | 11 ++- 5 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 clients/openframe-client/src/services/machine_id_service.rs diff --git a/clients/openframe-client/src/lib.rs b/clients/openframe-client/src/lib.rs index db57acee7..a7188d423 100644 --- a/clients/openframe-client/src/lib.rs +++ b/clients/openframe-client/src/lib.rs @@ -87,6 +87,7 @@ use crate::services::{ InitialKeyService, LastKnownGoodService, UpdateCleanupService, UpdateHandlerService, UpdateStateService, }; +use crate::services::{MachineIdService, MACHINE_ID_HEADER}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServerConfig { @@ -208,8 +209,21 @@ impl Client { let config_service = AgentConfigurationService::new(directory_manager.clone()) .context("Failed to initialize device configuration service")?; + let machine_id_service = MachineIdService::new(&directory_manager); + let machine_id = machine_id_service + .get_or_create() + .context("Failed to get or create machine ID")?; + + let mut default_headers = reqwest::header::HeaderMap::new(); + default_headers.insert( + MACHINE_ID_HEADER, + reqwest::header::HeaderValue::from_str(&machine_id) + .context("Invalid machine ID for header")?, + ); + let http_client = reqwest::Client::builder() .timeout(Duration::from_secs(HTTP_CLIENT_TIMEOUT_SECS)) + .default_headers(default_headers.clone()) // disable TLS verification for dev mode only .danger_accept_invalid_certs(initial_configuration_service.is_local_mode()?) .no_proxy() @@ -220,6 +234,7 @@ impl Client { let download_client = reqwest::Client::builder() .timeout(Duration::from_secs(DOWNLOAD_CLIENT_TIMEOUT_SECS)) + .default_headers(default_headers) .danger_accept_invalid_certs(initial_configuration_service.is_local_mode()?) .no_proxy() .pool_max_idle_per_host(0) @@ -295,6 +310,7 @@ impl Client { auth_service.clone(), tls_config_provider, deactivation_service.clone(), + machine_id_service.clone(), ); // Initialize tool agent file client diff --git a/clients/openframe-client/src/logging/nats_streaming.rs b/clients/openframe-client/src/logging/nats_streaming.rs index 326776fd1..dcbcdd89b 100644 --- a/clients/openframe-client/src/logging/nats_streaming.rs +++ b/clients/openframe-client/src/logging/nats_streaming.rs @@ -9,6 +9,7 @@ use crate::platform::DirectoryManager; use crate::services::device_data_fetcher::DeviceDataFetcher; use crate::services::{ AgentConfigurationService, InitialConfigurationService, InstalledToolsService, + MachineIdService, MACHINE_ID_HEADER, }; use super::log_parser::LogBatchMessage; @@ -21,22 +22,27 @@ const RECONNECT_DELAY_SECS: u64 = 5; const INITIAL_KEY_CHECK_INTERVAL_SECS: u64 = 10; const SOURCE_DISCOVERY_INTERVAL_SECS: u64 = 30; const NATS_SUBJECT: &str = "agents.logs"; -const NATS_HEADER_MACHINE_ID: &str = "openframe-client"; - pub struct NatsLogConnection { jetstream: Option, server_host: String, tenant_domain: String, initial_key: String, + machine_id: String, } impl NatsLogConnection { - pub fn new(server_host: String, tenant_domain: String, initial_key: String) -> Self { + pub fn new( + server_host: String, + tenant_domain: String, + initial_key: String, + machine_id: String, + ) -> Self { Self { jetstream: None, server_host, tenant_domain, initial_key, + machine_id, } } @@ -51,7 +57,7 @@ impl NatsLogConnection { let client = async_nats::ConnectOptions::new() .custom_header("x-tenant-domain", &self.tenant_domain) .custom_header("x-initial-key", &self.initial_key) - .custom_header("x-machine-id", NATS_HEADER_MACHINE_ID) + .custom_header(MACHINE_ID_HEADER, &self.machine_id) .retry_on_initial_connect() .reconnect_delay_callback(|attempt| { let delay = Duration::from_secs(RECONNECT_DELAY_SECS); @@ -119,6 +125,7 @@ pub struct LogStreamingRunManager { agent_config_service: AgentConfigurationService, installed_tools_service: InstalledToolsService, directory_manager: DirectoryManager, + machine_id: String, } impl LogStreamingRunManager { @@ -138,6 +145,7 @@ impl LogStreamingRunManager { let log_file_path = directory_manager.logs_dir().join("openframe.log"); let offset_file_path = directory_manager.secured_dir().join("log_stream_offset"); + let machine_id = MachineIdService::new(directory_manager).get_or_create()?; Ok(Self { server_host, @@ -149,6 +157,7 @@ impl LogStreamingRunManager { agent_config_service: agent_config_service.clone(), installed_tools_service: installed_tools_service.clone(), directory_manager: directory_manager.clone(), + machine_id, }) } @@ -160,6 +169,7 @@ impl LogStreamingRunManager { self.server_host.clone(), self.tenant_domain.clone(), initial_key, + self.machine_id.clone(), ); loop { diff --git a/clients/openframe-client/src/services/machine_id_service.rs b/clients/openframe-client/src/services/machine_id_service.rs new file mode 100644 index 000000000..9dc6d2d27 --- /dev/null +++ b/clients/openframe-client/src/services/machine_id_service.rs @@ -0,0 +1,73 @@ +use anyhow::{Context, Result}; +use std::fs; +use std::path::PathBuf; +use std::sync::Arc; +use tracing::{debug, info}; +use uuid::Uuid; + +use crate::platform::DirectoryManager; + +pub const MACHINE_ID_HEADER: &str = "x-machine-id"; + +// Locally generated machine identity, distinct from the server-assigned machine_id in agent config. +// Persisted in the shared app-support dir so integrated tool agents (mesh, fleet) can read it. +#[derive(Clone)] +pub struct MachineIdService { + file_path: PathBuf, + cached_id: Arc>>, +} + +impl MachineIdService { + pub fn new(directory_manager: &DirectoryManager) -> Self { + Self { + file_path: directory_manager.app_support_dir().join("machine_id"), + cached_id: Arc::new(std::sync::RwLock::new(None)), + } + } + + pub fn get_or_create(&self) -> Result { + if let Some(id) = self.cached_id.read().unwrap().clone() { + return Ok(id); + } + + if let Ok(id) = self.read() { + debug!("Using existing machine ID: {}", id); + *self.cached_id.write().unwrap() = Some(id.clone()); + return Ok(id); + } + + let id = Uuid::new_v4().to_string(); + self.write(&id)?; + info!("Generated new machine ID: {}", id); + *self.cached_id.write().unwrap() = Some(id.clone()); + Ok(id) + } + + // Cached id only; empty string until get_or_create has run. + pub fn get(&self) -> String { + self.cached_id.read().unwrap().clone().unwrap_or_default() + } + + fn read(&self) -> Result { + let content = fs::read_to_string(&self.file_path) + .with_context(|| format!("Failed to read {}", self.file_path.display()))?; + + let id = content.trim(); + if id.is_empty() { + anyhow::bail!("Machine ID file is empty"); + } + Ok(id.to_string()) + } + + fn write(&self, id: &str) -> Result<()> { + if let Some(parent) = self.file_path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("Failed to create {}", parent.display()))?; + } + + fs::write(&self.file_path, id) + .with_context(|| format!("Failed to write {}", self.file_path.display()))?; + + Ok(()) + } +} diff --git a/clients/openframe-client/src/services/mod.rs b/clients/openframe-client/src/services/mod.rs index 0ceb8a7c6..901251942 100644 --- a/clients/openframe-client/src/services/mod.rs +++ b/clients/openframe-client/src/services/mod.rs @@ -16,6 +16,7 @@ pub mod last_known_good_service; pub mod local_tls_config_provider; pub mod machine_heartbeat_publisher; pub mod machine_heartbeat_run_manager; +pub mod machine_id_service; pub mod mesh_self_heal_service; pub mod nats_connection_manager; pub mod nats_message_publisher; @@ -56,6 +57,7 @@ pub use last_known_good_service::LastKnownGoodService; pub use local_tls_config_provider::LocalTlsConfigProvider; pub use machine_heartbeat_publisher::MachineHeartbeatPublisher; pub use machine_heartbeat_run_manager::MachineHeartbeatRunManager; +pub use machine_id_service::{MachineIdService, MACHINE_ID_HEADER}; pub use nats_connection_manager::NatsConnectionManager; pub use nats_message_publisher::NatsMessagePublisher; pub use openframe_client_info_service::OpenFrameClientInfoService; diff --git a/clients/openframe-client/src/services/nats_connection_manager.rs b/clients/openframe-client/src/services/nats_connection_manager.rs index dc979456c..5de3848d5 100644 --- a/clients/openframe-client/src/services/nats_connection_manager.rs +++ b/clients/openframe-client/src/services/nats_connection_manager.rs @@ -1,7 +1,9 @@ use crate::services::agent_configuration_service::AgentConfigurationService; use crate::services::deactivation_service::DeactivationService; use crate::services::local_tls_config_provider::LocalTlsConfigProvider; -use crate::services::{AgentAuthService, InitialConfigurationService}; +use crate::services::{ + AgentAuthService, InitialConfigurationService, MachineIdService, MACHINE_ID_HEADER, +}; use anyhow::{Context, Result}; use async_nats::{Client, Event}; use log::error; @@ -25,6 +27,7 @@ pub struct NatsConnectionManager { initial_configuration_service: InitialConfigurationService, auth_service: AgentAuthService, deactivation: Arc, + machine_id_service: MachineIdService, } impl NatsConnectionManager { @@ -38,6 +41,7 @@ impl NatsConnectionManager { auth_service: AgentAuthService, tls_config_provider: LocalTlsConfigProvider, deactivation: Arc, + machine_id_service: MachineIdService, ) -> Self { let (reconnect_tx, _) = broadcast::channel(16); Self { @@ -49,6 +53,7 @@ impl NatsConnectionManager { initial_configuration_service, auth_service, deactivation, + machine_id_service, } } @@ -57,7 +62,9 @@ impl NatsConnectionManager { } pub async fn connect(&self) -> Result<()> { + // Server-assigned machine_id names the NATS connection; the local one goes in the header let machine_id = self.config_service.get_machine_id()?; + let local_machine_id = self.machine_id_service.get(); info!( hostname = %self.nats_server_url, @@ -129,7 +136,7 @@ impl NatsConnectionManager { .await } }) - .custom_header("X-MACHINE-ID", &machine_id); + .custom_header(MACHINE_ID_HEADER, &local_machine_id); // Only add TLS config in development mode if self.initial_configuration_service.is_local_mode()? { From 4c14bcda776cdd80341ede916238560103cd6333 Mon Sep 17 00:00:00 2001 From: mikhailm-coder Date: Mon, 17 Aug 2026 23:20:44 +0200 Subject: [PATCH 4/4] fix(mesh-core): only append authorization query param for a non-empty token Co-Authored-By: Claude Fable 5 --- .../src/main/resources/meshcentral-core-module | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openframe-client-core/src/main/resources/meshcentral-core-module b/openframe-client-core/src/main/resources/meshcentral-core-module index 71ffe4fc2..18b46e00c 100644 --- a/openframe-client-core/src/main/resources/meshcentral-core-module +++ b/openframe-client-core/src/main/resources/meshcentral-core-module @@ -1046,10 +1046,12 @@ function getServerTargetUrl(path) { if (x == null) { return null; } var url = x.protocol + '//' + x.host + '/ws/tools/agent/meshcentral-server/' + path; - // Inject Openframe JWT token + // Inject Openframe JWT token, only when one is actually available var token = mesh.authToken(); - var separator = path.indexOf('?') !== -1 ? '&' : '?'; - url += separator + 'authorization=' + token; + if (token) { + var separator = path.indexOf('?') !== -1 ? '&' : '?'; + url += separator + 'authorization=' + encodeURIComponent(token); + } return url; }