3 Commits

Author SHA1 Message Date
Jaakko Vanhala
84b78eb9c6 GPU-tunnistus valinnainen: cargo run --no-default-features toimii ilman nvml/wgpu
Native-node kääntyy nyt macOS:llä ja muilla koneilla ilman NVIDIA-ajureita:
  cargo run --no-default-features  ← vain Ollama, ei GPU-tunnistusta
  cargo run                        ← oletus: GPU-tunnistus mukana (nvml + wgpu)

Feature flag "gpu-detect" kontrolloi nvml-wrapper ja wgpu -riippuvuuksia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:42:35 +03:00
Jaakko Vanhala
4f18377a3b Native-node lähettää NODE_API_KEY auth-viestissä hubille
Luetaan NODE_API_KEY-ympäristömuuttuja ja lisätään api_key-kenttä
auth-viestiin. Hub tarkistaa avaimen ja hylkää solmun jos se ei täsmää.

Käyttö:
  NODE_API_KEY=kpn_sk_abc123 HUB_URL=ws://hub:3000/ws cargo run

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:39:48 +03:00
Jaakko Vanhala
7f5bb45138 API-avain -autentikaatio natiivisolmuille
Natiivisolmujen (node_type: native) auth-viesti vaatii api_key-kentän
joka vastaa hubin NODE_API_KEY-ympäristömuuttujaa. Virheellinen avain
sulkee WebSocket-yhteyden.

Selainsolmut eivät vaadi avainta (Origin-validointi suojaa niitä).
Jos NODE_API_KEY ei ole asetettu, kaikki natiivisolmut hyväksytään
(kehitysympäristö).

Käyttö:
  Hub:  NODE_API_KEY=kpn_sk_abc123 cargo run
  Node: NODE_API_KEY=kpn_sk_abc123 cargo run

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 15:39:05 +03:00
3 changed files with 42 additions and 10 deletions

View File

