1#![allow(clippy::missing_safety_doc)]
44#![expect(
45 clippy::undocumented_unsafe_blocks,
46 reason = "module-wide FFI safety contract documented in the # Safety preamble above"
47)]
48#![expect(
49 clippy::multiple_unsafe_ops_per_block,
50 reason = "FFI entry points routinely deref + write to multiple out-parameter fields under the same caller contract; splitting per-op would obscure the single boundary-cross"
51)]
52
53use std::ffi::{c_char, c_int, CStr, CString};
54use std::mem::ManuallyDrop;
55use std::sync::Arc;
56
57use bytes::Bytes;
58use serde::{Deserialize, Serialize};
59use tokio::runtime::Runtime;
60
61use crate::adapter::net::identity::{
62 EntityId, PermissionToken, TokenCache, TokenError as CoreTokenError, TokenScope,
63};
64use crate::adapter::net::{
65 ChannelConfig as InnerChannelConfig, ChannelConfigRegistry, ChannelHash, ChannelId,
66 ChannelName as InnerChannelName, ChannelPublisher, EntityKeypair, MeshNode, MeshNodeConfig,
67 OnFailure as InnerOnFailure, PublishConfig as InnerPublishConfig,
68 PublishReport as InnerPublishReport, Reliability, Stream as CoreStream, StreamConfig,
69 StreamError, Visibility as InnerVisibility, DEFAULT_STREAM_WINDOW_BYTES,
70};
71use crate::adapter::net::{SubnetId, SubnetPolicy, SubnetRule};
72use crate::adapter::Adapter;
73use crate::error::AdapterError;
74
75use super::handle_guard::{HandleGuard, FFI_HANDLE_FREE_DEADLINE};
76use super::NetError;
77
78pub(crate) const NET_ERR_MESH_INIT: c_int = -110;
84pub(crate) const NET_ERR_MESH_HANDSHAKE: c_int = -111;
85pub(crate) const NET_ERR_MESH_BACKPRESSURE: c_int = -112;
86pub(crate) const NET_ERR_MESH_NOT_CONNECTED: c_int = -113;
87pub(crate) const NET_ERR_MESH_TRANSPORT: c_int = -114;
88pub(crate) const NET_ERR_CHANNEL: c_int = -115;
89pub(crate) const NET_ERR_CHANNEL_AUTH: c_int = -116;
90
91pub(crate) const NET_ERR_IDENTITY: c_int = -120;
96pub(crate) const NET_ERR_TOKEN_INVALID_FORMAT: c_int = -121;
97pub(crate) const NET_ERR_TOKEN_INVALID_SIGNATURE: c_int = -122;
98pub(crate) const NET_ERR_TOKEN_EXPIRED: c_int = -123;
99pub(crate) const NET_ERR_TOKEN_NOT_YET_VALID: c_int = -124;
100pub(crate) const NET_ERR_TOKEN_DELEGATION_EXHAUSTED: c_int = -125;
101pub(crate) const NET_ERR_TOKEN_DELEGATION_NOT_ALLOWED: c_int = -126;
102pub(crate) const NET_ERR_TOKEN_NOT_AUTHORIZED: c_int = -127;
103
104#[cfg(feature = "nat-traversal")]
117pub(crate) const NET_ERR_TRAVERSAL_REFLEX_TIMEOUT: c_int = -130;
118#[cfg(feature = "nat-traversal")]
119pub(crate) const NET_ERR_TRAVERSAL_PEER_NOT_REACHABLE: c_int = -131;
120#[cfg(feature = "nat-traversal")]
121pub(crate) const NET_ERR_TRAVERSAL_TRANSPORT: c_int = -132;
122#[cfg(feature = "nat-traversal")]
123pub(crate) const NET_ERR_TRAVERSAL_RENDEZVOUS_NO_RELAY: c_int = -133;
124#[cfg(feature = "nat-traversal")]
125pub(crate) const NET_ERR_TRAVERSAL_RENDEZVOUS_REJECTED: c_int = -134;
126#[cfg(feature = "nat-traversal")]
127pub(crate) const NET_ERR_TRAVERSAL_PUNCH_FAILED: c_int = -135;
128#[cfg(feature = "nat-traversal")]
129pub(crate) const NET_ERR_TRAVERSAL_PORT_MAP_UNAVAILABLE: c_int = -136;
130pub(crate) const NET_ERR_TRAVERSAL_UNSUPPORTED: c_int = -137;
136
137#[cfg(feature = "nat-traversal")]
138fn traversal_err_to_code(e: &crate::adapter::net::traversal::TraversalError) -> c_int {
139 use crate::adapter::net::traversal::TraversalError;
140 match e {
141 TraversalError::ReflexTimeout => NET_ERR_TRAVERSAL_REFLEX_TIMEOUT,
142 TraversalError::PeerNotReachable => NET_ERR_TRAVERSAL_PEER_NOT_REACHABLE,
143 TraversalError::Transport(_) => NET_ERR_TRAVERSAL_TRANSPORT,
144 TraversalError::RendezvousNoRelay => NET_ERR_TRAVERSAL_RENDEZVOUS_NO_RELAY,
145 TraversalError::RendezvousRejected(_) => NET_ERR_TRAVERSAL_RENDEZVOUS_REJECTED,
146 TraversalError::PunchFailed => NET_ERR_TRAVERSAL_PUNCH_FAILED,
147 TraversalError::PortMapUnavailable => NET_ERR_TRAVERSAL_PORT_MAP_UNAVAILABLE,
148 TraversalError::Unsupported => NET_ERR_TRAVERSAL_UNSUPPORTED,
149 }
150}
151
152#[cfg(feature = "nat-traversal")]
156fn nat_class_to_str(class: crate::adapter::net::traversal::classify::NatClass) -> &'static str {
157 use crate::adapter::net::traversal::classify::NatClass;
158 match class {
159 NatClass::Open => "open",
160 NatClass::Cone => "cone",
161 NatClass::Symmetric => "symmetric",
162 NatClass::Unknown => "unknown",
163 }
164}
165
166fn token_err_to_code(e: &CoreTokenError) -> c_int {
167 match e {
168 CoreTokenError::InvalidFormat => NET_ERR_TOKEN_INVALID_FORMAT,
169 CoreTokenError::InvalidSignature => NET_ERR_TOKEN_INVALID_SIGNATURE,
170 CoreTokenError::Expired => NET_ERR_TOKEN_EXPIRED,
171 CoreTokenError::NotYetValid => NET_ERR_TOKEN_NOT_YET_VALID,
172 CoreTokenError::DelegationExhausted => NET_ERR_TOKEN_DELEGATION_EXHAUSTED,
173 CoreTokenError::DelegationNotAllowed => NET_ERR_TOKEN_DELEGATION_NOT_ALLOWED,
174 CoreTokenError::NotAuthorized => NET_ERR_TOKEN_NOT_AUTHORIZED,
175 CoreTokenError::Revoked => NET_ERR_TOKEN_NOT_AUTHORIZED,
180 CoreTokenError::ReadOnly => NET_ERR_IDENTITY,
185 CoreTokenError::ZeroTtl => NET_ERR_TOKEN_INVALID_FORMAT,
192 CoreTokenError::TtlTooLong => NET_ERR_TOKEN_INVALID_FORMAT,
196 }
197}
198
199fn runtime() -> &'static Arc<Runtime> {
215 use std::sync::OnceLock;
216 static RT: OnceLock<Arc<Runtime>> = OnceLock::new();
217 RT.get_or_init(|| {
218 match tokio::runtime::Builder::new_multi_thread()
219 .enable_all()
220 .build()
221 {
222 Ok(rt) => Arc::new(rt),
223 Err(e) => {
224 eprintln!(
225 "FATAL: mesh FFI tokio runtime build failure ({e:?}); aborting to avoid panic across the FFI boundary"
226 );
227 std::process::abort();
228 }
229 }
230 })
231}
232
233pub(super) fn block_on<F: std::future::Future>(future: F) -> F::Output {
253 if tokio::runtime::Handle::try_current().is_ok() {
254 eprintln!(
255 "FATAL: mesh FFI called from inside a tokio runtime context; \
256 aborting to avoid runtime-in-runtime panic across the FFI boundary"
257 );
258 std::process::abort();
259 }
260 runtime().block_on(future)
261}
262
263#[inline]
286pub(super) unsafe fn c_str_to_string(p: *const c_char) -> Option<String> {
287 if p.is_null() {
288 return None;
289 }
290 CStr::from_ptr(p).to_str().ok().map(str::to_owned)
291}
292
293fn write_json_out<T: Serialize>(
299 value: &T,
300 out_ptr: *mut *mut c_char,
301 out_len: *mut usize,
302) -> c_int {
303 if out_ptr.is_null() || out_len.is_null() {
304 return NetError::NullPointer.into();
305 }
306 let Ok(s) = serde_json::to_string(value) else {
307 return NetError::Unknown.into();
308 };
309 let len = s.len();
310 let Ok(cs) = CString::new(s) else {
311 return NetError::Unknown.into();
312 };
313 unsafe {
314 *out_ptr = cs.into_raw();
315 *out_len = len;
316 }
317 0
318}
319
320pub(super) fn write_string_out(s: String, out_ptr: *mut *mut c_char, out_len: *mut usize) -> c_int {
321 if out_ptr.is_null() || out_len.is_null() {
322 return NetError::NullPointer.into();
323 }
324 let len = s.len();
325 let Ok(cs) = CString::new(s) else {
326 return NetError::Unknown.into();
327 };
328 unsafe {
329 *out_ptr = cs.into_raw();
330 *out_len = len;
331 }
332 0
333}
334
335fn adapter_err_to_code(err: &AdapterError) -> c_int {
336 match err {
337 AdapterError::Connection(_) => NET_ERR_MESH_HANDSHAKE,
338 _ => NET_ERR_MESH_TRANSPORT,
339 }
340}
341
342fn stream_err_to_code(err: &StreamError) -> c_int {
343 match err {
344 StreamError::Backpressure => NET_ERR_MESH_BACKPRESSURE,
345 StreamError::NotConnected => NET_ERR_MESH_NOT_CONNECTED,
346 StreamError::Transport(_) => NET_ERR_MESH_TRANSPORT,
347 }
348}
349
350#[derive(Deserialize)]
355struct SubnetPolicyJson {
356 #[serde(default)]
357 rules: Vec<SubnetRuleJson>,
358}
359
360#[derive(Deserialize)]
361struct SubnetRuleJson {
362 tag_prefix: String,
363 level: u32,
364 #[serde(default)]
365 values: std::collections::HashMap<String, u32>,
366}
367
368fn u8_from_u32(value: u32) -> Option<u8> {
369 if value > 255 {
370 None
371 } else {
372 Some(value as u8)
373 }
374}
375
376fn subnet_id_from_json(levels: Vec<u32>) -> Option<SubnetId> {
377 if levels.is_empty() || levels.len() > 4 {
378 return None;
379 }
380 let mut bytes = [0u8; 4];
381 for (i, raw) in levels.iter().enumerate() {
382 bytes[i] = u8_from_u32(*raw)?;
383 }
384 Some(SubnetId::new(&bytes[..levels.len()]))
385}
386
387fn subnet_policy_from_json(p: SubnetPolicyJson) -> Option<SubnetPolicy> {
388 let mut policy = SubnetPolicy::new();
389 for rule_json in p.rules {
390 let level = u8_from_u32(rule_json.level)?;
391 if level > 3 {
392 return None;
393 }
394 let mut rule = SubnetRule::new(rule_json.tag_prefix, level);
395 for (tag_value, raw_val) in rule_json.values {
396 let v = u8_from_u32(raw_val)?;
397 if v == 0 {
403 return None;
404 }
405 rule = rule.map(tag_value, v);
406 }
407 policy = policy.add_rule(rule);
408 }
409 Some(policy)
410}
411
412#[derive(Deserialize)]
413struct MeshNewConfig {
414 bind_addr: String,
415 psk_hex: String,
417 heartbeat_ms: Option<u64>,
418 session_timeout_ms: Option<u64>,
419 num_shards: Option<u16>,
420 capability_gc_interval_ms: Option<u64>,
423 require_signed_capabilities: Option<bool>,
426 subnet: Option<Vec<u32>>,
428 subnet_policy: Option<SubnetPolicyJson>,
430 identity_seed_hex: Option<String>,
435 #[serde(default)]
441 reflex_override: Option<String>,
442 #[serde(default)]
446 try_port_mapping: bool,
447}
448
449pub struct MeshNodeHandle {
462 inner: ManuallyDrop<Arc<MeshNode>>,
463 channel_configs: ManuallyDrop<Arc<ChannelConfigRegistry>>,
464 guard: HandleGuard,
465}
466
467#[unsafe(no_mangle)]
482pub unsafe extern "C" fn net_mesh_new(
483 config_json: *const c_char,
484 out_handle: *mut *mut MeshNodeHandle,
485) -> c_int {
486 if config_json.is_null() || out_handle.is_null() {
487 return NetError::NullPointer.into();
488 }
489 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
490 return NetError::InvalidUtf8.into();
491 };
492 let cfg: MeshNewConfig = match serde_json::from_str(&s) {
493 Ok(v) => v,
494 Err(_) => return NetError::InvalidJson.into(),
495 };
496 let bind_addr: std::net::SocketAddr = match cfg.bind_addr.parse() {
497 Ok(a) => a,
498 Err(_) => return NET_ERR_MESH_INIT,
499 };
500 let psk_bytes = match hex::decode(&cfg.psk_hex) {
501 Ok(b) => b,
502 Err(_) => return NET_ERR_MESH_INIT,
503 };
504 if psk_bytes.len() != 32 {
505 return NET_ERR_MESH_INIT;
506 }
507 let mut psk = [0u8; 32];
508 psk.copy_from_slice(&psk_bytes);
509
510 let mut node_cfg = MeshNodeConfig::new(bind_addr, psk);
511 if let Some(ms) = cfg.heartbeat_ms {
519 if ms == 0 {
520 return NetError::InvalidJson.into();
521 }
522 node_cfg = node_cfg.with_heartbeat_interval(std::time::Duration::from_millis(ms));
523 }
524 if let Some(ms) = cfg.session_timeout_ms {
525 if ms == 0 {
526 return NetError::InvalidJson.into();
527 }
528 node_cfg = node_cfg.with_session_timeout(std::time::Duration::from_millis(ms));
529 }
530 if let Some(n) = cfg.num_shards {
531 node_cfg = node_cfg.with_num_shards(n);
532 }
533 if let Some(ms) = cfg.capability_gc_interval_ms {
534 node_cfg = node_cfg.with_capability_gc_interval(std::time::Duration::from_millis(ms));
535 }
536 if let Some(b) = cfg.require_signed_capabilities {
537 node_cfg = node_cfg.with_require_signed_capabilities(b);
538 }
539 if let Some(levels) = cfg.subnet {
540 let Some(id) = subnet_id_from_json(levels) else {
541 return NET_ERR_MESH_INIT;
542 };
543 node_cfg = node_cfg.with_subnet(id);
544 }
545 if let Some(policy_js) = cfg.subnet_policy {
546 let Some(policy) = subnet_policy_from_json(policy_js) else {
547 return NET_ERR_MESH_INIT;
548 };
549 node_cfg = node_cfg.with_subnet_policy(Arc::new(policy));
550 }
551 #[cfg(feature = "nat-traversal")]
552 if let Some(external_str) = cfg.reflex_override.as_deref() {
553 let Ok(external) = external_str.parse::<std::net::SocketAddr>() else {
554 return NET_ERR_MESH_INIT;
555 };
556 node_cfg = node_cfg.with_reflex_override(external);
557 }
558 #[cfg(not(feature = "nat-traversal"))]
562 let _ = cfg.reflex_override;
563 #[cfg(feature = "port-mapping")]
564 if cfg.try_port_mapping {
565 node_cfg = node_cfg.with_try_port_mapping(true);
566 }
567 #[cfg(not(feature = "port-mapping"))]
569 let _ = cfg.try_port_mapping;
570
571 let identity = match cfg.identity_seed_hex {
572 Some(seed_hex) => {
573 let bytes = match hex::decode(&seed_hex) {
574 Ok(b) => b,
575 Err(_) => return NET_ERR_MESH_INIT,
576 };
577 if bytes.len() != 32 {
578 return NET_ERR_MESH_INIT;
579 }
580 let mut arr = [0u8; 32];
581 arr.copy_from_slice(&bytes);
582 EntityKeypair::from_bytes(arr)
583 }
584 None => EntityKeypair::generate(),
585 };
586 let result = block_on(async move { MeshNode::new(identity, node_cfg).await });
587 match result {
588 Ok(mut node) => {
589 let channel_configs = Arc::new(ChannelConfigRegistry::new());
590 node.set_channel_configs(channel_configs.clone());
591 node.set_token_cache(Arc::new(TokenCache::new()));
595 let handle = Box::new(MeshNodeHandle {
596 inner: ManuallyDrop::new(Arc::new(node)),
597 channel_configs: ManuallyDrop::new(channel_configs),
598 guard: HandleGuard::new(),
599 });
600 unsafe {
601 *out_handle = Box::into_raw(handle);
602 }
603 0
604 }
605 Err(_) => NET_ERR_MESH_INIT,
606 }
607}
608
609#[unsafe(no_mangle)]
610pub unsafe extern "C" fn net_mesh_free(handle: *mut MeshNodeHandle) {
611 if handle.is_null() {
612 return;
613 }
614 let h: &MeshNodeHandle = unsafe { &*handle };
619 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
620 unsafe {
622 let mh = &mut *handle;
623 let inner = ManuallyDrop::take(&mut mh.inner);
624 let configs = ManuallyDrop::take(&mut mh.channel_configs);
625 drop(inner);
626 drop(configs);
627 }
628 } else {
629 tracing::warn!(
630 "net_mesh_free: in-flight ops did not drain within deadline; \
631 leaking inner to avoid use-after-free"
632 );
633 }
634}
635
636#[cfg(any(feature = "cortex", feature = "dataforts"))]
654pub(super) fn mesh_node_arc(h: &MeshNodeHandle) -> Option<Arc<MeshNode>> {
655 let _op = h.guard.try_enter()?;
656 Some(Arc::clone(&h.inner))
657}
658
659#[unsafe(no_mangle)]
667pub unsafe extern "C" fn net_mesh_arc_clone(handle: *mut MeshNodeHandle) -> *mut Arc<MeshNode> {
668 if handle.is_null() {
669 return std::ptr::null_mut();
670 }
671 let h = unsafe { &*handle };
672 let _op = match h.guard.try_enter() {
674 Some(op) => op,
675 None => return std::ptr::null_mut(),
676 };
677 let cloned: Arc<MeshNode> = Arc::clone(&h.inner);
678 Box::into_raw(Box::new(cloned))
679}
680
681#[unsafe(no_mangle)]
688pub unsafe extern "C" fn net_mesh_channel_configs_arc_clone(
689 handle: *mut MeshNodeHandle,
690) -> *mut Arc<ChannelConfigRegistry> {
691 if handle.is_null() {
692 return std::ptr::null_mut();
693 }
694 let h = unsafe { &*handle };
695 let _op = match h.guard.try_enter() {
697 Some(op) => op,
698 None => return std::ptr::null_mut(),
699 };
700 let cloned: Arc<ChannelConfigRegistry> = Arc::clone(&h.channel_configs);
701 Box::into_raw(Box::new(cloned))
702}
703
704#[unsafe(no_mangle)]
707pub unsafe extern "C" fn net_mesh_arc_free(p: *mut Arc<MeshNode>) {
708 if p.is_null() {
709 return;
710 }
711 unsafe {
712 drop(Box::from_raw(p));
713 }
714}
715
716#[unsafe(no_mangle)]
719pub unsafe extern "C" fn net_mesh_channel_configs_arc_free(p: *mut Arc<ChannelConfigRegistry>) {
720 if p.is_null() {
721 return;
722 }
723 unsafe {
724 drop(Box::from_raw(p));
725 }
726}
727
728#[unsafe(no_mangle)]
731pub unsafe extern "C" fn net_mesh_public_key_hex(
732 handle: *mut MeshNodeHandle,
733 out_ptr: *mut *mut c_char,
734 out_len: *mut usize,
735) -> c_int {
736 if handle.is_null() || out_ptr.is_null() || out_len.is_null() {
737 return NetError::NullPointer.into();
738 }
739 let h = unsafe { &*handle };
740 let _op = match h.guard.try_enter() {
741 Some(op) => op,
742 None => return NetError::ShuttingDown.into(),
743 };
744 let s = hex::encode(h.inner.public_key());
745 write_string_out(s, out_ptr, out_len)
746}
747
748#[unsafe(no_mangle)]
749pub unsafe extern "C" fn net_mesh_node_id(handle: *mut MeshNodeHandle) -> u64 {
750 if handle.is_null() {
751 return 0;
752 }
753 let h = unsafe { &*handle };
754 let _op = match h.guard.try_enter() {
756 Some(op) => op,
757 None => return 0,
758 };
759 h.inner.node_id()
760}
761
762#[unsafe(no_mangle)]
766pub unsafe extern "C" fn net_mesh_entity_id(handle: *mut MeshNodeHandle, out: *mut u8) -> c_int {
767 if handle.is_null() || out.is_null() {
768 return NetError::NullPointer.into();
769 }
770 let h = unsafe { &*handle };
771 let _op = match h.guard.try_enter() {
772 Some(op) => op,
773 None => return NetError::ShuttingDown.into(),
774 };
775 let bytes = h.inner.entity_id().as_bytes();
776 unsafe {
777 std::ptr::copy_nonoverlapping(bytes.as_ptr(), out, 32);
778 }
779 0
780}
781
782#[unsafe(no_mangle)]
784pub unsafe extern "C" fn net_mesh_connect(
785 handle: *mut MeshNodeHandle,
786 peer_addr: *const c_char,
787 peer_pubkey_hex: *const c_char,
788 peer_node_id: u64,
789) -> c_int {
790 if handle.is_null() || peer_addr.is_null() || peer_pubkey_hex.is_null() {
791 return NetError::NullPointer.into();
792 }
793 let h = unsafe { &*handle };
794 let _op = match h.guard.try_enter() {
795 Some(op) => op,
796 None => return NetError::ShuttingDown.into(),
797 };
798 let Some(addr_s) = (unsafe { c_str_to_string(peer_addr) }) else {
799 return NetError::InvalidUtf8.into();
800 };
801 let addr: std::net::SocketAddr = match addr_s.parse() {
802 Ok(a) => a,
803 Err(_) => return NET_ERR_MESH_HANDSHAKE,
804 };
805 let Some(pk_s) = (unsafe { c_str_to_string(peer_pubkey_hex) }) else {
806 return NetError::InvalidUtf8.into();
807 };
808 let pk_bytes = match hex::decode(pk_s) {
809 Ok(b) => b,
810 Err(_) => return NET_ERR_MESH_HANDSHAKE,
811 };
812 if pk_bytes.len() != 32 {
813 return NET_ERR_MESH_HANDSHAKE;
814 }
815 let mut pk = [0u8; 32];
816 pk.copy_from_slice(&pk_bytes);
817
818 let node = h.inner.clone();
819 match block_on(async move { node.connect(addr, &pk, peer_node_id).await }) {
820 Ok(_) => 0,
821 Err(e) => adapter_err_to_code(&e),
822 }
823}
824
825#[unsafe(no_mangle)]
828pub unsafe extern "C" fn net_mesh_accept(
829 handle: *mut MeshNodeHandle,
830 peer_node_id: u64,
831 out_addr: *mut *mut c_char,
832 out_len: *mut usize,
833) -> c_int {
834 if handle.is_null() || out_addr.is_null() || out_len.is_null() {
835 return NetError::NullPointer.into();
836 }
837 let h = unsafe { &*handle };
838 let _op = match h.guard.try_enter() {
839 Some(op) => op,
840 None => return NetError::ShuttingDown.into(),
841 };
842 let node = h.inner.clone();
843 match block_on(async move { node.accept(peer_node_id).await }) {
844 Ok((addr, _)) => write_string_out(addr.to_string(), out_addr, out_len),
845 Err(e) => adapter_err_to_code(&e),
846 }
847}
848
849#[unsafe(no_mangle)]
850pub unsafe extern "C" fn net_mesh_start(handle: *mut MeshNodeHandle) -> c_int {
851 if handle.is_null() {
852 return NetError::NullPointer.into();
853 }
854 let h = unsafe { &*handle };
855 let _op = match h.guard.try_enter() {
856 Some(op) => op,
857 None => return NetError::ShuttingDown.into(),
858 };
859 let node = h.inner.clone();
860 block_on(async move { node.start_arc() });
864 0
865}
866
867#[unsafe(no_mangle)]
879pub unsafe extern "C" fn net_mesh_shutdown(handle: *mut MeshNodeHandle) -> c_int {
880 if handle.is_null() {
881 return NetError::NullPointer.into();
882 }
883 let h = unsafe { &*handle };
884 let _op = match h.guard.try_enter() {
885 Some(op) => op,
886 None => return NetError::ShuttingDown.into(),
887 };
888 match block_on(async { h.inner.shutdown().await }) {
889 Ok(()) => 0,
890 Err(e) => adapter_err_to_code(&e),
891 }
892}
893
894#[cfg(feature = "nat-traversal")]
916#[unsafe(no_mangle)]
917pub unsafe extern "C" fn net_mesh_nat_type(
918 handle: *mut MeshNodeHandle,
919 out_str: *mut *mut c_char,
920 out_len: *mut usize,
921) -> c_int {
922 if handle.is_null() || out_str.is_null() || out_len.is_null() {
923 return NetError::NullPointer.into();
924 }
925 let h = unsafe { &*handle };
926 let _op = match h.guard.try_enter() {
927 Some(op) => op,
928 None => return NetError::ShuttingDown.into(),
929 };
930 write_string_out(
931 nat_class_to_str(h.inner.nat_class()).to_string(),
932 out_str,
933 out_len,
934 )
935}
936
937#[cfg(feature = "nat-traversal")]
942#[unsafe(no_mangle)]
943pub unsafe extern "C" fn net_mesh_reflex_addr(
944 handle: *mut MeshNodeHandle,
945 out_str: *mut *mut c_char,
946 out_len: *mut usize,
947) -> c_int {
948 if handle.is_null() || out_str.is_null() || out_len.is_null() {
949 return NetError::NullPointer.into();
950 }
951 let h = unsafe { &*handle };
952 let _op = match h.guard.try_enter() {
953 Some(op) => op,
954 None => return NetError::ShuttingDown.into(),
955 };
956 let s = h
957 .inner
958 .reflex_addr()
959 .map(|a| a.to_string())
960 .unwrap_or_default();
961 write_string_out(s, out_str, out_len)
962}
963
964#[cfg(feature = "nat-traversal")]
968#[unsafe(no_mangle)]
969pub unsafe extern "C" fn net_mesh_peer_nat_type(
970 handle: *mut MeshNodeHandle,
971 peer_node_id: u64,
972 out_str: *mut *mut c_char,
973 out_len: *mut usize,
974) -> c_int {
975 if handle.is_null() || out_str.is_null() || out_len.is_null() {
976 return NetError::NullPointer.into();
977 }
978 let h = unsafe { &*handle };
979 let _op = match h.guard.try_enter() {
980 Some(op) => op,
981 None => return NetError::ShuttingDown.into(),
982 };
983 write_string_out(
984 nat_class_to_str(h.inner.peer_nat_class(peer_node_id)).to_string(),
985 out_str,
986 out_len,
987 )
988}
989
990#[cfg(feature = "nat-traversal")]
999#[unsafe(no_mangle)]
1000pub unsafe extern "C" fn net_mesh_probe_reflex(
1001 handle: *mut MeshNodeHandle,
1002 peer_node_id: u64,
1003 out_str: *mut *mut c_char,
1004 out_len: *mut usize,
1005) -> c_int {
1006 if handle.is_null() || out_str.is_null() || out_len.is_null() {
1007 return NetError::NullPointer.into();
1008 }
1009 let h = unsafe { &*handle };
1010 let _op = match h.guard.try_enter() {
1011 Some(op) => op,
1012 None => return NetError::ShuttingDown.into(),
1013 };
1014 let node = h.inner.clone();
1015 match block_on(async move { node.probe_reflex(peer_node_id).await }) {
1016 Ok(addr) => write_string_out(addr.to_string(), out_str, out_len),
1017 Err(e) => traversal_err_to_code(&e),
1018 }
1019}
1020
1021#[cfg(feature = "nat-traversal")]
1026#[unsafe(no_mangle)]
1027pub unsafe extern "C" fn net_mesh_reclassify_nat(handle: *mut MeshNodeHandle) -> c_int {
1028 if handle.is_null() {
1029 return NetError::NullPointer.into();
1030 }
1031 let h = unsafe { &*handle };
1032 let _op = match h.guard.try_enter() {
1033 Some(op) => op,
1034 None => return NetError::ShuttingDown.into(),
1035 };
1036 let node = h.inner.clone();
1037 block_on(async move { node.reclassify_nat().await });
1038 0
1039}
1040
1041#[cfg(feature = "nat-traversal")]
1046#[unsafe(no_mangle)]
1047pub unsafe extern "C" fn net_mesh_traversal_stats(
1048 handle: *mut MeshNodeHandle,
1049 out_punches_attempted: *mut u64,
1050 out_punches_succeeded: *mut u64,
1051 out_relay_fallbacks: *mut u64,
1052) -> c_int {
1053 if handle.is_null() {
1054 return NetError::NullPointer.into();
1055 }
1056 let h = unsafe { &*handle };
1057 let _op = match h.guard.try_enter() {
1058 Some(op) => op,
1059 None => return NetError::ShuttingDown.into(),
1060 };
1061 let snap = h.inner.traversal_stats();
1062 unsafe {
1063 if !out_punches_attempted.is_null() {
1064 *out_punches_attempted = snap.punches_attempted;
1065 }
1066 if !out_punches_succeeded.is_null() {
1067 *out_punches_succeeded = snap.punches_succeeded;
1068 }
1069 if !out_relay_fallbacks.is_null() {
1070 *out_relay_fallbacks = snap.relay_fallbacks;
1071 }
1072 }
1073 0
1074}
1075
1076#[cfg(feature = "nat-traversal")]
1088#[unsafe(no_mangle)]
1089pub unsafe extern "C" fn net_mesh_connect_direct(
1090 handle: *mut MeshNodeHandle,
1091 peer_node_id: u64,
1092 peer_pubkey_hex: *const c_char,
1093 coordinator: u64,
1094) -> c_int {
1095 if handle.is_null() || peer_pubkey_hex.is_null() {
1096 return NetError::NullPointer.into();
1097 }
1098 let h = unsafe { &*handle };
1099 let _op = match h.guard.try_enter() {
1100 Some(op) => op,
1101 None => return NetError::ShuttingDown.into(),
1102 };
1103 let Some(pk_s) = (unsafe { c_str_to_string(peer_pubkey_hex) }) else {
1104 return NetError::InvalidUtf8.into();
1105 };
1106 let pk_bytes = match hex::decode(pk_s) {
1107 Ok(b) => b,
1108 Err(_) => return NET_ERR_MESH_HANDSHAKE,
1109 };
1110 if pk_bytes.len() != 32 {
1111 return NET_ERR_MESH_HANDSHAKE;
1112 }
1113 let mut pk = [0u8; 32];
1114 pk.copy_from_slice(&pk_bytes);
1115
1116 let node = h.inner.clone();
1117 match block_on(async move { node.connect_direct(peer_node_id, &pk, coordinator).await }) {
1118 Ok(_) => 0,
1119 Err(e) => traversal_err_to_code(&e),
1120 }
1121}
1122
1123#[cfg(feature = "nat-traversal")]
1131#[unsafe(no_mangle)]
1132pub unsafe extern "C" fn net_mesh_set_reflex_override(
1133 handle: *mut MeshNodeHandle,
1134 external: *const c_char,
1135) -> c_int {
1136 if handle.is_null() || external.is_null() {
1137 return NetError::NullPointer.into();
1138 }
1139 let h = unsafe { &*handle };
1140 let _op = match h.guard.try_enter() {
1141 Some(op) => op,
1142 None => return NetError::ShuttingDown.into(),
1143 };
1144 let Some(s) = (unsafe { c_str_to_string(external) }) else {
1145 return NetError::InvalidUtf8.into();
1146 };
1147 let Ok(addr) = s.parse::<std::net::SocketAddr>() else {
1148 return NET_ERR_MESH_INIT;
1149 };
1150 h.inner.set_reflex_override(addr);
1151 0
1152}
1153
1154#[cfg(feature = "nat-traversal")]
1162#[unsafe(no_mangle)]
1163pub unsafe extern "C" fn net_mesh_clear_reflex_override(handle: *mut MeshNodeHandle) -> c_int {
1164 if handle.is_null() {
1165 return NetError::NullPointer.into();
1166 }
1167 let h = unsafe { &*handle };
1168 let _op = match h.guard.try_enter() {
1169 Some(op) => op,
1170 None => return NetError::ShuttingDown.into(),
1171 };
1172 h.inner.clear_reflex_override();
1173 0
1174}
1175
1176#[cfg(not(feature = "nat-traversal"))]
1199#[unsafe(no_mangle)]
1200pub unsafe extern "C" fn net_mesh_nat_type(
1201 _handle: *mut MeshNodeHandle,
1202 _out_str: *mut *mut c_char,
1203 _out_len: *mut usize,
1204) -> c_int {
1205 NET_ERR_TRAVERSAL_UNSUPPORTED
1206}
1207
1208#[cfg(not(feature = "nat-traversal"))]
1209#[unsafe(no_mangle)]
1210pub unsafe extern "C" fn net_mesh_reflex_addr(
1211 _handle: *mut MeshNodeHandle,
1212 _out_str: *mut *mut c_char,
1213 _out_len: *mut usize,
1214) -> c_int {
1215 NET_ERR_TRAVERSAL_UNSUPPORTED
1216}
1217
1218#[cfg(not(feature = "nat-traversal"))]
1219#[unsafe(no_mangle)]
1220pub unsafe extern "C" fn net_mesh_peer_nat_type(
1221 _handle: *mut MeshNodeHandle,
1222 _peer_node_id: u64,
1223 _out_str: *mut *mut c_char,
1224 _out_len: *mut usize,
1225) -> c_int {
1226 NET_ERR_TRAVERSAL_UNSUPPORTED
1227}
1228
1229#[cfg(not(feature = "nat-traversal"))]
1230#[unsafe(no_mangle)]
1231pub unsafe extern "C" fn net_mesh_probe_reflex(
1232 _handle: *mut MeshNodeHandle,
1233 _peer_node_id: u64,
1234 _out_str: *mut *mut c_char,
1235 _out_len: *mut usize,
1236) -> c_int {
1237 NET_ERR_TRAVERSAL_UNSUPPORTED
1238}
1239
1240#[cfg(not(feature = "nat-traversal"))]
1241#[unsafe(no_mangle)]
1242pub unsafe extern "C" fn net_mesh_reclassify_nat(_handle: *mut MeshNodeHandle) -> c_int {
1243 NET_ERR_TRAVERSAL_UNSUPPORTED
1244}
1245
1246#[cfg(not(feature = "nat-traversal"))]
1247#[unsafe(no_mangle)]
1248pub unsafe extern "C" fn net_mesh_traversal_stats(
1249 _handle: *mut MeshNodeHandle,
1250 _out_punches_attempted: *mut u64,
1251 _out_punches_succeeded: *mut u64,
1252 _out_relay_fallbacks: *mut u64,
1253) -> c_int {
1254 NET_ERR_TRAVERSAL_UNSUPPORTED
1255}
1256
1257#[cfg(not(feature = "nat-traversal"))]
1258#[unsafe(no_mangle)]
1259pub unsafe extern "C" fn net_mesh_connect_direct(
1260 _handle: *mut MeshNodeHandle,
1261 _peer_node_id: u64,
1262 _peer_pubkey_hex: *const c_char,
1263 _coordinator: u64,
1264) -> c_int {
1265 NET_ERR_TRAVERSAL_UNSUPPORTED
1266}
1267
1268#[cfg(not(feature = "nat-traversal"))]
1269#[unsafe(no_mangle)]
1270pub unsafe extern "C" fn net_mesh_set_reflex_override(
1271 _handle: *mut MeshNodeHandle,
1272 _external: *const c_char,
1273) -> c_int {
1274 NET_ERR_TRAVERSAL_UNSUPPORTED
1275}
1276
1277#[cfg(not(feature = "nat-traversal"))]
1278#[unsafe(no_mangle)]
1279pub unsafe extern "C" fn net_mesh_clear_reflex_override(_handle: *mut MeshNodeHandle) -> c_int {
1280 NET_ERR_TRAVERSAL_UNSUPPORTED
1281}
1282
1283#[derive(Deserialize, Default)]
1288struct StreamOpenConfig {
1289 reliability: Option<String>,
1291 window_bytes: Option<u32>,
1294 fairness_weight: Option<u8>,
1295}
1296
1297pub struct MeshStreamHandle {
1312 stream: ManuallyDrop<CoreStream>,
1313 _node: ManuallyDrop<Arc<MeshNode>>,
1316 guard: HandleGuard,
1317}
1318
1319#[unsafe(no_mangle)]
1320pub unsafe extern "C" fn net_mesh_open_stream(
1321 handle: *mut MeshNodeHandle,
1322 peer_node_id: u64,
1323 stream_id: u64,
1324 config_json: *const c_char,
1325 out_stream: *mut *mut MeshStreamHandle,
1326) -> c_int {
1327 if handle.is_null() || out_stream.is_null() {
1328 return NetError::NullPointer.into();
1329 }
1330 let h = unsafe { &*handle };
1331 let _op = match h.guard.try_enter() {
1332 Some(op) => op,
1333 None => return NetError::ShuttingDown.into(),
1334 };
1335 let cfg_json: StreamOpenConfig = if config_json.is_null() {
1336 StreamOpenConfig::default()
1337 } else {
1338 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1339 return NetError::InvalidUtf8.into();
1340 };
1341 match serde_json::from_str(&s) {
1342 Ok(v) => v,
1343 Err(_) => return NetError::InvalidJson.into(),
1344 }
1345 };
1346 let reliability = match cfg_json.reliability.as_deref() {
1347 None | Some("fire_and_forget") => Reliability::FireAndForget,
1348 Some("reliable") => Reliability::Reliable,
1349 Some(_) => return NET_ERR_MESH_TRANSPORT,
1350 };
1351 let window = cfg_json.window_bytes.unwrap_or(DEFAULT_STREAM_WINDOW_BYTES);
1352 let weight = cfg_json.fairness_weight.unwrap_or(1);
1353 let cfg = StreamConfig::new()
1354 .with_reliability(reliability)
1355 .with_window_bytes(window)
1356 .with_fairness_weight(weight);
1357 match h.inner.open_stream(peer_node_id, stream_id, cfg) {
1358 Ok(stream) => {
1359 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
1360 let sh = Box::new(MeshStreamHandle {
1361 stream: ManuallyDrop::new(stream),
1362 _node: ManuallyDrop::new(node_clone),
1363 guard: HandleGuard::new(),
1364 });
1365 unsafe {
1366 *out_stream = Box::into_raw(sh);
1367 }
1368 0
1369 }
1370 Err(e) => adapter_err_to_code(&e),
1371 }
1372}
1373
1374#[unsafe(no_mangle)]
1375pub unsafe extern "C" fn net_mesh_stream_free(handle: *mut MeshStreamHandle) {
1376 if handle.is_null() {
1377 return;
1378 }
1379 let h: &MeshStreamHandle = unsafe { &*handle };
1381 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
1382 unsafe {
1384 let _stream = ManuallyDrop::take(&mut (*handle).stream);
1388 let node = ManuallyDrop::take(&mut (*handle)._node);
1389 drop(node);
1390 }
1391 } else {
1392 tracing::warn!(
1393 "net_mesh_stream_free: in-flight ops did not drain within deadline; \
1394 leaking inner to avoid use-after-free"
1395 );
1396 }
1397}
1398
1399unsafe fn collect_payloads(
1409 payloads: *const *const u8,
1410 lens: *const usize,
1411 count: usize,
1412) -> Option<Vec<Bytes>> {
1413 let mut out = Vec::with_capacity(count);
1414 for i in 0..count {
1415 let ptr = *payloads.add(i);
1416 let len = *lens.add(i);
1417 if ptr.is_null() {
1418 if len == 0 {
1419 out.push(Bytes::new());
1420 continue;
1421 }
1422 return None;
1423 }
1424 if len > isize::MAX as usize {
1428 return None;
1429 }
1430 let slice = std::slice::from_raw_parts(ptr, len);
1431 out.push(Bytes::copy_from_slice(slice));
1432 }
1433 Some(out)
1434}
1435
1436#[inline]
1444fn handles_match(sh: &MeshStreamHandle, nh: &MeshNodeHandle) -> bool {
1445 Arc::ptr_eq(&sh._node, &nh.inner)
1446}
1447
1448#[unsafe(no_mangle)]
1449pub unsafe extern "C" fn net_mesh_send(
1450 handle: *mut MeshStreamHandle,
1451 payloads: *const *const u8,
1452 lens: *const usize,
1453 count: usize,
1454 node_handle: *mut MeshNodeHandle,
1455) -> c_int {
1456 if handle.is_null() || node_handle.is_null() {
1457 return NetError::NullPointer.into();
1458 }
1459 if count > 0 && (payloads.is_null() || lens.is_null()) {
1460 return NetError::NullPointer.into();
1461 }
1462 let sh = unsafe { &*handle };
1463 let nh = unsafe { &*node_handle };
1464 let _sh_op = match sh.guard.try_enter() {
1467 Some(op) => op,
1468 None => return NetError::ShuttingDown.into(),
1469 };
1470 let _nh_op = match nh.guard.try_enter() {
1471 Some(op) => op,
1472 None => return NetError::ShuttingDown.into(),
1473 };
1474 if !handles_match(sh, nh) {
1475 return NetError::MismatchedHandles.into();
1476 }
1477 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1478 Some(v) => v,
1479 None => return NetError::NullPointer.into(),
1480 };
1481 let node = nh.inner.clone();
1482 let stream = sh.stream.clone();
1483 match block_on(async move { node.send_on_stream(&stream, &payloads).await }) {
1484 Ok(()) => 0,
1485 Err(e) => stream_err_to_code(&e),
1486 }
1487}
1488
1489#[unsafe(no_mangle)]
1490pub unsafe extern "C" fn net_mesh_send_with_retry(
1491 handle: *mut MeshStreamHandle,
1492 payloads: *const *const u8,
1493 lens: *const usize,
1494 count: usize,
1495 max_retries: u32,
1496 node_handle: *mut MeshNodeHandle,
1497) -> c_int {
1498 if handle.is_null() || node_handle.is_null() {
1499 return NetError::NullPointer.into();
1500 }
1501 if count > 0 && (payloads.is_null() || lens.is_null()) {
1502 return NetError::NullPointer.into();
1503 }
1504 let sh = unsafe { &*handle };
1505 let nh = unsafe { &*node_handle };
1506 let _sh_op = match sh.guard.try_enter() {
1509 Some(op) => op,
1510 None => return NetError::ShuttingDown.into(),
1511 };
1512 let _nh_op = match nh.guard.try_enter() {
1513 Some(op) => op,
1514 None => return NetError::ShuttingDown.into(),
1515 };
1516 if !handles_match(sh, nh) {
1517 return NetError::MismatchedHandles.into();
1518 }
1519 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1520 Some(v) => v,
1521 None => return NetError::NullPointer.into(),
1522 };
1523 let node = nh.inner.clone();
1524 let stream = sh.stream.clone();
1525 match block_on(async move {
1526 node.send_with_retry(&stream, &payloads, max_retries as usize)
1527 .await
1528 }) {
1529 Ok(()) => 0,
1530 Err(e) => stream_err_to_code(&e),
1531 }
1532}
1533
1534#[unsafe(no_mangle)]
1535pub unsafe extern "C" fn net_mesh_send_blocking(
1536 handle: *mut MeshStreamHandle,
1537 payloads: *const *const u8,
1538 lens: *const usize,
1539 count: usize,
1540 node_handle: *mut MeshNodeHandle,
1541) -> c_int {
1542 if handle.is_null() || node_handle.is_null() {
1543 return NetError::NullPointer.into();
1544 }
1545 if count > 0 && (payloads.is_null() || lens.is_null()) {
1546 return NetError::NullPointer.into();
1547 }
1548 let sh = unsafe { &*handle };
1549 let nh = unsafe { &*node_handle };
1550 let _sh_op = match sh.guard.try_enter() {
1553 Some(op) => op,
1554 None => return NetError::ShuttingDown.into(),
1555 };
1556 let _nh_op = match nh.guard.try_enter() {
1557 Some(op) => op,
1558 None => return NetError::ShuttingDown.into(),
1559 };
1560 if !handles_match(sh, nh) {
1561 return NetError::MismatchedHandles.into();
1562 }
1563 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1564 Some(v) => v,
1565 None => return NetError::NullPointer.into(),
1566 };
1567 let node = nh.inner.clone();
1568 let stream = sh.stream.clone();
1569 match block_on(async move { node.send_blocking(&stream, &payloads).await }) {
1570 Ok(()) => 0,
1571 Err(e) => stream_err_to_code(&e),
1572 }
1573}
1574
1575#[derive(Serialize)]
1576struct StreamStatsJson {
1577 tx_seq: u64,
1578 rx_seq: u64,
1579 inbound_pending: u64,
1580 last_activity_ns: u64,
1581 active: bool,
1582 backpressure_events: u64,
1583 tx_credit_remaining: u32,
1584 tx_window: u32,
1585 credit_grants_received: u64,
1586 credit_grants_sent: u64,
1587}
1588
1589#[unsafe(no_mangle)]
1590pub unsafe extern "C" fn net_mesh_stream_stats(
1591 node_handle: *mut MeshNodeHandle,
1592 peer_node_id: u64,
1593 stream_id: u64,
1594 out_json: *mut *mut c_char,
1595 out_len: *mut usize,
1596) -> c_int {
1597 if node_handle.is_null() || out_json.is_null() || out_len.is_null() {
1598 return NetError::NullPointer.into();
1599 }
1600 let h = unsafe { &*node_handle };
1601 let _op = match h.guard.try_enter() {
1602 Some(op) => op,
1603 None => return NetError::ShuttingDown.into(),
1604 };
1605 match h.inner.stream_stats(peer_node_id, stream_id) {
1606 Some(s) => {
1607 let js = StreamStatsJson {
1608 tx_seq: s.tx_seq,
1609 rx_seq: s.rx_seq,
1610 inbound_pending: s.inbound_pending,
1611 last_activity_ns: s.last_activity_ns,
1612 active: s.active,
1613 backpressure_events: s.backpressure_events,
1614 tx_credit_remaining: s.tx_credit_remaining,
1615 tx_window: s.tx_window,
1616 credit_grants_received: s.credit_grants_received,
1617 credit_grants_sent: s.credit_grants_sent,
1618 };
1619 write_json_out(&js, out_json, out_len)
1620 }
1621 None => {
1622 write_string_out("null".to_string(), out_json, out_len)
1625 }
1626 }
1627}
1628
1629#[derive(Serialize)]
1634struct RecvEventJson {
1635 id: String,
1636 payload_b64: String,
1638 insertion_ts: u64,
1639 shard_id: u16,
1640}
1641
1642#[unsafe(no_mangle)]
1643pub unsafe extern "C" fn net_mesh_recv_shard(
1644 handle: *mut MeshNodeHandle,
1645 shard_id: u16,
1646 limit: u32,
1647 out_json: *mut *mut c_char,
1648 out_len: *mut usize,
1649) -> c_int {
1650 if handle.is_null() || out_json.is_null() || out_len.is_null() {
1651 return NetError::NullPointer.into();
1652 }
1653 let h = unsafe { &*handle };
1654 let _op = match h.guard.try_enter() {
1655 Some(op) => op,
1656 None => return NetError::ShuttingDown.into(),
1657 };
1658 let node = h.inner.clone();
1659 let result = block_on(async move { node.poll_shard(shard_id, None, limit as usize).await });
1660 let result = match result {
1661 Ok(r) => r,
1662 Err(e) => return adapter_err_to_code(&e),
1663 };
1664 let events: Vec<RecvEventJson> = result
1665 .events
1666 .into_iter()
1667 .map(|e| RecvEventJson {
1668 id: e.id,
1669 payload_b64: encode_b64(&e.raw),
1670 insertion_ts: e.insertion_ts,
1671 shard_id: e.shard_id,
1672 })
1673 .collect();
1674 write_json_out(&events, out_json, out_len)
1675}
1676
1677fn encode_b64(bytes: &[u8]) -> String {
1678 const ALPH: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1681 let mut s = String::with_capacity(bytes.len().div_ceil(3) * 4);
1682 let mut i = 0;
1683 while i + 3 <= bytes.len() {
1684 let chunk = &bytes[i..i + 3];
1685 s.push(ALPH[(chunk[0] >> 2) as usize] as char);
1686 s.push(ALPH[(((chunk[0] & 0b11) << 4) | (chunk[1] >> 4)) as usize] as char);
1687 s.push(ALPH[(((chunk[1] & 0b1111) << 2) | (chunk[2] >> 6)) as usize] as char);
1688 s.push(ALPH[(chunk[2] & 0b111111) as usize] as char);
1689 i += 3;
1690 }
1691 let rem = bytes.len() - i;
1692 if rem == 1 {
1693 let b = bytes[i];
1694 s.push(ALPH[(b >> 2) as usize] as char);
1695 s.push(ALPH[((b & 0b11) << 4) as usize] as char);
1696 s.push('=');
1697 s.push('=');
1698 } else if rem == 2 {
1699 let b0 = bytes[i];
1700 let b1 = bytes[i + 1];
1701 s.push(ALPH[(b0 >> 2) as usize] as char);
1702 s.push(ALPH[(((b0 & 0b11) << 4) | (b1 >> 4)) as usize] as char);
1703 s.push(ALPH[((b1 & 0b1111) << 2) as usize] as char);
1704 s.push('=');
1705 }
1706 s
1707}
1708
1709#[derive(Deserialize)]
1714struct ChannelConfigInput {
1715 name: String,
1716 visibility: Option<String>,
1717 reliable: Option<bool>,
1718 require_token: Option<bool>,
1719 token_roots: Option<Vec<String>>,
1725 priority: Option<u8>,
1726 max_rate_pps: Option<u32>,
1727 publish_caps: Option<CapabilityFilterJson>,
1731 subscribe_caps: Option<CapabilityFilterJson>,
1735}
1736
1737fn parse_visibility(s: &str) -> Option<InnerVisibility> {
1738 match s {
1739 "subnet-local" => Some(InnerVisibility::SubnetLocal),
1740 "parent-visible" => Some(InnerVisibility::ParentVisible),
1741 "exported" => Some(InnerVisibility::Exported),
1742 "global" => Some(InnerVisibility::Global),
1743 _ => None,
1744 }
1745}
1746
1747#[unsafe(no_mangle)]
1748pub unsafe extern "C" fn net_mesh_register_channel(
1749 handle: *mut MeshNodeHandle,
1750 config_json: *const c_char,
1751) -> c_int {
1752 if handle.is_null() || config_json.is_null() {
1753 return NetError::NullPointer.into();
1754 }
1755 let h = unsafe { &*handle };
1756 let _op = match h.guard.try_enter() {
1757 Some(op) => op,
1758 None => return NetError::ShuttingDown.into(),
1759 };
1760 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1761 return NetError::InvalidUtf8.into();
1762 };
1763 let input: ChannelConfigInput = match serde_json::from_str(&s) {
1764 Ok(v) => v,
1765 Err(_) => return NetError::InvalidJson.into(),
1766 };
1767 let name = match InnerChannelName::new(&input.name) {
1768 Ok(n) => n,
1769 Err(_) => return NET_ERR_CHANNEL,
1770 };
1771 let mut cfg = InnerChannelConfig::new(ChannelId::new(name));
1772 if let Some(v) = input.visibility {
1773 let Some(vis) = parse_visibility(&v) else {
1774 return NET_ERR_CHANNEL;
1775 };
1776 cfg = cfg.with_visibility(vis);
1777 }
1778 if let Some(r) = input.reliable {
1779 cfg = cfg.with_reliable(r);
1780 }
1781 if let Some(t) = input.require_token {
1782 cfg = cfg.with_require_token(t);
1783 }
1784 if let Some(roots) = input.token_roots {
1785 let mut parsed = Vec::with_capacity(roots.len());
1786 for hex_id in roots {
1787 let bytes = match hex::decode(&hex_id) {
1788 Ok(b) => b,
1789 Err(_) => return NET_ERR_CHANNEL,
1790 };
1791 let Ok(arr) = <[u8; 32]>::try_from(bytes.as_slice()) else {
1792 return NET_ERR_CHANNEL;
1793 };
1794 parsed.push(EntityId::from_bytes(arr));
1795 }
1796 cfg = cfg.with_token_roots(parsed);
1797 }
1798 if let Some(p) = input.priority {
1799 cfg = cfg.with_priority(p);
1800 }
1801 if let Some(pps) = input.max_rate_pps {
1802 cfg = cfg.with_rate_limit(pps);
1803 }
1804 if let Some(filter_json) = input.publish_caps {
1805 cfg = cfg.with_publish_caps(capability_filter_from_json(filter_json));
1806 }
1807 if let Some(filter_json) = input.subscribe_caps {
1808 cfg = cfg.with_subscribe_caps(capability_filter_from_json(filter_json));
1809 }
1810 h.channel_configs.insert(cfg);
1811 0
1812}
1813
1814#[unsafe(no_mangle)]
1815pub unsafe extern "C" fn net_mesh_subscribe_channel(
1816 handle: *mut MeshNodeHandle,
1817 publisher_node_id: u64,
1818 channel: *const c_char,
1819) -> c_int {
1820 subscribe_or_unsubscribe(handle, publisher_node_id, channel, true)
1821}
1822
1823#[unsafe(no_mangle)]
1824pub unsafe extern "C" fn net_mesh_unsubscribe_channel(
1825 handle: *mut MeshNodeHandle,
1826 publisher_node_id: u64,
1827 channel: *const c_char,
1828) -> c_int {
1829 subscribe_or_unsubscribe(handle, publisher_node_id, channel, false)
1830}
1831
1832#[unsafe(no_mangle)]
1839pub unsafe extern "C" fn net_mesh_subscribe_channel_with_token(
1840 handle: *mut MeshNodeHandle,
1841 publisher_node_id: u64,
1842 channel: *const c_char,
1843 token: *const u8,
1844 token_len: usize,
1845) -> c_int {
1846 if handle.is_null() || channel.is_null() || token.is_null() {
1847 return NetError::NullPointer.into();
1848 }
1849 let h = unsafe { &*handle };
1850 let _op = match h.guard.try_enter() {
1851 Some(op) => op,
1852 None => return NetError::ShuttingDown.into(),
1853 };
1854 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
1855 return NetError::InvalidUtf8.into();
1856 };
1857 let name = match InnerChannelName::new(&s) {
1858 Ok(n) => n,
1859 Err(_) => return NET_ERR_CHANNEL,
1860 };
1861 if token_len > isize::MAX as usize {
1863 return NetError::InvalidJson.into();
1864 }
1865 let slice = unsafe { std::slice::from_raw_parts(token, token_len) };
1866 let parsed = match PermissionToken::from_bytes(slice) {
1867 Ok(t) => t,
1868 Err(e) => return token_err_to_code(&e),
1869 };
1870 let node = h.inner.clone();
1871 match block_on(async move {
1872 node.subscribe_channel_with_token(publisher_node_id, name, parsed)
1873 .await
1874 }) {
1875 Ok(()) => 0,
1876 Err(e) => adapter_err_to_channel_code(&e),
1877 }
1878}
1879
1880fn subscribe_or_unsubscribe(
1881 handle: *mut MeshNodeHandle,
1882 publisher_node_id: u64,
1883 channel: *const c_char,
1884 subscribe: bool,
1885) -> c_int {
1886 if handle.is_null() || channel.is_null() {
1887 return NetError::NullPointer.into();
1888 }
1889 let h = unsafe { &*handle };
1890 let _op = match h.guard.try_enter() {
1891 Some(op) => op,
1892 None => return NetError::ShuttingDown.into(),
1893 };
1894 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
1895 return NetError::InvalidUtf8.into();
1896 };
1897 let name = match InnerChannelName::new(&s) {
1898 Ok(n) => n,
1899 Err(_) => return NET_ERR_CHANNEL,
1900 };
1901 let node = h.inner.clone();
1902 let outcome = if subscribe {
1903 block_on(async move { node.subscribe_channel(publisher_node_id, name).await })
1904 } else {
1905 block_on(async move { node.unsubscribe_channel(publisher_node_id, name).await })
1906 };
1907 match outcome {
1908 Ok(()) => 0,
1909 Err(e) => adapter_err_to_channel_code(&e),
1910 }
1911}
1912
1913fn adapter_err_to_channel_code(err: &AdapterError) -> c_int {
1914 if let AdapterError::Connection(msg) = err {
1915 let prefix = "membership request rejected: ";
1916 if let Some(tail) = msg.strip_prefix(prefix) {
1917 if tail.trim() == "Some(Unauthorized)" {
1918 return NET_ERR_CHANNEL_AUTH;
1919 }
1920 }
1921 }
1922 NET_ERR_CHANNEL
1923}
1924
1925#[derive(Deserialize, Default)]
1926struct PublishConfigInput {
1927 reliability: Option<String>,
1928 on_failure: Option<String>,
1929 max_inflight: Option<u32>,
1930}
1931
1932#[derive(Serialize)]
1933struct PublishReportJson {
1934 attempted: u32,
1935 delivered: u32,
1936 errors: Vec<PublishFailureJson>,
1937}
1938
1939#[derive(Serialize)]
1940struct PublishFailureJson {
1941 node_id: u64,
1942 message: String,
1943}
1944
1945fn to_publish_report_json(r: InnerPublishReport) -> PublishReportJson {
1946 PublishReportJson {
1947 attempted: r.attempted as u32,
1948 delivered: r.delivered as u32,
1949 errors: r
1950 .errors
1951 .into_iter()
1952 .map(|(id, e)| PublishFailureJson {
1953 node_id: id,
1954 message: format!("{}", e),
1955 })
1956 .collect(),
1957 }
1958}
1959
1960#[unsafe(no_mangle)]
1961pub unsafe extern "C" fn net_mesh_publish(
1962 handle: *mut MeshNodeHandle,
1963 channel: *const c_char,
1964 payload: *const u8,
1965 len: usize,
1966 config_json: *const c_char,
1967 out_json: *mut *mut c_char,
1968 out_len: *mut usize,
1969) -> c_int {
1970 if handle.is_null() || channel.is_null() || out_json.is_null() || out_len.is_null() {
1971 return NetError::NullPointer.into();
1972 }
1973 let h = unsafe { &*handle };
1974 let _op = match h.guard.try_enter() {
1975 Some(op) => op,
1976 None => return NetError::ShuttingDown.into(),
1977 };
1978 let Some(ch) = (unsafe { c_str_to_string(channel) }) else {
1979 return NetError::InvalidUtf8.into();
1980 };
1981 let name = match InnerChannelName::new(&ch) {
1982 Ok(n) => n,
1983 Err(_) => return NET_ERR_CHANNEL,
1984 };
1985 let cfg_in: PublishConfigInput = if config_json.is_null() {
1986 PublishConfigInput::default()
1987 } else {
1988 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1989 return NetError::InvalidUtf8.into();
1990 };
1991 match serde_json::from_str(&s) {
1992 Ok(v) => v,
1993 Err(_) => return NetError::InvalidJson.into(),
1994 }
1995 };
1996 let reliability = match cfg_in.reliability.as_deref() {
1997 None | Some("fire_and_forget") => Reliability::FireAndForget,
1998 Some("reliable") => Reliability::Reliable,
1999 Some(_) => return NET_ERR_CHANNEL,
2000 };
2001 let on_failure = match cfg_in.on_failure.as_deref() {
2002 None | Some("best_effort") => InnerOnFailure::BestEffort,
2003 Some("fail_fast") => InnerOnFailure::FailFast,
2004 Some("collect") => InnerOnFailure::Collect,
2005 Some(_) => return NET_ERR_CHANNEL,
2006 };
2007 let max_inflight = cfg_in.max_inflight.unwrap_or(32) as usize;
2008 let publish_cfg = InnerPublishConfig {
2009 reliability,
2010 on_failure,
2011 max_inflight,
2012 };
2013 let publisher = ChannelPublisher::new(name, publish_cfg);
2014
2015 let bytes = if len == 0 {
2017 Bytes::new()
2018 } else if payload.is_null() {
2019 return NetError::NullPointer.into();
2020 } else if len > isize::MAX as usize {
2021 return NetError::InvalidJson.into();
2023 } else {
2024 Bytes::copy_from_slice(unsafe { std::slice::from_raw_parts(payload, len) })
2025 };
2026
2027 let node = h.inner.clone();
2028 match block_on(async move { node.publish(&publisher, bytes).await }) {
2029 Ok(report) => {
2030 let js = to_publish_report_json(report);
2031 write_json_out(&js, out_json, out_len)
2032 }
2033 Err(e) => adapter_err_to_channel_code(&e),
2034 }
2035}
2036
2037pub struct IdentityHandle {
2051 keypair: ManuallyDrop<Arc<EntityKeypair>>,
2052 cache: ManuallyDrop<Arc<TokenCache>>,
2053 guard: HandleGuard,
2054}
2055
2056fn alloc_bytes(src: &[u8], out_ptr: *mut *mut u8, out_len: *mut usize) -> c_int {
2070 if out_ptr.is_null() || out_len.is_null() {
2071 return NetError::NullPointer.into();
2072 }
2073 let len = src.len();
2074 if len == 0 {
2075 unsafe {
2076 *out_ptr = std::ptr::null_mut();
2077 *out_len = 0;
2078 }
2079 return 0;
2080 }
2081 let layout = match std::alloc::Layout::array::<u8>(len) {
2090 Ok(l) => l,
2091 Err(_) => return NET_ERR_IDENTITY,
2097 };
2098 let ptr = unsafe { std::alloc::alloc(layout) };
2099 if ptr.is_null() {
2100 std::alloc::handle_alloc_error(layout);
2101 }
2102 unsafe {
2103 std::ptr::copy_nonoverlapping(src.as_ptr(), ptr, len);
2104 *out_ptr = ptr;
2105 *out_len = len;
2106 }
2107 0
2108}
2109
2110#[unsafe(no_mangle)]
2125pub unsafe extern "C" fn net_free_bytes(ptr: *mut u8, len: usize) {
2126 if ptr.is_null() || len == 0 {
2127 return;
2128 }
2129 let layout = match std::alloc::Layout::array::<u8>(len) {
2135 Ok(l) => l,
2136 Err(_) => return,
2137 };
2138 unsafe {
2139 std::alloc::dealloc(ptr, layout);
2140 }
2141}
2142
2143fn entity_id_from_bytes(bytes: *const u8, len: usize) -> Option<EntityId> {
2144 if bytes.is_null() || len != 32 {
2145 return None;
2146 }
2147 let slice = unsafe { std::slice::from_raw_parts(bytes, 32) };
2148 let mut arr = [0u8; 32];
2149 arr.copy_from_slice(slice);
2150 Some(EntityId::from_bytes(arr))
2151}
2152
2153fn parse_scope_list(raw: &str) -> Option<TokenScope> {
2154 let values: Vec<String> = serde_json::from_str(raw).ok()?;
2158 let mut acc = TokenScope::NONE;
2159 for s in &values {
2160 acc = acc.union(match s.as_str() {
2161 "publish" => TokenScope::PUBLISH,
2162 "subscribe" => TokenScope::SUBSCRIBE,
2163 "admin" => TokenScope::ADMIN,
2164 "delegate" => TokenScope::DELEGATE,
2165 _ => return None,
2166 });
2167 }
2168 Some(acc)
2169}
2170
2171fn scope_to_strings(scope: TokenScope) -> Vec<&'static str> {
2172 let mut out = Vec::new();
2173 if scope.contains(TokenScope::PUBLISH) {
2174 out.push("publish");
2175 }
2176 if scope.contains(TokenScope::SUBSCRIBE) {
2177 out.push("subscribe");
2178 }
2179 if scope.contains(TokenScope::ADMIN) {
2180 out.push("admin");
2181 }
2182 if scope.contains(TokenScope::DELEGATE) {
2183 out.push("delegate");
2184 }
2185 out
2186}
2187
2188fn channel_name_to_hash(channel: &str) -> Option<ChannelHash> {
2189 InnerChannelName::new(channel).ok().map(|n| n.hash())
2190}
2191
2192#[unsafe(no_mangle)]
2195pub unsafe extern "C" fn net_identity_generate(out_handle: *mut *mut IdentityHandle) -> c_int {
2196 if out_handle.is_null() {
2197 return NetError::NullPointer.into();
2198 }
2199 let handle = Box::new(IdentityHandle {
2200 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::generate())),
2201 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2202 guard: HandleGuard::new(),
2203 });
2204 unsafe {
2205 *out_handle = Box::into_raw(handle);
2206 }
2207 0
2208}
2209
2210#[unsafe(no_mangle)]
2214pub unsafe extern "C" fn net_identity_from_seed(
2215 seed: *const u8,
2216 seed_len: usize,
2217 out_handle: *mut *mut IdentityHandle,
2218) -> c_int {
2219 if seed.is_null() || out_handle.is_null() {
2220 return NetError::NullPointer.into();
2221 }
2222 if seed_len != 32 {
2223 return NET_ERR_IDENTITY;
2224 }
2225 let mut arr = [0u8; 32];
2226 arr.copy_from_slice(unsafe { std::slice::from_raw_parts(seed, 32) });
2227 let handle = Box::new(IdentityHandle {
2228 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::from_bytes(arr))),
2229 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2230 guard: HandleGuard::new(),
2231 });
2232 unsafe {
2233 *out_handle = Box::into_raw(handle);
2234 }
2235 0
2236}
2237
2238#[unsafe(no_mangle)]
2239pub unsafe extern "C" fn net_identity_free(handle: *mut IdentityHandle) {
2240 if handle.is_null() {
2241 return;
2242 }
2243 let h: &IdentityHandle = unsafe { &*handle };
2245 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
2246 unsafe {
2248 let mh = &mut *handle;
2249 let kp = ManuallyDrop::take(&mut mh.keypair);
2250 let cache = ManuallyDrop::take(&mut mh.cache);
2251 drop(kp);
2252 drop(cache);
2253 }
2254 } else {
2255 tracing::warn!(
2256 "net_identity_free: in-flight ops did not drain within deadline; \
2257 leaking inner to avoid use-after-free"
2258 );
2259 }
2260}
2261
2262#[unsafe(no_mangle)]
2265pub unsafe extern "C" fn net_identity_to_seed(handle: *mut IdentityHandle, out: *mut u8) -> c_int {
2266 if handle.is_null() || out.is_null() {
2267 return NetError::NullPointer.into();
2268 }
2269 let h = unsafe { &*handle };
2270 let _op = match h.guard.try_enter() {
2271 Some(op) => op,
2272 None => return NetError::ShuttingDown.into(),
2273 };
2274 let seed = h.keypair.secret_bytes();
2275 unsafe {
2276 std::ptr::copy_nonoverlapping(seed.as_ptr(), out, 32);
2277 }
2278 0
2279}
2280
2281#[unsafe(no_mangle)]
2283pub unsafe extern "C" fn net_identity_entity_id(
2284 handle: *mut IdentityHandle,
2285 out: *mut u8,
2286) -> c_int {
2287 if handle.is_null() || out.is_null() {
2288 return NetError::NullPointer.into();
2289 }
2290 let h = unsafe { &*handle };
2291 let _op = match h.guard.try_enter() {
2292 Some(op) => op,
2293 None => return NetError::ShuttingDown.into(),
2294 };
2295 let id = h.keypair.entity_id().as_bytes();
2296 unsafe {
2297 std::ptr::copy_nonoverlapping(id.as_ptr(), out, 32);
2298 }
2299 0
2300}
2301
2302#[unsafe(no_mangle)]
2303pub unsafe extern "C" fn net_identity_node_id(handle: *mut IdentityHandle) -> u64 {
2304 if handle.is_null() {
2305 return 0;
2306 }
2307 let h = unsafe { &*handle };
2308 let _op = match h.guard.try_enter() {
2310 Some(op) => op,
2311 None => return 0,
2312 };
2313 h.keypair.node_id()
2314}
2315
2316#[unsafe(no_mangle)]
2317pub unsafe extern "C" fn net_identity_origin_hash(handle: *mut IdentityHandle) -> u64 {
2318 if handle.is_null() {
2319 return 0;
2320 }
2321 let h = unsafe { &*handle };
2322 let _op = match h.guard.try_enter() {
2324 Some(op) => op,
2325 None => return 0,
2326 };
2327 h.keypair.origin_hash()
2328}
2329
2330#[unsafe(no_mangle)]
2333pub unsafe extern "C" fn net_identity_sign(
2334 handle: *mut IdentityHandle,
2335 msg: *const u8,
2336 len: usize,
2337 out_sig: *mut u8,
2338) -> c_int {
2339 if handle.is_null() || out_sig.is_null() {
2340 return NetError::NullPointer.into();
2341 }
2342 if len > 0 && msg.is_null() {
2343 return NetError::NullPointer.into();
2344 }
2345 let h = unsafe { &*handle };
2346 let _op = match h.guard.try_enter() {
2347 Some(op) => op,
2348 None => return NetError::ShuttingDown.into(),
2349 };
2350 let slice = if len == 0 {
2351 &[][..]
2352 } else if len > isize::MAX as usize {
2353 return NetError::InvalidJson.into();
2355 } else {
2356 unsafe { std::slice::from_raw_parts(msg, len) }
2357 };
2358 let sig = h.keypair.sign(slice).to_bytes();
2359 unsafe {
2360 std::ptr::copy_nonoverlapping(sig.as_ptr(), out_sig, 64);
2361 }
2362 0
2363}
2364
2365#[unsafe(no_mangle)]
2368pub unsafe extern "C" fn net_identity_issue_token(
2369 signer: *mut IdentityHandle,
2370 subject: *const u8,
2371 subject_len: usize,
2372 scope_json: *const c_char,
2373 channel: *const c_char,
2374 ttl_seconds: u32,
2375 delegation_depth: u8,
2376 out_token: *mut *mut u8,
2377 out_token_len: *mut usize,
2378) -> c_int {
2379 if signer.is_null() || out_token.is_null() || out_token_len.is_null() {
2380 return NetError::NullPointer.into();
2381 }
2382 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
2383 return NET_ERR_IDENTITY;
2384 };
2385 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
2386 return NetError::InvalidUtf8.into();
2387 };
2388 let Some(scope) = parse_scope_list(&scope_s) else {
2389 return NET_ERR_IDENTITY;
2390 };
2391 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
2392 return NetError::InvalidUtf8.into();
2393 };
2394 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
2395 return NET_ERR_IDENTITY;
2396 };
2397 let h = unsafe { &*signer };
2398 let _op = match h.guard.try_enter() {
2402 Some(op) => op,
2403 None => return NetError::ShuttingDown.into(),
2404 };
2405 let token = match PermissionToken::try_issue(
2411 &h.keypair,
2412 subject_id,
2413 scope,
2414 channel_hash,
2415 u64::from(ttl_seconds),
2416 delegation_depth,
2417 ) {
2418 Ok(t) => t,
2419 Err(e) => return token_err_to_code(&e),
2420 };
2421 alloc_bytes(&token.to_bytes(), out_token, out_token_len)
2422}
2423
2424#[unsafe(no_mangle)]
2428pub unsafe extern "C" fn net_identity_install_token(
2429 handle: *mut IdentityHandle,
2430 token: *const u8,
2431 len: usize,
2432) -> c_int {
2433 if handle.is_null() || token.is_null() {
2434 return NetError::NullPointer.into();
2435 }
2436 if len > isize::MAX as usize {
2438 return NetError::InvalidJson.into();
2439 }
2440 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2441 let parsed = match PermissionToken::from_bytes(slice) {
2442 Ok(t) => t,
2443 Err(e) => return token_err_to_code(&e),
2444 };
2445 let h = unsafe { &*handle };
2446 let _op = match h.guard.try_enter() {
2447 Some(op) => op,
2448 None => return NetError::ShuttingDown.into(),
2449 };
2450 match h.cache.insert(parsed) {
2451 Ok(()) => 0,
2452 Err(e) => token_err_to_code(&e),
2453 }
2454}
2455
2456#[unsafe(no_mangle)]
2460pub unsafe extern "C" fn net_identity_lookup_token(
2461 handle: *mut IdentityHandle,
2462 subject: *const u8,
2463 subject_len: usize,
2464 channel: *const c_char,
2465 out_token: *mut *mut u8,
2466 out_token_len: *mut usize,
2467) -> c_int {
2468 if handle.is_null() || out_token.is_null() || out_token_len.is_null() {
2469 return NetError::NullPointer.into();
2470 }
2471 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
2472 return NET_ERR_IDENTITY;
2473 };
2474 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
2475 return NetError::InvalidUtf8.into();
2476 };
2477 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
2478 return NET_ERR_IDENTITY;
2479 };
2480 let h = unsafe { &*handle };
2481 let _op = match h.guard.try_enter() {
2482 Some(op) => op,
2483 None => return NetError::ShuttingDown.into(),
2484 };
2485 match h.cache.get(&subject_id, channel_hash) {
2486 Some(token) => alloc_bytes(&token.to_bytes(), out_token, out_token_len),
2487 None => {
2488 unsafe {
2489 *out_token = std::ptr::null_mut();
2490 *out_token_len = 0;
2491 }
2492 0
2493 }
2494 }
2495}
2496
2497#[unsafe(no_mangle)]
2498pub unsafe extern "C" fn net_identity_token_cache_len(handle: *mut IdentityHandle) -> u32 {
2499 if handle.is_null() {
2500 return 0;
2501 }
2502 let h = unsafe { &*handle };
2503 let _op = match h.guard.try_enter() {
2505 Some(op) => op,
2506 None => return 0,
2507 };
2508 h.cache.len() as u32
2509}
2510
2511#[derive(Serialize)]
2516struct ParsedTokenJson {
2517 issuer_hex: String,
2518 subject_hex: String,
2519 scope: Vec<&'static str>,
2520 channel_hash: ChannelHash,
2521 not_before: u64,
2522 not_after: u64,
2523 delegation_depth: u8,
2524 nonce: u64,
2525 signature_hex: String,
2526}
2527
2528#[unsafe(no_mangle)]
2533pub unsafe extern "C" fn net_parse_token(
2534 token: *const u8,
2535 len: usize,
2536 out_json: *mut *mut c_char,
2537 out_len: *mut usize,
2538) -> c_int {
2539 if token.is_null() || out_json.is_null() || out_len.is_null() {
2540 return NetError::NullPointer.into();
2541 }
2542 if len > isize::MAX as usize {
2544 return NetError::InvalidJson.into();
2545 }
2546 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2547 let parsed = match PermissionToken::from_bytes(slice) {
2548 Ok(t) => t,
2549 Err(e) => return token_err_to_code(&e),
2550 };
2551 let out = ParsedTokenJson {
2552 issuer_hex: hex::encode(parsed.issuer.as_bytes()),
2553 subject_hex: hex::encode(parsed.subject.as_bytes()),
2554 scope: scope_to_strings(parsed.scope),
2555 channel_hash: parsed.channel_hash,
2556 not_before: parsed.not_before,
2557 not_after: parsed.not_after,
2558 delegation_depth: parsed.delegation_depth,
2559 nonce: parsed.nonce,
2560 signature_hex: hex::encode(parsed.signature),
2561 };
2562 write_json_out(&out, out_json, out_len)
2563}
2564
2565#[unsafe(no_mangle)]
2569pub unsafe extern "C" fn net_verify_token(
2570 token: *const u8,
2571 len: usize,
2572 out_ok: *mut c_int,
2573) -> c_int {
2574 if token.is_null() || out_ok.is_null() {
2575 return NetError::NullPointer.into();
2576 }
2577 if len > isize::MAX as usize {
2579 return NetError::InvalidJson.into();
2580 }
2581 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2582 let parsed = match PermissionToken::from_bytes(slice) {
2583 Ok(t) => t,
2584 Err(e) => return token_err_to_code(&e),
2585 };
2586 unsafe {
2587 *out_ok = if parsed.verify().is_ok() { 1 } else { 0 };
2588 }
2589 0
2590}
2591
2592#[unsafe(no_mangle)]
2597pub unsafe extern "C" fn net_token_is_expired(
2598 token: *const u8,
2599 len: usize,
2600 out_expired: *mut c_int,
2601) -> c_int {
2602 if token.is_null() || out_expired.is_null() {
2603 return NetError::NullPointer.into();
2604 }
2605 if len > isize::MAX as usize {
2607 return NetError::InvalidJson.into();
2608 }
2609 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2610 let parsed = match PermissionToken::from_bytes(slice) {
2611 Ok(t) => t,
2612 Err(e) => return token_err_to_code(&e),
2613 };
2614 unsafe {
2615 *out_expired = if parsed.is_expired() { 1 } else { 0 };
2616 }
2617 0
2618}
2619
2620#[unsafe(no_mangle)]
2623pub unsafe extern "C" fn net_delegate_token(
2624 signer: *mut IdentityHandle,
2625 parent: *const u8,
2626 parent_len: usize,
2627 new_subject: *const u8,
2628 new_subject_len: usize,
2629 restricted_scope_json: *const c_char,
2630 out_token: *mut *mut u8,
2631 out_token_len: *mut usize,
2632) -> c_int {
2633 if signer.is_null()
2634 || parent.is_null()
2635 || new_subject.is_null()
2636 || restricted_scope_json.is_null()
2637 || out_token.is_null()
2638 || out_token_len.is_null()
2639 {
2640 return NetError::NullPointer.into();
2641 }
2642 if parent_len > isize::MAX as usize {
2644 return NetError::InvalidJson.into();
2645 }
2646 let parent_slice = unsafe { std::slice::from_raw_parts(parent, parent_len) };
2647 let parent_tok = match PermissionToken::from_bytes(parent_slice) {
2648 Ok(t) => t,
2649 Err(e) => return token_err_to_code(&e),
2650 };
2651 let Some(subject_id) = entity_id_from_bytes(new_subject, new_subject_len) else {
2652 return NET_ERR_IDENTITY;
2653 };
2654 let Some(scope_s) = (unsafe { c_str_to_string(restricted_scope_json) }) else {
2655 return NetError::InvalidUtf8.into();
2656 };
2657 let Some(scope) = parse_scope_list(&scope_s) else {
2658 return NET_ERR_IDENTITY;
2659 };
2660 let h = unsafe { &*signer };
2661 let _op = match h.guard.try_enter() {
2665 Some(op) => op,
2666 None => return NetError::ShuttingDown.into(),
2667 };
2668 match parent_tok.delegate(&h.keypair, subject_id, scope) {
2669 Ok(child) => alloc_bytes(&child.to_bytes(), out_token, out_token_len),
2670 Err(e) => token_err_to_code(&e),
2671 }
2672}
2673
2674#[unsafe(no_mangle)]
2679pub unsafe extern "C" fn net_channel_hash(channel: *const c_char, out_hash: *mut u64) -> c_int {
2680 if channel.is_null() || out_hash.is_null() {
2681 return NetError::NullPointer.into();
2682 }
2683 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
2684 return NetError::InvalidUtf8.into();
2685 };
2686 let Some(hash) = channel_name_to_hash(&s) else {
2687 return NET_ERR_IDENTITY;
2688 };
2689 unsafe {
2690 *out_hash = hash;
2691 }
2692 0
2693}
2694
2695use crate::adapter::net::behavior::capability::{
2702 AcceleratorInfo, AcceleratorType, CapabilityFilter, CapabilitySet, GpuInfo, GpuVendor,
2703 HardwareCapabilities, Modality, ModelCapability, ResourceLimits, SoftwareCapabilities,
2704 ToolCapability, TAG_SCOPE_REGION_PREFIX, TAG_SCOPE_SUBNET_LOCAL, TAG_SCOPE_TENANT_PREFIX,
2705};
2706
2707fn parse_gpu_vendor_cap(s: &str) -> GpuVendor {
2710 match s.to_ascii_lowercase().as_str() {
2711 "nvidia" => GpuVendor::Nvidia,
2712 "amd" => GpuVendor::Amd,
2713 "intel" => GpuVendor::Intel,
2714 "apple" => GpuVendor::Apple,
2715 "qualcomm" => GpuVendor::Qualcomm,
2716 _ => GpuVendor::Unknown,
2717 }
2718}
2719
2720fn gpu_vendor_to_string_cap(v: GpuVendor) -> &'static str {
2721 match v {
2722 GpuVendor::Nvidia => "nvidia",
2723 GpuVendor::Amd => "amd",
2724 GpuVendor::Intel => "intel",
2725 GpuVendor::Apple => "apple",
2726 GpuVendor::Qualcomm => "qualcomm",
2727 GpuVendor::Unknown => "unknown",
2728 }
2729}
2730
2731fn parse_modality_cap(s: &str) -> Option<Modality> {
2732 match s.to_ascii_lowercase().as_str() {
2733 "text" => Some(Modality::Text),
2734 "image" => Some(Modality::Image),
2735 "audio" => Some(Modality::Audio),
2736 "video" => Some(Modality::Video),
2737 "code" => Some(Modality::Code),
2738 "embedding" => Some(Modality::Embedding),
2739 "tool-use" | "tool_use" | "tooluse" => Some(Modality::ToolUse),
2740 _ => None,
2749 }
2750}
2751
2752fn parse_accelerator_type_cap(s: &str) -> AcceleratorType {
2753 match s.to_ascii_lowercase().as_str() {
2754 "tpu" => AcceleratorType::Tpu,
2755 "npu" => AcceleratorType::Npu,
2756 "fpga" => AcceleratorType::Fpga,
2757 "asic" => AcceleratorType::Asic,
2758 "dsp" => AcceleratorType::Dsp,
2759 _ => AcceleratorType::Unknown,
2760 }
2761}
2762
2763#[derive(Deserialize, Default)]
2766struct CapabilitySetJson {
2767 #[serde(default)]
2768 hardware: Option<HardwareJson>,
2769 #[serde(default)]
2770 software: Option<SoftwareJson>,
2771 #[serde(default)]
2772 models: Vec<ModelJson>,
2773 #[serde(default)]
2774 tools: Vec<ToolJson>,
2775 #[serde(default)]
2776 tags: Vec<String>,
2777 #[serde(default)]
2778 limits: Option<LimitsJson>,
2779}
2780
2781#[derive(Deserialize, Default)]
2782struct HardwareJson {
2783 cpu_cores: Option<u32>,
2784 cpu_threads: Option<u32>,
2785 memory_gb: Option<u32>,
2786 gpu: Option<GpuJson>,
2787 #[serde(default)]
2788 additional_gpus: Vec<GpuJson>,
2789 storage_gb: Option<u64>,
2790 network_gbps: Option<u32>,
2791 #[serde(default)]
2792 accelerators: Vec<AcceleratorJson>,
2793}
2794
2795#[derive(Deserialize)]
2796struct GpuJson {
2797 vendor: Option<String>,
2798 #[serde(default)]
2799 model: String,
2800 #[serde(default)]
2801 vram_gb: u32,
2802 compute_units: Option<u32>,
2803 tensor_cores: Option<u32>,
2804 fp16_tflops_x10: Option<u32>,
2805}
2806
2807#[derive(Deserialize)]
2808struct AcceleratorJson {
2809 #[serde(default)]
2810 kind: String,
2811 #[serde(default)]
2812 model: String,
2813 memory_gb: Option<u32>,
2814 tops_x10: Option<u32>,
2815}
2816
2817#[derive(Deserialize, Default)]
2818struct SoftwareJson {
2819 os: Option<String>,
2820 os_version: Option<String>,
2821 #[serde(default)]
2822 runtimes: Vec<Vec<String>>,
2823 #[serde(default)]
2824 frameworks: Vec<Vec<String>>,
2825 cuda_version: Option<String>,
2826 #[serde(default)]
2827 drivers: Vec<Vec<String>>,
2828}
2829
2830#[derive(Deserialize)]
2831struct ModelJson {
2832 #[serde(default)]
2833 model_id: String,
2834 #[serde(default)]
2835 family: String,
2836 parameters_b_x10: Option<u32>,
2837 context_length: Option<u32>,
2838 quantization: Option<String>,
2839 #[serde(default)]
2840 modalities: Vec<String>,
2841 tokens_per_sec: Option<u32>,
2842 loaded: Option<bool>,
2843}
2844
2845#[derive(Deserialize)]
2846struct ToolJson {
2847 #[serde(default)]
2848 tool_id: String,
2849 #[serde(default)]
2850 name: String,
2851 version: Option<String>,
2852 input_schema: Option<String>,
2853 output_schema: Option<String>,
2854 #[serde(default)]
2855 requires: Vec<String>,
2856 estimated_time_ms: Option<u32>,
2857 stateless: Option<bool>,
2858}
2859
2860#[derive(Deserialize, Default)]
2861struct LimitsJson {
2862 max_concurrent_requests: Option<u32>,
2863 max_tokens_per_request: Option<u32>,
2864 rate_limit_rpm: Option<u32>,
2865 max_batch_size: Option<u32>,
2866 max_input_bytes: Option<u32>,
2867 max_output_bytes: Option<u32>,
2868}
2869
2870#[derive(Deserialize, Default)]
2871struct CapabilityFilterJson {
2872 #[serde(default)]
2873 require_tags: Vec<String>,
2874 #[serde(default)]
2875 require_models: Vec<String>,
2876 #[serde(default)]
2877 require_tools: Vec<String>,
2878 min_memory_gb: Option<u32>,
2879 require_gpu: Option<bool>,
2880 gpu_vendor: Option<String>,
2881 min_vram_gb: Option<u32>,
2882 min_context_length: Option<u32>,
2883 #[serde(default)]
2884 require_modalities: Vec<String>,
2885}
2886
2887fn pair_vec(xs: Vec<Vec<String>>) -> Vec<(String, String)> {
2890 xs.into_iter()
2891 .filter_map(|mut p| {
2892 if p.len() >= 2 {
2893 Some((std::mem::take(&mut p[0]), std::mem::take(&mut p[1])))
2894 } else {
2895 None
2896 }
2897 })
2898 .collect()
2899}
2900
2901#[inline]
2907fn saturating_u16_cap(v: u32) -> u16 {
2908 v.min(u16::MAX as u32) as u16
2909}
2910
2911fn gpu_info_from_json(g: GpuJson) -> GpuInfo {
2912 let vendor = g
2913 .vendor
2914 .as_deref()
2915 .map(parse_gpu_vendor_cap)
2916 .unwrap_or(GpuVendor::Unknown);
2917 let mut info = GpuInfo::new(vendor, g.model, g.vram_gb);
2918 if let Some(cu) = g.compute_units {
2919 info = info.with_compute_units(saturating_u16_cap(cu));
2920 }
2921 if let Some(tc) = g.tensor_cores {
2922 info = info.with_tensor_cores(saturating_u16_cap(tc));
2923 }
2924 if let Some(tf) = g.fp16_tflops_x10 {
2925 let tf_capped = saturating_u16_cap(tf);
2939 info = info.with_fp16_tflops(tf_capped as f32 / 10.0);
2940 }
2941 info
2942}
2943
2944fn accelerator_from_json(a: AcceleratorJson) -> AcceleratorInfo {
2945 AcceleratorInfo {
2946 accel_type: parse_accelerator_type_cap(&a.kind),
2947 model: a.model,
2948 memory_gb: a.memory_gb.unwrap_or(0),
2949 tops_x10: a.tops_x10.map(saturating_u16_cap).unwrap_or(0),
2950 }
2951}
2952
2953fn hardware_from_json(h: HardwareJson) -> HardwareCapabilities {
2954 let mut hw = HardwareCapabilities::new();
2955 match (h.cpu_cores, h.cpu_threads) {
2956 (Some(c), Some(t)) => hw = hw.with_cpu(saturating_u16_cap(c), saturating_u16_cap(t)),
2957 (Some(c), None) => {
2958 let c16 = saturating_u16_cap(c);
2959 hw = hw.with_cpu(c16, c16);
2960 }
2961 _ => {}
2962 }
2963 if let Some(mb) = h.memory_gb {
2964 hw = hw.with_memory(mb);
2965 }
2966 if let Some(g) = h.gpu {
2967 hw = hw.with_gpu(gpu_info_from_json(g));
2968 }
2969 for g in h.additional_gpus {
2970 hw = hw.add_gpu(gpu_info_from_json(g));
2971 }
2972 if let Some(mb) = h.storage_gb {
2973 hw = hw.with_storage(mb);
2974 }
2975 if let Some(gbps) = h.network_gbps {
2976 hw = hw.with_network(gbps);
2977 }
2978 for a in h.accelerators {
2979 hw = hw.add_accelerator(accelerator_from_json(a));
2980 }
2981 hw
2982}
2983
2984fn software_from_json(s: SoftwareJson) -> SoftwareCapabilities {
2985 let mut sw = SoftwareCapabilities::new()
2986 .with_os(s.os.unwrap_or_default(), s.os_version.unwrap_or_default());
2987 for (k, v) in pair_vec(s.runtimes) {
2988 sw = sw.add_runtime(k, v);
2989 }
2990 for (k, v) in pair_vec(s.frameworks) {
2991 sw = sw.add_framework(k, v);
2992 }
2993 if let Some(c) = s.cuda_version {
2994 sw = sw.with_cuda(c);
2995 }
2996 sw.drivers = pair_vec(s.drivers);
2997 sw
2998}
2999
3000fn model_from_json(m: ModelJson) -> ModelCapability {
3001 let mut mc = ModelCapability::new(m.model_id, m.family);
3002 if let Some(p) = m.parameters_b_x10 {
3003 mc.parameters_b_x10 = p;
3004 }
3005 if let Some(c) = m.context_length {
3006 mc = mc.with_context_length(c);
3007 }
3008 if let Some(q) = m.quantization {
3009 mc = mc.with_quantization(q);
3010 }
3011 for modality in m.modalities {
3012 match parse_modality_cap(&modality) {
3013 Some(parsed) => mc = mc.add_modality(parsed),
3014 None => {
3015 tracing::warn!(
3016 modality = %modality,
3017 "announce_capabilities: unknown modality string (typo?), \
3018 skipping rather than the pre-fix silent fallback to Text — \
3019 advertising a Text capability the node doesn't actually \
3020 have produced wrong scheduling decisions on the receiver",
3021 );
3022 }
3023 }
3024 }
3025 if let Some(t) = m.tokens_per_sec {
3026 mc = mc.with_tokens_per_sec(t);
3027 }
3028 if let Some(l) = m.loaded {
3029 mc = mc.with_loaded(l);
3030 }
3031 mc
3032}
3033
3034fn tool_from_json(t: ToolJson) -> ToolCapability {
3035 let mut tc = ToolCapability::new(t.tool_id, t.name);
3036 if let Some(v) = t.version {
3037 tc = tc.with_version(v);
3038 }
3039 if let Some(s) = t.input_schema {
3040 tc = tc.with_input_schema(s);
3041 }
3042 if let Some(s) = t.output_schema {
3043 tc = tc.with_output_schema(s);
3044 }
3045 for r in t.requires {
3046 tc = tc.requires(r);
3047 }
3048 if let Some(ms) = t.estimated_time_ms {
3049 tc = tc.with_estimated_time(ms);
3050 }
3051 if let Some(st) = t.stateless {
3052 tc = tc.with_stateless(st);
3053 }
3054 tc
3055}
3056
3057fn limits_from_json(l: LimitsJson) -> ResourceLimits {
3058 let mut rl = ResourceLimits::new();
3059 if let Some(n) = l.max_concurrent_requests {
3060 rl = rl.with_max_concurrent(n);
3061 }
3062 if let Some(n) = l.max_tokens_per_request {
3063 rl = rl.with_max_tokens(n);
3064 }
3065 if let Some(n) = l.rate_limit_rpm {
3066 rl = rl.with_rate_limit(n);
3067 }
3068 if let Some(n) = l.max_batch_size {
3069 rl = rl.with_max_batch(n);
3070 }
3071 if let Some(n) = l.max_input_bytes {
3072 rl.max_input_bytes = n;
3073 }
3074 if let Some(n) = l.max_output_bytes {
3075 rl.max_output_bytes = n;
3076 }
3077 rl
3078}
3079
3080fn capability_set_from_json(caps: CapabilitySetJson) -> CapabilitySet {
3081 let mut cs = CapabilitySet::new();
3082 if let Some(h) = caps.hardware {
3083 cs = cs.with_hardware(hardware_from_json(h));
3084 }
3085 if let Some(s) = caps.software {
3086 cs = cs.with_software(software_from_json(s));
3087 }
3088 for m in caps.models {
3089 cs = cs.add_model(model_from_json(m));
3090 }
3091 for t in caps.tools {
3092 cs = cs.add_tool(tool_from_json(t));
3093 }
3094 for tag in caps.tags {
3102 if tag == TAG_SCOPE_SUBNET_LOCAL {
3103 cs = cs.with_subnet_local_scope();
3104 } else if let Some(id) = tag.strip_prefix(TAG_SCOPE_TENANT_PREFIX) {
3105 cs = cs.with_tenant_scope(id);
3106 } else if let Some(name) = tag.strip_prefix(TAG_SCOPE_REGION_PREFIX) {
3107 cs = cs.with_region_scope(name);
3108 } else {
3109 cs = cs.add_tag(tag);
3110 }
3111 }
3112 if let Some(l) = caps.limits {
3113 cs = cs.with_limits(limits_from_json(l));
3114 }
3115 cs
3116}
3117
3118fn capability_filter_from_json(f: CapabilityFilterJson) -> CapabilityFilter {
3119 let mut cf = CapabilityFilter::new();
3120 for t in f.require_tags {
3121 cf = cf.require_tag(t);
3122 }
3123 for m in f.require_models {
3124 cf = cf.require_model(m);
3125 }
3126 for t in f.require_tools {
3127 cf = cf.require_tool(t);
3128 }
3129 if let Some(mb) = f.min_memory_gb {
3130 cf = cf.with_min_memory(mb);
3131 }
3132 if f.require_gpu.unwrap_or(false) {
3133 cf = cf.require_gpu();
3134 }
3135 if let Some(v) = f.gpu_vendor {
3136 cf = cf.with_gpu_vendor(parse_gpu_vendor_cap(&v));
3137 }
3138 if let Some(mb) = f.min_vram_gb {
3139 cf = cf.with_min_vram(mb);
3140 }
3141 if let Some(n) = f.min_context_length {
3142 cf = cf.with_min_context(n);
3143 }
3144 for m in f.require_modalities {
3145 match parse_modality_cap(&m) {
3146 Some(parsed) => cf = cf.require_modality(parsed),
3147 None => {
3148 tracing::warn!(
3161 modality = %m,
3162 "find_nodes: unknown modality string in require_modalities \
3163 filter (typo?), dropping the constraint; the resulting \
3164 filter is too permissive — pre-fix it was silently \
3165 re-interpreted as `require Text`, which returned the \
3166 wrong nodes",
3167 );
3168 }
3169 }
3170 }
3171 cf
3172}
3173
3174pub(crate) const NET_ERR_CAPABILITY: c_int = -128;
3177
3178#[unsafe(no_mangle)]
3185pub unsafe extern "C" fn net_mesh_announce_capabilities(
3186 handle: *mut MeshNodeHandle,
3187 caps_json: *const c_char,
3188) -> c_int {
3189 if handle.is_null() || caps_json.is_null() {
3190 return NetError::NullPointer.into();
3191 }
3192 let h = unsafe { &*handle };
3193 let _op = match h.guard.try_enter() {
3194 Some(op) => op,
3195 None => return NetError::ShuttingDown.into(),
3196 };
3197 let Some(s) = (unsafe { c_str_to_string(caps_json) }) else {
3198 return NetError::InvalidUtf8.into();
3199 };
3200 let parsed: CapabilitySetJson = match serde_json::from_str(&s) {
3201 Ok(v) => v,
3202 Err(_) => return NetError::InvalidJson.into(),
3203 };
3204 let caps = capability_set_from_json(parsed);
3205 let node = h.inner.clone();
3206 match block_on(async move { node.announce_capabilities(caps).await }) {
3207 Ok(()) => 0,
3208 Err(_) => NET_ERR_CAPABILITY,
3209 }
3210}
3211
3212#[unsafe(no_mangle)]
3215pub unsafe extern "C" fn net_mesh_find_nodes(
3216 handle: *mut MeshNodeHandle,
3217 filter_json: *const c_char,
3218 out_json: *mut *mut c_char,
3219 out_len: *mut usize,
3220) -> c_int {
3221 if handle.is_null() || filter_json.is_null() || out_json.is_null() || out_len.is_null() {
3222 return NetError::NullPointer.into();
3223 }
3224 let h = unsafe { &*handle };
3225 let _op = match h.guard.try_enter() {
3226 Some(op) => op,
3227 None => return NetError::ShuttingDown.into(),
3228 };
3229 let Some(s) = (unsafe { c_str_to_string(filter_json) }) else {
3230 return NetError::InvalidUtf8.into();
3231 };
3232 let parsed: CapabilityFilterJson = match serde_json::from_str(&s) {
3233 Ok(v) => v,
3234 Err(_) => return NetError::InvalidJson.into(),
3235 };
3236 let filter = capability_filter_from_json(parsed);
3237 let ids = h.inner.find_nodes_by_filter(&filter);
3238 write_json_out(&ids, out_json, out_len)
3239}
3240
3241#[derive(serde::Deserialize)]
3258struct ScopeFilterJson {
3259 kind: String,
3260 #[serde(default)]
3261 tenant: Option<String>,
3262 #[serde(default)]
3263 tenants: Option<Vec<String>>,
3264 #[serde(default)]
3265 region: Option<String>,
3266 #[serde(default)]
3267 regions: Option<Vec<String>>,
3268}
3269
3270enum ScopeFilterOwned {
3276 Any,
3277 GlobalOnly,
3278 SameSubnet,
3279 Tenant(String),
3280 Tenants(Vec<String>),
3281 Region(String),
3282 Regions(Vec<String>),
3283}
3284
3285fn scope_filter_from_json(f: ScopeFilterJson) -> ScopeFilterOwned {
3286 match f.kind.as_str() {
3287 "any" => ScopeFilterOwned::Any,
3288 "global_only" | "globalOnly" => ScopeFilterOwned::GlobalOnly,
3289 "same_subnet" | "sameSubnet" => ScopeFilterOwned::SameSubnet,
3290 "tenant" => match f.tenant {
3291 Some(t) if !t.is_empty() => ScopeFilterOwned::Tenant(t),
3292 _ => ScopeFilterOwned::Any,
3293 },
3294 "tenants" => match f.tenants {
3295 Some(ts) => {
3301 let cleaned: Vec<String> = ts.into_iter().filter(|t| !t.is_empty()).collect();
3302 if cleaned.is_empty() {
3303 ScopeFilterOwned::Any
3304 } else {
3305 ScopeFilterOwned::Tenants(cleaned)
3306 }
3307 }
3308 None => ScopeFilterOwned::Any,
3309 },
3310 "region" => match f.region {
3311 Some(r) if !r.is_empty() => ScopeFilterOwned::Region(r),
3312 _ => ScopeFilterOwned::Any,
3313 },
3314 "regions" => match f.regions {
3315 Some(rs) => {
3317 let cleaned: Vec<String> = rs.into_iter().filter(|r| !r.is_empty()).collect();
3318 if cleaned.is_empty() {
3319 ScopeFilterOwned::Any
3320 } else {
3321 ScopeFilterOwned::Regions(cleaned)
3322 }
3323 }
3324 None => ScopeFilterOwned::Any,
3325 },
3326 _ => ScopeFilterOwned::Any,
3327 }
3328}
3329
3330fn with_scope_filter<R>(
3335 owned: &ScopeFilterOwned,
3336 f: impl FnOnce(&crate::adapter::net::behavior::capability::ScopeFilter<'_>) -> R,
3337) -> R {
3338 use crate::adapter::net::behavior::capability::ScopeFilter as F;
3339 match owned {
3340 ScopeFilterOwned::Any => f(&F::Any),
3341 ScopeFilterOwned::GlobalOnly => f(&F::GlobalOnly),
3342 ScopeFilterOwned::SameSubnet => f(&F::SameSubnet),
3343 ScopeFilterOwned::Tenant(t) => f(&F::Tenant(t.as_str())),
3344 ScopeFilterOwned::Tenants(ts) => {
3345 let refs: Vec<&str> = ts.iter().map(|s| s.as_str()).collect();
3346 f(&F::Tenants(refs.as_slice()))
3347 }
3348 ScopeFilterOwned::Region(r) => f(&F::Region(r.as_str())),
3349 ScopeFilterOwned::Regions(rs) => {
3350 let refs: Vec<&str> = rs.iter().map(|s| s.as_str()).collect();
3351 f(&F::Regions(refs.as_slice()))
3352 }
3353 }
3354}
3355
3356#[unsafe(no_mangle)]
3379pub unsafe extern "C" fn net_mesh_find_nodes_scoped(
3380 handle: *mut MeshNodeHandle,
3381 filter_json: *const c_char,
3382 scope_json: *const c_char,
3383 out_json: *mut *mut c_char,
3384 out_len: *mut usize,
3385) -> c_int {
3386 if handle.is_null()
3387 || filter_json.is_null()
3388 || scope_json.is_null()
3389 || out_json.is_null()
3390 || out_len.is_null()
3391 {
3392 return NetError::NullPointer.into();
3393 }
3394 let h = unsafe { &*handle };
3395 let _op = match h.guard.try_enter() {
3396 Some(op) => op,
3397 None => return NetError::ShuttingDown.into(),
3398 };
3399 let Some(filter_s) = (unsafe { c_str_to_string(filter_json) }) else {
3400 return NetError::InvalidUtf8.into();
3401 };
3402 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
3403 return NetError::InvalidUtf8.into();
3404 };
3405 let parsed_filter: CapabilityFilterJson = match serde_json::from_str(&filter_s) {
3406 Ok(v) => v,
3407 Err(_) => return NetError::InvalidJson.into(),
3408 };
3409 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
3410 Ok(v) => v,
3411 Err(_) => return NetError::InvalidJson.into(),
3412 };
3413 let filter = capability_filter_from_json(parsed_filter);
3414 let owned = scope_filter_from_json(parsed_scope);
3415 let ids = with_scope_filter(&owned, |sf| {
3416 h.inner.find_nodes_by_filter_scoped(&filter, sf)
3417 });
3418 write_json_out(&ids, out_json, out_len)
3419}
3420
3421#[derive(serde::Deserialize)]
3435struct CapabilityRequirementJson {
3436 #[serde(default)]
3437 filter: CapabilityFilterJson,
3438 #[serde(default)]
3439 prefer_more_memory: f32,
3440 #[serde(default)]
3441 prefer_more_vram: f32,
3442 #[serde(default)]
3443 prefer_faster_inference: f32,
3444 #[serde(default)]
3445 prefer_loaded_models: f32,
3446}
3447
3448fn capability_requirement_from_json(
3449 j: CapabilityRequirementJson,
3450) -> crate::adapter::net::behavior::capability::CapabilityRequirement {
3451 crate::adapter::net::behavior::capability::CapabilityRequirement::from_filter(
3452 capability_filter_from_json(j.filter),
3453 )
3454 .prefer_memory(j.prefer_more_memory)
3455 .prefer_vram(j.prefer_more_vram)
3456 .prefer_speed(j.prefer_faster_inference)
3457 .prefer_loaded(j.prefer_loaded_models)
3458}
3459
3460#[unsafe(no_mangle)]
3470pub unsafe extern "C" fn net_mesh_find_best_node(
3471 handle: *mut MeshNodeHandle,
3472 requirement_json: *const c_char,
3473 out_node_id: *mut u64,
3474 out_has_match: *mut c_int,
3475) -> c_int {
3476 if handle.is_null()
3477 || requirement_json.is_null()
3478 || out_node_id.is_null()
3479 || out_has_match.is_null()
3480 {
3481 return NetError::NullPointer.into();
3482 }
3483 let h = unsafe { &*handle };
3484 let _op = match h.guard.try_enter() {
3485 Some(op) => op,
3486 None => return NetError::ShuttingDown.into(),
3487 };
3488 let Some(s) = (unsafe { c_str_to_string(requirement_json) }) else {
3489 return NetError::InvalidUtf8.into();
3490 };
3491 let parsed: CapabilityRequirementJson = match serde_json::from_str(&s) {
3492 Ok(v) => v,
3493 Err(_) => return NetError::InvalidJson.into(),
3494 };
3495 let req = capability_requirement_from_json(parsed);
3496 match h.inner.find_best_node(&req) {
3497 Some(node_id) => unsafe {
3498 *out_node_id = node_id;
3499 *out_has_match = 1;
3500 },
3501 None => unsafe {
3502 *out_has_match = 0;
3503 },
3504 }
3505 0
3506}
3507
3508#[unsafe(no_mangle)]
3517pub unsafe extern "C" fn net_mesh_find_best_node_scoped(
3518 handle: *mut MeshNodeHandle,
3519 requirement_json: *const c_char,
3520 scope_json: *const c_char,
3521 out_node_id: *mut u64,
3522 out_has_match: *mut c_int,
3523) -> c_int {
3524 if handle.is_null()
3525 || requirement_json.is_null()
3526 || scope_json.is_null()
3527 || out_node_id.is_null()
3528 || out_has_match.is_null()
3529 {
3530 return NetError::NullPointer.into();
3531 }
3532 let h = unsafe { &*handle };
3533 let _op = match h.guard.try_enter() {
3534 Some(op) => op,
3535 None => return NetError::ShuttingDown.into(),
3536 };
3537 let Some(req_s) = (unsafe { c_str_to_string(requirement_json) }) else {
3538 return NetError::InvalidUtf8.into();
3539 };
3540 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
3541 return NetError::InvalidUtf8.into();
3542 };
3543 let parsed_req: CapabilityRequirementJson = match serde_json::from_str(&req_s) {
3544 Ok(v) => v,
3545 Err(_) => return NetError::InvalidJson.into(),
3546 };
3547 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
3548 Ok(v) => v,
3549 Err(_) => return NetError::InvalidJson.into(),
3550 };
3551 let req = capability_requirement_from_json(parsed_req);
3552 let owned = scope_filter_from_json(parsed_scope);
3553 let result = with_scope_filter(&owned, |sf| h.inner.find_best_node_scoped(&req, sf));
3554 match result {
3555 Some(node_id) => unsafe {
3556 *out_node_id = node_id;
3557 *out_has_match = 1;
3558 },
3559 None => unsafe {
3560 *out_has_match = 0;
3561 },
3562 }
3563 0
3564}
3565
3566#[unsafe(no_mangle)]
3568pub unsafe extern "C" fn net_normalize_gpu_vendor(
3569 raw: *const c_char,
3570 out_json: *mut *mut c_char,
3571 out_len: *mut usize,
3572) -> c_int {
3573 if raw.is_null() || out_json.is_null() || out_len.is_null() {
3574 return NetError::NullPointer.into();
3575 }
3576 let Some(s) = (unsafe { c_str_to_string(raw) }) else {
3577 return NetError::InvalidUtf8.into();
3578 };
3579 let canonical = gpu_vendor_to_string_cap(parse_gpu_vendor_cap(&s));
3580 write_string_out(canonical.to_string(), out_json, out_len)
3581}
3582
3583pub(crate) const NET_ERR_GANG_INVALID: c_int = -140;
3594
3595#[derive(Deserialize)]
3599struct GangCriteriaJson {
3600 #[serde(default)]
3602 tags_all: Vec<String>,
3603 #[serde(default)]
3604 tags_any: Vec<String>,
3605 #[serde(default)]
3606 tag_groups_all: Vec<Vec<String>>,
3607 #[serde(default)]
3609 region: Option<String>,
3610 #[serde(default)]
3612 min_units: usize,
3613 #[serde(default)]
3614 max_load: Option<f32>,
3615 #[serde(default)]
3616 max_p50_latency_us: Option<u32>,
3617 #[serde(default)]
3618 require_all: Vec<String>,
3619 #[serde(default)]
3620 require_any: Vec<String>,
3621 #[serde(default)]
3622 selection: Option<String>,
3623 #[serde(default)]
3624 load_band_target: Option<f32>,
3625 #[serde(default)]
3626 prefer_capability: Option<String>,
3627}
3628
3629#[derive(Deserialize)]
3632struct IslandRecordJson {
3633 id: u64,
3634 #[serde(default)]
3635 units: Vec<u32>,
3636 #[serde(default)]
3637 capabilities: Vec<String>,
3638 #[serde(default)]
3639 load: f32,
3640 #[serde(default)]
3641 p50_latency_us: u32,
3642}
3643
3644fn build_gang_criteria(
3645 c: GangCriteriaJson,
3646) -> Option<crate::adapter::net::behavior::gang::MatchCriteria> {
3647 use crate::adapter::net::behavior::fold::{CapabilityFilter, CapabilityQuery};
3648 use crate::adapter::net::behavior::gang::{MatchCriteria, NumericFilter, SelectionPolicy};
3649 let selection = match c.selection.as_deref() {
3650 None | Some("least_loaded") => SelectionPolicy::LeastLoaded,
3651 Some("pack") => SelectionPolicy::Pack,
3652 Some("lowest_id") => SelectionPolicy::LowestId,
3653 Some("load_band") => SelectionPolicy::LoadBand(c.load_band_target.unwrap_or(0.5)),
3654 Some(_) => return None,
3655 };
3656 Some(MatchCriteria {
3657 capability: CapabilityQuery::Composite(CapabilityFilter {
3658 tags_all: c.tags_all,
3659 tags_any: c.tags_any,
3660 tag_groups_all: c.tag_groups_all,
3661 region: c.region,
3662 ..Default::default()
3663 }),
3664 numeric: NumericFilter {
3665 min_units: c.min_units,
3666 max_load: c.max_load,
3667 max_p50_latency_us: c.max_p50_latency_us,
3668 require_all: c.require_all,
3669 require_any: c.require_any,
3670 },
3671 selection,
3672 prefer_capability: c.prefer_capability,
3673 })
3674}
3675
3676#[unsafe(no_mangle)]
3681pub unsafe extern "C" fn net_mesh_publish_island_topology(
3682 handle: *mut MeshNodeHandle,
3683 record_json: *const c_char,
3684 out_count: *mut usize,
3685) -> c_int {
3686 if handle.is_null() || record_json.is_null() {
3687 return NetError::NullPointer.into();
3688 }
3689 let h = unsafe { &*handle };
3690 let _op = match h.guard.try_enter() {
3691 Some(op) => op,
3692 None => return NetError::ShuttingDown.into(),
3693 };
3694 let Some(js) = (unsafe { c_str_to_string(record_json) }) else {
3695 return NetError::InvalidUtf8.into();
3696 };
3697 let rec: IslandRecordJson = match serde_json::from_str(&js) {
3698 Ok(r) => r,
3699 Err(_) => return NET_ERR_GANG_INVALID,
3700 };
3701 use crate::adapter::net::behavior::fold::{IslandRecord, UnitSet};
3702 let record = IslandRecord {
3703 id: rec.id,
3704 units: UnitSet::new(rec.units),
3705 host: 0, capabilities: rec.capabilities,
3707 load: rec.load,
3708 p50_latency_us: rec.p50_latency_us,
3709 };
3710 let node = h.inner.clone();
3711 match block_on(async move { node.publish_island_topology(record).await }) {
3712 Ok(n) => {
3713 if !out_count.is_null() {
3714 unsafe {
3715 *out_count = n;
3716 }
3717 }
3718 0
3719 }
3720 Err(e) => adapter_err_to_code(&e),
3721 }
3722}
3723
3724#[unsafe(no_mangle)]
3728pub unsafe extern "C" fn net_mesh_match_islands(
3729 handle: *mut MeshNodeHandle,
3730 criteria_json: *const c_char,
3731 out_ids: *mut u64,
3732 cap: usize,
3733 out_count: *mut usize,
3734) -> c_int {
3735 if handle.is_null() || criteria_json.is_null() || out_count.is_null() {
3736 return NetError::NullPointer.into();
3737 }
3738 let h = unsafe { &*handle };
3739 let _op = match h.guard.try_enter() {
3740 Some(op) => op,
3741 None => return NetError::ShuttingDown.into(),
3742 };
3743 let Some(js) = (unsafe { c_str_to_string(criteria_json) }) else {
3744 return NetError::InvalidUtf8.into();
3745 };
3746 let parsed: GangCriteriaJson = match serde_json::from_str(&js) {
3747 Ok(c) => c,
3748 Err(_) => return NET_ERR_GANG_INVALID,
3749 };
3750 let Some(criteria) = build_gang_criteria(parsed) else {
3751 return NET_ERR_GANG_INVALID;
3752 };
3753 let ids = h.inner.match_islands(&criteria);
3754 unsafe {
3755 *out_count = ids.len();
3756 if !out_ids.is_null() {
3757 let n = ids.len().min(cap);
3758 std::ptr::copy_nonoverlapping(ids.as_ptr(), out_ids, n);
3759 }
3760 }
3761 0
3762}
3763
3764#[unsafe(no_mangle)]
3767pub unsafe extern "C" fn net_mesh_reserve_island(
3768 handle: *mut MeshNodeHandle,
3769 island: u64,
3770 until_unix_us: u64,
3771 out_outcome: *mut c_int,
3772) -> c_int {
3773 if handle.is_null() || out_outcome.is_null() {
3774 return NetError::NullPointer.into();
3775 }
3776 let h = unsafe { &*handle };
3777 let _op = match h.guard.try_enter() {
3778 Some(op) => op,
3779 None => return NetError::ShuttingDown.into(),
3780 };
3781 let node = h.inner.clone();
3782 match block_on(async move { node.reserve_island(island, until_unix_us).await }) {
3783 Ok(outcome) => {
3784 unsafe {
3785 *out_outcome = claim_outcome_code(outcome);
3786 }
3787 0
3788 }
3789 Err(e) => adapter_err_to_code(&e),
3790 }
3791}
3792
3793#[unsafe(no_mangle)]
3796pub unsafe extern "C" fn net_mesh_release_island(
3797 handle: *mut MeshNodeHandle,
3798 island: u64,
3799 out_outcome: *mut c_int,
3800) -> c_int {
3801 if handle.is_null() || out_outcome.is_null() {
3802 return NetError::NullPointer.into();
3803 }
3804 let h = unsafe { &*handle };
3805 let _op = match h.guard.try_enter() {
3806 Some(op) => op,
3807 None => return NetError::ShuttingDown.into(),
3808 };
3809 let node = h.inner.clone();
3810 match block_on(async move { node.release_island(island).await }) {
3811 Ok(outcome) => {
3812 unsafe {
3813 *out_outcome = claim_outcome_code(outcome);
3814 }
3815 0
3816 }
3817 Err(e) => adapter_err_to_code(&e),
3818 }
3819}
3820
3821#[unsafe(no_mangle)]
3825pub unsafe extern "C" fn net_mesh_claim_island(
3826 handle: *mut MeshNodeHandle,
3827 criteria_json: *const c_char,
3828 until_unix_us: u64,
3829 out_found: *mut c_int,
3830 out_island: *mut u64,
3831) -> c_int {
3832 if handle.is_null() || criteria_json.is_null() || out_found.is_null() || out_island.is_null() {
3833 return NetError::NullPointer.into();
3834 }
3835 unsafe {
3840 *out_found = 0;
3841 *out_island = 0;
3842 }
3843 let h = unsafe { &*handle };
3844 let _op = match h.guard.try_enter() {
3845 Some(op) => op,
3846 None => return NetError::ShuttingDown.into(),
3847 };
3848 let Some(js) = (unsafe { c_str_to_string(criteria_json) }) else {
3849 return NetError::InvalidUtf8.into();
3850 };
3851 let parsed: GangCriteriaJson = match serde_json::from_str(&js) {
3852 Ok(c) => c,
3853 Err(_) => return NET_ERR_GANG_INVALID,
3854 };
3855 let Some(criteria) = build_gang_criteria(parsed) else {
3856 return NET_ERR_GANG_INVALID;
3857 };
3858 let node = h.inner.clone();
3859 match block_on(async move { node.claim_island(&criteria, until_unix_us).await }) {
3860 Ok(Some(id)) => {
3861 unsafe {
3862 *out_found = 1;
3863 *out_island = id;
3864 }
3865 0
3866 }
3867 Ok(None) => 0,
3868 Err(e) => adapter_err_to_code(&e),
3869 }
3870}
3871
3872fn claim_outcome_code(o: crate::adapter::net::behavior::gang::ClaimOutcome) -> c_int {
3873 use crate::adapter::net::behavior::gang::ClaimOutcome;
3874 match o {
3875 ClaimOutcome::Won => 0,
3876 ClaimOutcome::Lost => 1,
3877 }
3878}
3879
3880#[cfg(test)]
3881mod tests {
3882 use super::*;
3883
3884 #[test]
3896 fn saturating_u16_cap_clamps_at_u16_max() {
3897 assert_eq!(saturating_u16_cap(0), 0);
3898 assert_eq!(saturating_u16_cap(42), 42);
3899 assert_eq!(saturating_u16_cap(u16::MAX as u32), u16::MAX);
3900 assert_eq!(saturating_u16_cap(u16::MAX as u32 + 1), u16::MAX);
3901 assert_eq!(saturating_u16_cap(u32::MAX), u16::MAX);
3902 }
3903
3904 #[test]
3913 fn parse_modality_cap_returns_none_on_unknown_strings() {
3914 for (s, expected) in [
3916 ("text", Modality::Text),
3917 ("Text", Modality::Text),
3918 ("TEXT", Modality::Text),
3919 ("image", Modality::Image),
3920 ("audio", Modality::Audio),
3921 ("video", Modality::Video),
3922 ("code", Modality::Code),
3923 ("embedding", Modality::Embedding),
3924 ("tool-use", Modality::ToolUse),
3925 ("tool_use", Modality::ToolUse),
3926 ("tooluse", Modality::ToolUse),
3927 ] {
3928 assert_eq!(
3929 parse_modality_cap(s),
3930 Some(expected),
3931 "known modality `{s}` must parse",
3932 );
3933 }
3934
3935 for s in ["audoi", "imageX", "vidoe", "embeding", "garbage", ""] {
3937 assert_eq!(
3938 parse_modality_cap(s),
3939 None,
3940 "unknown modality `{s}` must return None — pre-fix this \
3941 fell back to Modality::Text, advertising a capability \
3942 the node didn't actually have",
3943 );
3944 }
3945 }
3946
3947 #[test]
3957 fn gpu_info_from_json_saturates_fp16_tflops_to_u16_max() {
3958 let g = GpuJson {
3961 vendor: None,
3962 model: "test".to_string(),
3963 vram_gb: 0,
3964 compute_units: None,
3965 tensor_cores: None,
3966 fp16_tflops_x10: Some(1_000_000_000u32),
3967 };
3968 let info = gpu_info_from_json(g);
3969 assert_eq!(
3973 info.fp16_tflops_x10,
3974 u16::MAX as u32,
3975 "fp16_tflops_x10 must saturate at u16::MAX (65535) instead of \
3976 losing precision through the f32 round-trip; got {}",
3977 info.fp16_tflops_x10,
3978 );
3979
3980 let g_small = GpuJson {
3982 vendor: None,
3983 model: "test".to_string(),
3984 vram_gb: 0,
3985 compute_units: None,
3986 tensor_cores: None,
3987 fp16_tflops_x10: Some(425), };
3989 let info_small = gpu_info_from_json(g_small);
3990 assert_eq!(
3991 info_small.fp16_tflops_x10, 425,
3992 "small fp16_tflops_x10 must round-trip exactly"
3993 );
3994 }
3995
3996 #[test]
4009 fn alloc_bytes_round_trip_across_sizes() {
4010 for size in [0usize, 1, 15, 16, 17, 32, 64, 1024, 8192] {
4011 let src: Vec<u8> = (0..size).map(|i| (i as u8).wrapping_mul(37)).collect();
4012 let mut ptr: *mut u8 = std::ptr::null_mut();
4013 let mut len: usize = 0;
4014 let rc = alloc_bytes(&src, &mut ptr as *mut _, &mut len as *mut _);
4015 assert_eq!(rc, 0);
4016 assert_eq!(len, size);
4017 if size == 0 {
4018 assert!(ptr.is_null());
4019 } else {
4020 assert!(!ptr.is_null());
4021 let observed = unsafe { std::slice::from_raw_parts(ptr, len) };
4022 assert_eq!(observed, &src[..]);
4023 }
4024 unsafe { net_free_bytes(ptr, len) };
4027 }
4028 }
4029
4030 #[test]
4031 fn net_free_bytes_null_and_zero_len_are_noops() {
4032 unsafe { net_free_bytes(std::ptr::null_mut(), 0) };
4034 unsafe { net_free_bytes(std::ptr::null_mut(), 42) };
4035 let mut sentinel: u8 = 0;
4038 unsafe { net_free_bytes(&mut sentinel as *mut u8, 0) };
4039 }
4040
4041 #[test]
4053 fn net_free_bytes_does_not_panic_on_oversized_len() {
4054 let mut sentinel: u8 = 0;
4062 let ptr = &mut sentinel as *mut u8;
4063 unsafe { net_free_bytes(ptr, usize::MAX) };
4066 assert_eq!(sentinel, 0, "sentinel must not have been written through");
4069 }
4070
4071 #[test]
4080 fn net_mesh_shutdown_runs_even_with_outstanding_arc_refs() {
4081 let cfg = serde_json::json!({
4082 "bind_addr": "127.0.0.1:0",
4083 "psk_hex": "0".repeat(64),
4084 });
4085 let cfg_c = CString::new(cfg.to_string()).unwrap();
4086 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
4087 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
4088 assert_eq!(rc, 0, "net_mesh_new failed: {rc}");
4089 assert!(!out.is_null());
4090
4091 let inner_clone = {
4094 let h = unsafe { &*out };
4095 Arc::clone(&h.inner)
4096 };
4097 assert!(Arc::strong_count(&inner_clone) >= 2);
4098 assert!(!inner_clone.is_shutdown());
4099
4100 let rc = unsafe { net_mesh_shutdown(out) };
4101 assert_eq!(rc, 0, "net_mesh_shutdown returned {rc}");
4102 assert!(
4103 inner_clone.is_shutdown(),
4104 "shutdown flag must be set even when extra Arc refs are outstanding"
4105 );
4106
4107 drop(inner_clone);
4108 unsafe { net_mesh_free(out) };
4112 }
4113
4114 #[test]
4126 fn handles_match_rejects_stream_node_mismatch() {
4127 fn make_node_handle() -> *mut MeshNodeHandle {
4128 let cfg = serde_json::json!({
4129 "bind_addr": "127.0.0.1:0",
4130 "psk_hex": "0".repeat(64),
4131 });
4132 let cfg_c = CString::new(cfg.to_string()).unwrap();
4133 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
4134 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
4135 assert_eq!(rc, 0);
4136 assert!(!out.is_null());
4137 out
4138 }
4139
4140 let nh_a = make_node_handle();
4141 let nh_b = make_node_handle();
4142
4143 let sh_a = {
4151 let h = unsafe { &*nh_a };
4152 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
4153 MeshStreamHandle {
4154 stream: ManuallyDrop::new(CoreStream {
4155 peer_node_id: 0xDEAD,
4156 stream_id: 1,
4157 epoch: 0,
4158 config: StreamConfig::new(),
4159 }),
4160 _node: ManuallyDrop::new(node_clone),
4161 guard: HandleGuard::new(),
4162 }
4163 };
4164
4165 assert!(
4167 handles_match(&sh_a, unsafe { &*nh_a }),
4168 "stream from node_a + node_a handle must match"
4169 );
4170 assert!(
4172 !handles_match(&sh_a, unsafe { &*nh_b }),
4173 "stream from node_a + node_b handle must be rejected (#19)"
4174 );
4175
4176 unsafe {
4185 let mut sh_a = sh_a;
4186 let _ = ManuallyDrop::take(&mut sh_a.stream);
4187 let _ = ManuallyDrop::take(&mut sh_a._node);
4188 }
4189 unsafe { net_mesh_free(nh_a) };
4190 unsafe { net_mesh_free(nh_b) };
4191 }
4192
4193 #[test]
4200 fn net_mesh_free_is_idempotent() {
4201 let cfg = serde_json::json!({
4202 "bind_addr": "127.0.0.1:0",
4203 "psk_hex": "0".repeat(64),
4204 });
4205 let cfg_c = CString::new(cfg.to_string()).unwrap();
4206 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
4207 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
4208 assert!(!nh.is_null());
4209
4210 unsafe { net_mesh_free(nh) };
4211 unsafe { net_mesh_free(nh) };
4215 }
4216
4217 #[test]
4221 fn net_identity_free_is_idempotent() {
4222 let mut h: *mut IdentityHandle = std::ptr::null_mut();
4223 assert_eq!(unsafe { net_identity_generate(&mut h) }, 0);
4224 assert!(!h.is_null());
4225
4226 unsafe { net_identity_free(h) };
4227 unsafe { net_identity_free(h) };
4229 }
4230
4231 #[test]
4243 fn net_mesh_free_waits_for_inflight_op() {
4244 use std::sync::atomic::{AtomicBool, Ordering};
4245 use std::time::{Duration, Instant};
4246
4247 let cfg = serde_json::json!({
4248 "bind_addr": "127.0.0.1:0",
4249 "psk_hex": "0".repeat(64),
4250 });
4251 let cfg_c = CString::new(cfg.to_string()).unwrap();
4252 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
4253 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
4254 assert!(!nh.is_null());
4255
4256 let nh_addr = nh as usize;
4259 let started = Arc::new(AtomicBool::new(false));
4260 let release = Arc::new(AtomicBool::new(false));
4261 let started_w = started.clone();
4262 let release_w = release.clone();
4263
4264 let worker = std::thread::spawn(move || {
4265 let h = unsafe { &*(nh_addr as *mut MeshNodeHandle) };
4266 let op = h.guard.try_enter().expect("entry must succeed pre-free");
4270 started_w.store(true, Ordering::SeqCst);
4271 while !release_w.load(Ordering::SeqCst) {
4272 std::thread::sleep(Duration::from_millis(1));
4273 }
4274 drop(op);
4275 });
4276
4277 while !started.load(Ordering::SeqCst) {
4279 std::thread::yield_now();
4280 }
4281
4282 let release_clone = release.clone();
4285 std::thread::spawn(move || {
4286 std::thread::sleep(Duration::from_millis(50));
4287 release_clone.store(true, Ordering::SeqCst);
4288 });
4289
4290 let t0 = Instant::now();
4292 unsafe { net_mesh_free(nh) };
4293 let elapsed = t0.elapsed();
4294 assert!(
4295 elapsed >= Duration::from_millis(40),
4296 "net_mesh_free returned in {:?} — pre-fix it would have proceeded \
4297 immediately and the worker's subsequent op would UAF",
4298 elapsed,
4299 );
4300 worker.join().unwrap();
4301 }
4302
4303 #[test]
4310 fn net_mesh_stream_stats_returns_shutting_down_after_free() {
4311 let cfg = serde_json::json!({
4312 "bind_addr": "127.0.0.1:0",
4313 "psk_hex": "0".repeat(64),
4314 });
4315 let cfg_c = CString::new(cfg.to_string()).unwrap();
4316 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
4317 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
4318 assert!(!nh.is_null());
4319
4320 unsafe { net_mesh_free(nh) };
4323
4324 let mut out_json: *mut c_char = std::ptr::null_mut();
4325 let mut out_len: usize = 0;
4326 let rc = unsafe { net_mesh_stream_stats(nh, 0xDEAD, 1, &mut out_json, &mut out_len) };
4327 assert_eq!(
4328 rc,
4329 NetError::ShuttingDown as c_int,
4330 "post-free stream_stats must surface ShuttingDown (got {rc})",
4331 );
4332 assert!(
4333 out_json.is_null(),
4334 "no payload may be written after the guard fires",
4335 );
4336 }
4337
4338 #[test]
4343 fn net_identity_issue_token_returns_shutting_down_after_free() {
4344 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
4345 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
4346 assert!(!signer.is_null());
4347 unsafe { net_identity_free(signer) };
4348
4349 let subject = [0u8; 32];
4352 let scope = CString::new("[\"publish\"]").unwrap();
4353 let channel = CString::new("test-channel").unwrap();
4354 let mut out_token: *mut u8 = std::ptr::null_mut();
4355 let mut out_token_len: usize = 0;
4356 let rc = unsafe {
4357 net_identity_issue_token(
4358 signer,
4359 subject.as_ptr(),
4360 subject.len(),
4361 scope.as_ptr(),
4362 channel.as_ptr(),
4363 60,
4364 0,
4365 &mut out_token,
4366 &mut out_token_len,
4367 )
4368 };
4369 assert_eq!(
4370 rc,
4371 NetError::ShuttingDown as c_int,
4372 "post-free issue_token must surface ShuttingDown (got {rc})",
4373 );
4374 assert!(out_token.is_null(), "no token bytes may be allocated");
4375 }
4376
4377 #[test]
4383 fn net_delegate_token_returns_shutting_down_after_free() {
4384 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
4385 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
4386 assert!(!signer.is_null());
4387
4388 let subject = [0u8; 32];
4390 let scope = CString::new("[\"publish\",\"delegate\"]").unwrap();
4391 let channel = CString::new("test-channel").unwrap();
4392 let mut parent_bytes: *mut u8 = std::ptr::null_mut();
4393 let mut parent_len: usize = 0;
4394 assert_eq!(
4395 unsafe {
4396 net_identity_issue_token(
4397 signer,
4398 subject.as_ptr(),
4399 subject.len(),
4400 scope.as_ptr(),
4401 channel.as_ptr(),
4402 60,
4403 1,
4404 &mut parent_bytes,
4405 &mut parent_len,
4406 )
4407 },
4408 0,
4409 );
4410 assert!(!parent_bytes.is_null());
4411
4412 unsafe { net_identity_free(signer) };
4414
4415 let new_subject = [1u8; 32];
4416 let restricted = CString::new("[\"publish\"]").unwrap();
4417 let mut child_bytes: *mut u8 = std::ptr::null_mut();
4418 let mut child_len: usize = 0;
4419 let rc = unsafe {
4420 net_delegate_token(
4421 signer,
4422 parent_bytes,
4423 parent_len,
4424 new_subject.as_ptr(),
4425 new_subject.len(),
4426 restricted.as_ptr(),
4427 &mut child_bytes,
4428 &mut child_len,
4429 )
4430 };
4431 assert_eq!(
4432 rc,
4433 NetError::ShuttingDown as c_int,
4434 "post-free delegate_token must surface ShuttingDown (got {rc})",
4435 );
4436 assert!(child_bytes.is_null(), "no child token may be allocated");
4437
4438 unsafe { net_free_bytes(parent_bytes, parent_len) };
4440 }
4441
4442 #[test]
4443 fn hardware_from_json_saturates_overflow_cpu_fields() {
4444 let h = HardwareJson {
4447 cpu_cores: Some(70_000),
4448 cpu_threads: Some(200_000),
4449 memory_gb: None,
4450 gpu: None,
4451 additional_gpus: Vec::new(),
4452 storage_gb: None,
4453 network_gbps: None,
4454 accelerators: Vec::new(),
4455 };
4456 let hw = hardware_from_json(h);
4457 assert_eq!(hw.cpu_cores, u16::MAX);
4458 assert_eq!(hw.cpu_threads, u16::MAX);
4459 }
4460
4461 #[test]
4468 fn token_entry_points_reject_oversize_len() {
4469 let invalid_json: c_int = NetError::InvalidJson.into();
4470 let mut sentinel: u8 = 0;
4471 let token = &mut sentinel as *mut u8 as *const u8;
4472
4473 let mut out_json: *mut c_char = std::ptr::null_mut();
4474 let mut out_len: usize = 0;
4475 assert_eq!(
4476 unsafe { net_parse_token(token, usize::MAX, &mut out_json, &mut out_len) },
4477 invalid_json,
4478 );
4479 assert!(out_json.is_null());
4480
4481 let mut out_ok: c_int = -42;
4482 assert_eq!(
4483 unsafe { net_verify_token(token, usize::MAX, &mut out_ok) },
4484 invalid_json,
4485 );
4486
4487 let mut out_expired: c_int = -42;
4488 assert_eq!(
4489 unsafe { net_token_is_expired(token, usize::MAX, &mut out_expired) },
4490 invalid_json,
4491 );
4492
4493 assert_eq!(
4494 sentinel, 0,
4495 "sentinel must not be touched: the length guard fires before any deref"
4496 );
4497 }
4498}
4499
4500#[cfg(all(test, not(feature = "nat-traversal")))]
4501mod nat_traversal_stub_tests {
4502 use super::*;
4519 use std::ptr;
4520
4521 #[test]
4522 fn nat_type_stub_returns_unsupported() {
4523 let mut out_str: *mut c_char = ptr::null_mut();
4524 let mut out_len: usize = 0;
4525 let code = unsafe { net_mesh_nat_type(ptr::null_mut(), &mut out_str, &mut out_len) };
4528 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4529 }
4530
4531 #[test]
4532 fn reflex_addr_stub_returns_unsupported() {
4533 let mut out_str: *mut c_char = ptr::null_mut();
4534 let mut out_len: usize = 0;
4535 let code = unsafe { net_mesh_reflex_addr(ptr::null_mut(), &mut out_str, &mut out_len) };
4537 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4538 }
4539
4540 #[test]
4541 fn peer_nat_type_stub_returns_unsupported() {
4542 let mut out_str: *mut c_char = ptr::null_mut();
4543 let mut out_len: usize = 0;
4544 let code =
4546 unsafe { net_mesh_peer_nat_type(ptr::null_mut(), 0, &mut out_str, &mut out_len) };
4547 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4548 }
4549
4550 #[test]
4551 fn probe_reflex_stub_returns_unsupported() {
4552 let mut out_str: *mut c_char = ptr::null_mut();
4553 let mut out_len: usize = 0;
4554 let code = unsafe { net_mesh_probe_reflex(ptr::null_mut(), 0, &mut out_str, &mut out_len) };
4556 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4557 }
4558
4559 #[test]
4560 fn reclassify_nat_stub_returns_unsupported() {
4561 let code = unsafe { net_mesh_reclassify_nat(ptr::null_mut()) };
4563 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4564 }
4565
4566 #[test]
4567 fn traversal_stats_stub_returns_unsupported() {
4568 let mut a: u64 = 0;
4569 let mut b: u64 = 0;
4570 let mut c: u64 = 0;
4571 let code = unsafe { net_mesh_traversal_stats(ptr::null_mut(), &mut a, &mut b, &mut c) };
4573 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4574 }
4575
4576 #[test]
4577 fn connect_direct_stub_returns_unsupported() {
4578 let code = unsafe { net_mesh_connect_direct(ptr::null_mut(), 0, ptr::null(), 0) };
4580 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4581 }
4582
4583 #[test]
4584 fn set_reflex_override_stub_returns_unsupported() {
4585 let code = unsafe { net_mesh_set_reflex_override(ptr::null_mut(), ptr::null()) };
4587 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4588 }
4589
4590 #[test]
4591 fn clear_reflex_override_stub_returns_unsupported() {
4592 let code = unsafe { net_mesh_clear_reflex_override(ptr::null_mut()) };
4594 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
4595 }
4596
4597 #[test]
4603 fn unsupported_code_is_stable() {
4604 assert_eq!(NET_ERR_TRAVERSAL_UNSUPPORTED, -137);
4605 }
4606
4607 #[test]
4611 fn capability_set_from_go_marshal_preserves_gpu_vendor() {
4612 let json = r#"{"hardware":{"cpu_cores":16,"memory_gb":64,"gpu":{"vendor":"nvidia","model":"h100","vram_gb":80}},"tags":["gpu"]}"#;
4613 let parsed: CapabilitySetJson = serde_json::from_str(json).expect("JSON should parse");
4614 let caps = capability_set_from_json(parsed);
4615 let views = caps.views();
4619 assert_eq!(
4620 views.hardware().gpu_vendor(),
4621 Some(super::GpuVendor::Nvidia),
4622 "vendor lost in conversion"
4623 );
4624 assert_eq!(views.hardware().memory_gb, 64);
4625 assert_eq!(views.hardware().total_vram_gb(), 80);
4626 assert!(caps.has_tag("gpu"));
4627 }
4628
4629 #[test]
4638 fn collect_payloads_rejects_null_entry_with_nonzero_length() {
4639 let buf_a = b"hello".as_slice();
4640 let buf_b = b"world".as_slice();
4641 let ptrs: [*const u8; 3] = [buf_a.as_ptr(), std::ptr::null(), buf_b.as_ptr()];
4642 let lens: [usize; 3] = [buf_a.len(), 4, buf_b.len()];
4643
4644 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 3) };
4645 assert!(
4646 result.is_none(),
4647 "null entry with non-zero length must reject the whole batch"
4648 );
4649 }
4650
4651 #[test]
4652 fn collect_payloads_allows_null_entry_with_zero_length() {
4653 let buf_a = b"hello".as_slice();
4654 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), std::ptr::null()];
4655 let lens: [usize; 2] = [buf_a.len(), 0];
4656
4657 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
4658 .expect("zero-length null is treated as empty payload");
4659 assert_eq!(result.len(), 2);
4660 assert_eq!(&result[0][..], b"hello");
4661 assert!(result[1].is_empty());
4662 }
4663
4664 #[test]
4665 fn collect_payloads_happy_path() {
4666 let buf_a = b"abc".as_slice();
4667 let buf_b = b"defg".as_slice();
4668 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), buf_b.as_ptr()];
4669 let lens: [usize; 2] = [buf_a.len(), buf_b.len()];
4670
4671 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
4672 .expect("non-null entries should succeed");
4673 assert_eq!(result.len(), 2);
4674 assert_eq!(&result[0][..], b"abc");
4675 assert_eq!(&result[1][..], b"defg");
4676 }
4677}