@@ -691,6 +691,18 @@ async fn handle_socket(socket: WebSocket, state: Arc<AppState>, ip: IpAddr) {
let allocated = json.get("allocated_gb").and_then(|v| v.as_u64()).unwrap_or(4) as u32; let allocated = json.get("allocated_gb").and_then(|v| v.as_u64()).unwrap_or(4) as u32;
let node_type = json.get("node_type").and_then(|v| v.as_str()).unwrap_or("browser"); let node_type = json.get("node_type").and_then(|v| v.as_str()).unwrap_or("browser");
// API-avain vaaditaan natiivisolmuilta (ei selaimilta)
if node_type == "native" {
let required_key = std::env::var("NODE_API_KEY").unwrap_or_default();
if !required_key.is_empty() {
let provided_key = json.get("api_key").and_then(|v| v.as_str()).unwrap_or("");
if provided_key != required_key {
tracing::warn!("Solmu {} ({}) hylätty: virheellinen API-avain", node_id, ip);
break; // Suljetaan WebSocket
}
}
}
{ {
let mut map = state.nodes_vram.lock().unwrap(); let mut map = state.nodes_vram.lock().unwrap();
map.insert(node_id, allocated); map.insert(node_id, allocated);

View File

@@ -3,6 +3,10 @@ name = "native-node"
version = "0.2.2" version = "0.2.2"
edition = "2024" edition = "2024"
[features]
default = ["gpu-detect"]
gpu-detect = ["nvml-wrapper", "wgpu"]
[dependencies] [dependencies]
tokio = { version = "1.36", features = ["full"] } tokio = { version = "1.36", features = ["full"] }
tokio-tungstenite = { version = "0.21", features = ["native-tls"] } tokio-tungstenite = { version = "0.21", features = ["native-tls"] }
@@ -10,8 +14,8 @@ futures-util = "0.3"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
sysinfo = "0.30" sysinfo = "0.30"
nvml-wrapper = "0.10" nvml-wrapper = { version = "0.10", optional = true }
wgpu = "24" wgpu = { version = "24", optional = true }
reqwest = { version = "0.12", features = ["json"] } reqwest = { version = "0.12", features = ["json"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }

View File

@@ -33,6 +33,7 @@ impl GpuInfo {
} }
} }
#[cfg(feature = "gpu-detect")]
/// Tunnistaa kaikki GPU:t wgpu:lla (NVIDIA/AMD/Apple/Intel) /// Tunnistaa kaikki GPU:t wgpu:lla (NVIDIA/AMD/Apple/Intel)
fn collect_gpus_wgpu() -> Vec<GpuInfo> { fn collect_gpus_wgpu() -> Vec<GpuInfo> {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
@@ -84,6 +85,7 @@ fn collect_gpus_wgpu() -> Vec<GpuInfo> {
gpus gpus
} }
#[cfg(feature = "gpu-detect")]
/// Täydentää NVIDIA-GPU:iden tiedot NVML:llä (VRAM, lämpötila, kuormitus) /// Täydentää NVIDIA-GPU:iden tiedot NVML:llä (VRAM, lämpötila, kuormitus)
fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) { fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
let Ok(nvml) = nvml_wrapper::Nvml::init() else { return }; let Ok(nvml) = nvml_wrapper::Nvml::init() else { return };
@@ -109,6 +111,7 @@ fn enrich_nvidia_gpus(gpus: &mut [GpuInfo]) {
} }
} }
#[cfg(feature = "gpu-detect")]
/// AMD GPU-tiedot Linuxin sysfs:stä (/sys/class/drm/) /// AMD GPU-tiedot Linuxin sysfs:stä (/sys/class/drm/)
fn enrich_amd_gpus(gpus: &mut [GpuInfo]) { fn enrich_amd_gpus(gpus: &mut [GpuInfo]) {
let Ok(entries) = std::fs::read_dir("/sys/class/drm") else { return }; let Ok(entries) = std::fs::read_dir("/sys/class/drm") else { return };
@@ -150,10 +153,12 @@ fn enrich_amd_gpus(gpus: &mut [GpuInfo]) {
} }
} }
#[cfg(feature = "gpu-detect")]
fn read_sysfs_u64(path: &std::path::Path) -> Option<u64> { fn read_sysfs_u64(path: &std::path::Path) -> Option<u64> {
std::fs::read_to_string(path).ok()?.trim().parse().ok() std::fs::read_to_string(path).ok()?.trim().parse().ok()
} }
#[cfg(feature = "gpu-detect")]
fn find_hwmon_temp(device_path: &std::path::Path) -> Option<u64> { fn find_hwmon_temp(device_path: &std::path::Path) -> Option<u64> {
let hwmon_dir = device_path.join("hwmon"); let hwmon_dir = device_path.join("hwmon");
let entries = std::fs::read_dir(&hwmon_dir).ok()?; let entries = std::fs::read_dir(&hwmon_dir).ok()?;
@@ -166,8 +171,8 @@ fn find_hwmon_temp(device_path: &std::path::Path) -> Option<u64> {
None None
} }
#[cfg(feature = "gpu-detect")]
/// Apple GPU-tiedot — wgpu/Metal antaa nimen, tarkempaa dataa ei saa ilman IOKit:ia /// Apple GPU-tiedot — wgpu/Metal antaa nimen, tarkempaa dataa ei saa ilman IOKit:ia
/// mutta Metal adapter_info sisältää jo olennaiset tiedot
fn enrich_apple_gpus(gpus: &mut [GpuInfo]) { fn enrich_apple_gpus(gpus: &mut [GpuInfo]) {
// Apple Silicon -koneiden unified memory: koko RAM on GPU:n käytettävissä // Apple Silicon -koneiden unified memory: koko RAM on GPU:n käytettävissä
// Arvioidaan system RAM:sta // Arvioidaan system RAM:sta
@@ -187,13 +192,18 @@ fn enrich_apple_gpus(gpus: &mut [GpuInfo]) {
/// Kerää kaikki GPU:t ja täydentää valmistajakohtaiset tiedot /// Kerää kaikki GPU:t ja täydentää valmistajakohtaiset tiedot
fn collect_all_gpus() -> Vec<GpuInfo> { fn collect_all_gpus() -> Vec<GpuInfo> {
#[cfg(feature = "gpu-detect")]
{
let mut gpus = collect_gpus_wgpu(); let mut gpus = collect_gpus_wgpu();
enrich_nvidia_gpus(&mut gpus); enrich_nvidia_gpus(&mut gpus);
enrich_amd_gpus(&mut gpus); enrich_amd_gpus(&mut gpus);
enrich_apple_gpus(&mut gpus); enrich_apple_gpus(&mut gpus);
return gpus;
gpus }
#[cfg(not(feature = "gpu-detect"))]
{
Vec::new()
}
} }
/// Kerää järjestelmätiedot (CPU, RAM, OS) /// Kerää järjestelmätiedot (CPU, RAM, OS)
@@ -222,6 +232,8 @@ fn build_auth_message(allocated_gb: u32) -> String {
v v
}).collect(); }).collect();
let api_key = std::env::var("NODE_API_KEY").unwrap_or_default();
let mut msg = json!({ let mut msg = json!({
"type": "auth", "type": "auth",
"status": "agent_ready", "status": "agent_ready",
@@ -231,6 +243,10 @@ fn build_auth_message(allocated_gb: u32) -> String {
"system": sys, "system": sys,
}); });
if !api_key.is_empty() {
msg.as_object_mut().unwrap().insert("api_key".to_string(), json!(api_key));
}
if !gpu_json.is_empty() { if !gpu_json.is_empty() {
msg.as_object_mut().unwrap().insert("gpus".to_string(), json!(gpu_json)); msg.as_object_mut().unwrap().insert("gpus".to_string(), json!(gpu_json));
} }