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 #[serde(default)]
441 subnet_authorities:
442 Option<Vec<crate::adapter::net::subnet::provision::dto::SubnetAuthorityConfigDto>>,
443 #[serde(default)]
449 subnet_attachment: Option<Vec<u8>>,
450 #[serde(default)]
454 subnet_control_channel: Option<String>,
455 #[serde(default)]
467 subnet_exports: Option<Vec<crate::adapter::net::subnet::provision::dto::SubnetNamedExportDto>>,
468 identity_seed_hex: Option<String>,
473 #[serde(default)]
479 reflex_override: Option<String>,
480 #[serde(default)]
484 try_port_mapping: bool,
485 #[serde(default)]
496 auto_direct_upgrade: Option<bool>,
497}
498
499pub struct MeshNodeHandle {
512 inner: ManuallyDrop<Arc<MeshNode>>,
513 channel_configs: ManuallyDrop<Arc<ChannelConfigRegistry>>,
514 guard: HandleGuard,
515}
516
517#[unsafe(no_mangle)]
532pub unsafe extern "C" fn net_mesh_new(
533 config_json: *const c_char,
534 out_handle: *mut *mut MeshNodeHandle,
535) -> c_int {
536 if config_json.is_null() || out_handle.is_null() {
537 return NetError::NullPointer.into();
538 }
539 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
540 return NetError::InvalidUtf8.into();
541 };
542 let cfg: MeshNewConfig = match serde_json::from_str(&s) {
543 Ok(v) => v,
544 Err(_) => return NetError::InvalidJson.into(),
545 };
546 let bind_addr: std::net::SocketAddr = match cfg.bind_addr.parse() {
547 Ok(a) => a,
548 Err(_) => return NET_ERR_MESH_INIT,
549 };
550 let psk_bytes = match hex::decode(&cfg.psk_hex) {
551 Ok(b) => b,
552 Err(_) => return NET_ERR_MESH_INIT,
553 };
554 if psk_bytes.len() != 32 {
555 return NET_ERR_MESH_INIT;
556 }
557 let mut psk = [0u8; 32];
558 psk.copy_from_slice(&psk_bytes);
559
560 let mut node_cfg = MeshNodeConfig::new(bind_addr, psk);
561 if let Some(ms) = cfg.heartbeat_ms {
569 if ms == 0 {
570 return NetError::InvalidJson.into();
571 }
572 node_cfg = node_cfg.with_heartbeat_interval(std::time::Duration::from_millis(ms));
573 }
574 if let Some(ms) = cfg.session_timeout_ms {
575 if ms == 0 {
576 return NetError::InvalidJson.into();
577 }
578 node_cfg = node_cfg.with_session_timeout(std::time::Duration::from_millis(ms));
579 }
580 if let Some(n) = cfg.num_shards {
581 node_cfg = node_cfg.with_num_shards(n);
582 }
583 if let Some(ms) = cfg.capability_gc_interval_ms {
584 node_cfg = node_cfg.with_capability_gc_interval(std::time::Duration::from_millis(ms));
585 }
586 if let Some(b) = cfg.require_signed_capabilities {
587 node_cfg = node_cfg.with_require_signed_capabilities(b);
588 }
589 if let Some(levels) = cfg.subnet {
590 let Some(id) = subnet_id_from_json(levels) else {
591 return NET_ERR_MESH_INIT;
592 };
593 node_cfg = node_cfg.with_subnet(id);
594 }
595 if let Some(policy_js) = cfg.subnet_policy {
596 let Some(policy) = subnet_policy_from_json(policy_js) else {
597 return NET_ERR_MESH_INIT;
598 };
599 node_cfg = node_cfg.with_subnet_policy(Arc::new(policy));
600 }
601 {
609 use crate::adapter::net::subnet::provision;
610 let authorities = cfg.subnet_authorities.unwrap_or_default();
611 let mut core_authorities = Vec::with_capacity(authorities.len());
612 for dto in &authorities {
613 let Ok(a) = dto.to_core() else {
614 return NET_ERR_MESH_INIT;
615 };
616 core_authorities.push(a);
617 }
618 if provision::validate_subnet_authorities(&core_authorities).is_err() {
619 return NET_ERR_MESH_INIT;
620 }
621 for authority in core_authorities {
622 node_cfg = node_cfg.with_subnet_authority(authority);
623 }
624 if let Some(levels) = cfg.subnet_attachment {
625 let Ok(path) = (provision::dto::SubnetPathDto { levels }).to_core() else {
626 return NET_ERR_MESH_INIT;
627 };
628 node_cfg.subnet_attachment = Some(path);
632 }
633 if let Some(name) = cfg.subnet_control_channel {
634 let Ok(channel) = crate::adapter::net::ChannelName::new(&name) else {
635 return NET_ERR_MESH_INIT;
636 };
637 node_cfg = node_cfg.with_subnet_control_channel(channel);
638 }
639 for dto in cfg.subnet_exports.unwrap_or_default().iter() {
640 let Ok(export) = dto.to_core() else {
641 return NET_ERR_MESH_INIT;
642 };
643 node_cfg = node_cfg.with_subnet_export(export);
644 }
645 }
648 #[cfg(feature = "nat-traversal")]
649 if let Some(external_str) = cfg.reflex_override.as_deref() {
650 let Ok(external) = external_str.parse::<std::net::SocketAddr>() else {
651 return NET_ERR_MESH_INIT;
652 };
653 node_cfg = node_cfg.with_reflex_override(external);
654 }
655 #[cfg(not(feature = "nat-traversal"))]
659 let _ = cfg.reflex_override;
660 #[cfg(feature = "port-mapping")]
661 if cfg.try_port_mapping {
662 node_cfg = node_cfg.with_try_port_mapping(true);
663 }
664 #[cfg(not(feature = "port-mapping"))]
666 let _ = cfg.try_port_mapping;
667 #[cfg(feature = "nat-traversal")]
668 if let Some(enabled) = cfg.auto_direct_upgrade {
669 node_cfg = node_cfg.with_auto_direct_upgrade(enabled);
670 }
671 #[cfg(not(feature = "nat-traversal"))]
673 let _ = cfg.auto_direct_upgrade;
674
675 node_cfg.configured_identity = cfg.identity_seed_hex.is_some();
682
683 let identity = match cfg.identity_seed_hex {
684 Some(seed_hex) => {
685 let bytes = match hex::decode(&seed_hex) {
686 Ok(b) => b,
687 Err(_) => return NET_ERR_MESH_INIT,
688 };
689 if bytes.len() != 32 {
690 return NET_ERR_MESH_INIT;
691 }
692 let mut arr = [0u8; 32];
693 arr.copy_from_slice(&bytes);
694 EntityKeypair::from_bytes(arr)
695 }
696 None => EntityKeypair::generate(),
697 };
698 let result = block_on(async move { MeshNode::new(identity, node_cfg).await });
699 match result {
700 Ok(mut node) => {
701 let channel_configs = Arc::new(ChannelConfigRegistry::new());
702 node.set_channel_configs(channel_configs.clone());
703 node.set_token_cache(Arc::new(TokenCache::new()));
710 let handle = Box::new(MeshNodeHandle {
711 inner: ManuallyDrop::new(Arc::new(node)),
712 channel_configs: ManuallyDrop::new(channel_configs),
713 guard: HandleGuard::new(),
714 });
715 unsafe {
716 *out_handle = Box::into_raw(handle);
717 }
718 0
719 }
720 Err(_) => NET_ERR_MESH_INIT,
721 }
722}
723
724#[unsafe(no_mangle)]
725pub unsafe extern "C" fn net_mesh_free(handle: *mut MeshNodeHandle) {
726 if handle.is_null() {
727 return;
728 }
729 let h: &MeshNodeHandle = unsafe { &*handle };
734 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
735 unsafe {
737 let mh = &mut *handle;
738 let inner = ManuallyDrop::take(&mut mh.inner);
739 let configs = ManuallyDrop::take(&mut mh.channel_configs);
740 drop(inner);
741 drop(configs);
742 }
743 } else {
744 tracing::warn!(
745 "net_mesh_free: in-flight ops did not drain within deadline; \
746 leaking inner to avoid use-after-free"
747 );
748 }
749}
750
751#[cfg(any(feature = "cortex", feature = "dataforts"))]
769pub(super) fn mesh_node_arc(h: &MeshNodeHandle) -> Option<Arc<MeshNode>> {
770 let _op = h.guard.try_enter()?;
771 Some(Arc::clone(&h.inner))
772}
773
774#[unsafe(no_mangle)]
782pub unsafe extern "C" fn net_mesh_arc_clone(handle: *mut MeshNodeHandle) -> *mut Arc<MeshNode> {
783 if handle.is_null() {
784 return std::ptr::null_mut();
785 }
786 let h = unsafe { &*handle };
787 let _op = match h.guard.try_enter() {
789 Some(op) => op,
790 None => return std::ptr::null_mut(),
791 };
792 let cloned: Arc<MeshNode> = Arc::clone(&h.inner);
793 Box::into_raw(Box::new(cloned))
794}
795
796#[unsafe(no_mangle)]
803pub unsafe extern "C" fn net_mesh_channel_configs_arc_clone(
804 handle: *mut MeshNodeHandle,
805) -> *mut Arc<ChannelConfigRegistry> {
806 if handle.is_null() {
807 return std::ptr::null_mut();
808 }
809 let h = unsafe { &*handle };
810 let _op = match h.guard.try_enter() {
812 Some(op) => op,
813 None => return std::ptr::null_mut(),
814 };
815 let cloned: Arc<ChannelConfigRegistry> = Arc::clone(&h.channel_configs);
816 Box::into_raw(Box::new(cloned))
817}
818
819#[unsafe(no_mangle)]
822pub unsafe extern "C" fn net_mesh_arc_free(p: *mut Arc<MeshNode>) {
823 if p.is_null() {
824 return;
825 }
826 unsafe {
827 drop(Box::from_raw(p));
828 }
829}
830
831#[unsafe(no_mangle)]
834pub unsafe extern "C" fn net_mesh_channel_configs_arc_free(p: *mut Arc<ChannelConfigRegistry>) {
835 if p.is_null() {
836 return;
837 }
838 unsafe {
839 drop(Box::from_raw(p));
840 }
841}
842
843#[unsafe(no_mangle)]
846pub unsafe extern "C" fn net_mesh_public_key_hex(
847 handle: *mut MeshNodeHandle,
848 out_ptr: *mut *mut c_char,
849 out_len: *mut usize,
850) -> c_int {
851 if handle.is_null() || out_ptr.is_null() || out_len.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 s = hex::encode(h.inner.public_key());
860 write_string_out(s, out_ptr, out_len)
861}
862
863#[unsafe(no_mangle)]
864pub unsafe extern "C" fn net_mesh_node_id(handle: *mut MeshNodeHandle) -> u64 {
865 if handle.is_null() {
866 return 0;
867 }
868 let h = unsafe { &*handle };
869 let _op = match h.guard.try_enter() {
871 Some(op) => op,
872 None => return 0,
873 };
874 h.inner.node_id()
875}
876
877#[unsafe(no_mangle)]
881pub unsafe extern "C" fn net_mesh_entity_id(handle: *mut MeshNodeHandle, out: *mut u8) -> c_int {
882 if handle.is_null() || out.is_null() {
883 return NetError::NullPointer.into();
884 }
885 let h = unsafe { &*handle };
886 let _op = match h.guard.try_enter() {
887 Some(op) => op,
888 None => return NetError::ShuttingDown.into(),
889 };
890 let bytes = h.inner.entity_id().as_bytes();
891 unsafe {
892 std::ptr::copy_nonoverlapping(bytes.as_ptr(), out, 32);
893 }
894 0
895}
896unsafe fn parse_peer_pubkey_hex(peer_pubkey_hex: *const c_char) -> Result<[u8; 32], c_int> {
909 let Some(pk_s) = (unsafe { c_str_to_string(peer_pubkey_hex) }) else {
910 return Err(NetError::InvalidUtf8.into());
911 };
912 let pk_bytes = match hex::decode(pk_s) {
913 Ok(b) => b,
914 Err(_) => return Err(NET_ERR_MESH_HANDSHAKE),
915 };
916 if pk_bytes.len() != 32 {
917 return Err(NET_ERR_MESH_HANDSHAKE);
918 }
919 let mut pk = [0u8; 32];
920 pk.copy_from_slice(&pk_bytes);
921 Ok(pk)
922}
923
924#[unsafe(no_mangle)]
926pub unsafe extern "C" fn net_mesh_connect(
927 handle: *mut MeshNodeHandle,
928 peer_addr: *const c_char,
929 peer_pubkey_hex: *const c_char,
930 peer_node_id: u64,
931) -> c_int {
932 if handle.is_null() || peer_addr.is_null() || peer_pubkey_hex.is_null() {
933 return NetError::NullPointer.into();
934 }
935 let h = unsafe { &*handle };
936 let _op = match h.guard.try_enter() {
937 Some(op) => op,
938 None => return NetError::ShuttingDown.into(),
939 };
940 let Some(addr_s) = (unsafe { c_str_to_string(peer_addr) }) else {
941 return NetError::InvalidUtf8.into();
942 };
943 let addr: std::net::SocketAddr = match addr_s.parse() {
944 Ok(a) => a,
945 Err(_) => return NET_ERR_MESH_HANDSHAKE,
946 };
947 let pk = match unsafe { parse_peer_pubkey_hex(peer_pubkey_hex) } {
948 Ok(pk) => pk,
949 Err(code) => return code,
950 };
951
952 let node = h.inner.clone();
953 match block_on(async move { node.connect(addr, &pk, peer_node_id).await }) {
954 Ok(_) => 0,
955 Err(e) => adapter_err_to_code(&e),
956 }
957}
958
959#[unsafe(no_mangle)]
962pub unsafe extern "C" fn net_mesh_accept(
963 handle: *mut MeshNodeHandle,
964 peer_node_id: u64,
965 out_addr: *mut *mut c_char,
966 out_len: *mut usize,
967) -> c_int {
968 if handle.is_null() || out_addr.is_null() || out_len.is_null() {
969 return NetError::NullPointer.into();
970 }
971 let h = unsafe { &*handle };
972 let _op = match h.guard.try_enter() {
973 Some(op) => op,
974 None => return NetError::ShuttingDown.into(),
975 };
976 let node = h.inner.clone();
977 match block_on(async move { node.accept(peer_node_id).await }) {
978 Ok((addr, _)) => write_string_out(addr.to_string(), out_addr, out_len),
979 Err(e) => adapter_err_to_code(&e),
980 }
981}
982
983#[unsafe(no_mangle)]
984pub unsafe extern "C" fn net_mesh_start(handle: *mut MeshNodeHandle) -> c_int {
985 if handle.is_null() {
986 return NetError::NullPointer.into();
987 }
988 let h = unsafe { &*handle };
989 let _op = match h.guard.try_enter() {
990 Some(op) => op,
991 None => return NetError::ShuttingDown.into(),
992 };
993 let node = h.inner.clone();
994 block_on(async move { node.start_arc() });
998 0
999}
1000
1001#[unsafe(no_mangle)]
1013pub unsafe extern "C" fn net_mesh_shutdown(handle: *mut MeshNodeHandle) -> c_int {
1014 if handle.is_null() {
1015 return NetError::NullPointer.into();
1016 }
1017 let h = unsafe { &*handle };
1018 let _op = match h.guard.try_enter() {
1019 Some(op) => op,
1020 None => return NetError::ShuttingDown.into(),
1021 };
1022 match block_on(async { h.inner.shutdown().await }) {
1023 Ok(()) => 0,
1024 Err(e) => adapter_err_to_code(&e),
1025 }
1026}
1027
1028#[cfg(feature = "nat-traversal")]
1050#[unsafe(no_mangle)]
1051pub unsafe extern "C" fn net_mesh_nat_type(
1052 handle: *mut MeshNodeHandle,
1053 out_str: *mut *mut c_char,
1054 out_len: *mut usize,
1055) -> c_int {
1056 if handle.is_null() || out_str.is_null() || out_len.is_null() {
1057 return NetError::NullPointer.into();
1058 }
1059 let h = unsafe { &*handle };
1060 let _op = match h.guard.try_enter() {
1061 Some(op) => op,
1062 None => return NetError::ShuttingDown.into(),
1063 };
1064 write_string_out(
1065 nat_class_to_str(h.inner.nat_class()).to_string(),
1066 out_str,
1067 out_len,
1068 )
1069}
1070
1071#[cfg(feature = "nat-traversal")]
1076#[unsafe(no_mangle)]
1077pub unsafe extern "C" fn net_mesh_reflex_addr(
1078 handle: *mut MeshNodeHandle,
1079 out_str: *mut *mut c_char,
1080 out_len: *mut usize,
1081) -> c_int {
1082 if handle.is_null() || out_str.is_null() || out_len.is_null() {
1083 return NetError::NullPointer.into();
1084 }
1085 let h = unsafe { &*handle };
1086 let _op = match h.guard.try_enter() {
1087 Some(op) => op,
1088 None => return NetError::ShuttingDown.into(),
1089 };
1090 let s = h
1091 .inner
1092 .reflex_addr()
1093 .map(|a| a.to_string())
1094 .unwrap_or_default();
1095 write_string_out(s, out_str, out_len)
1096}
1097
1098#[cfg(feature = "nat-traversal")]
1102#[unsafe(no_mangle)]
1103pub unsafe extern "C" fn net_mesh_peer_nat_type(
1104 handle: *mut MeshNodeHandle,
1105 peer_node_id: u64,
1106 out_str: *mut *mut c_char,
1107 out_len: *mut usize,
1108) -> c_int {
1109 if handle.is_null() || out_str.is_null() || out_len.is_null() {
1110 return NetError::NullPointer.into();
1111 }
1112 let h = unsafe { &*handle };
1113 let _op = match h.guard.try_enter() {
1114 Some(op) => op,
1115 None => return NetError::ShuttingDown.into(),
1116 };
1117 write_string_out(
1118 nat_class_to_str(h.inner.peer_nat_class(peer_node_id)).to_string(),
1119 out_str,
1120 out_len,
1121 )
1122}
1123
1124#[cfg(feature = "nat-traversal")]
1133#[unsafe(no_mangle)]
1134pub unsafe extern "C" fn net_mesh_probe_reflex(
1135 handle: *mut MeshNodeHandle,
1136 peer_node_id: u64,
1137 out_str: *mut *mut c_char,
1138 out_len: *mut usize,
1139) -> c_int {
1140 if handle.is_null() || out_str.is_null() || out_len.is_null() {
1141 return NetError::NullPointer.into();
1142 }
1143 let h = unsafe { &*handle };
1144 let _op = match h.guard.try_enter() {
1145 Some(op) => op,
1146 None => return NetError::ShuttingDown.into(),
1147 };
1148 let node = h.inner.clone();
1149 match block_on(async move { node.probe_reflex(peer_node_id).await }) {
1150 Ok(addr) => write_string_out(addr.to_string(), out_str, out_len),
1151 Err(e) => traversal_err_to_code(&e),
1152 }
1153}
1154
1155#[cfg(feature = "nat-traversal")]
1160#[unsafe(no_mangle)]
1161pub unsafe extern "C" fn net_mesh_reclassify_nat(handle: *mut MeshNodeHandle) -> c_int {
1162 if handle.is_null() {
1163 return NetError::NullPointer.into();
1164 }
1165 let h = unsafe { &*handle };
1166 let _op = match h.guard.try_enter() {
1167 Some(op) => op,
1168 None => return NetError::ShuttingDown.into(),
1169 };
1170 let node = h.inner.clone();
1171 block_on(async move { node.reclassify_nat().await });
1172 0
1173}
1174
1175#[cfg(feature = "nat-traversal")]
1180#[unsafe(no_mangle)]
1181pub unsafe extern "C" fn net_mesh_traversal_stats(
1182 handle: *mut MeshNodeHandle,
1183 out_punches_attempted: *mut u64,
1184 out_punches_succeeded: *mut u64,
1185 out_relay_fallbacks: *mut u64,
1186) -> c_int {
1187 if handle.is_null() {
1188 return NetError::NullPointer.into();
1189 }
1190 let h = unsafe { &*handle };
1191 let _op = match h.guard.try_enter() {
1192 Some(op) => op,
1193 None => return NetError::ShuttingDown.into(),
1194 };
1195 let snap = h.inner.traversal_stats();
1196 unsafe {
1197 if !out_punches_attempted.is_null() {
1198 *out_punches_attempted = snap.punches_attempted;
1199 }
1200 if !out_punches_succeeded.is_null() {
1201 *out_punches_succeeded = snap.punches_succeeded;
1202 }
1203 if !out_relay_fallbacks.is_null() {
1204 *out_relay_fallbacks = snap.relay_fallbacks;
1205 }
1206 }
1207 0
1208}
1209
1210#[cfg(feature = "nat-traversal")]
1222#[unsafe(no_mangle)]
1223pub unsafe extern "C" fn net_mesh_connect_direct(
1224 handle: *mut MeshNodeHandle,
1225 peer_node_id: u64,
1226 peer_pubkey_hex: *const c_char,
1227 coordinator: u64,
1228) -> c_int {
1229 if handle.is_null() || peer_pubkey_hex.is_null() {
1230 return NetError::NullPointer.into();
1231 }
1232 let h = unsafe { &*handle };
1233 let _op = match h.guard.try_enter() {
1234 Some(op) => op,
1235 None => return NetError::ShuttingDown.into(),
1236 };
1237 let pk = match unsafe { parse_peer_pubkey_hex(peer_pubkey_hex) } {
1238 Ok(pk) => pk,
1239 Err(code) => return code,
1240 };
1241
1242 let node = h.inner.clone();
1243 match block_on(async move { node.connect_direct(peer_node_id, &pk, coordinator).await }) {
1244 Ok(_) => 0,
1245 Err(e) => traversal_err_to_code(&e),
1246 }
1247}
1248
1249#[cfg(feature = "nat-traversal")]
1258#[unsafe(no_mangle)]
1259pub unsafe extern "C" fn net_mesh_connect_direct_auto(
1260 handle: *mut MeshNodeHandle,
1261 peer_node_id: u64,
1262 peer_pubkey_hex: *const c_char,
1263) -> c_int {
1264 if handle.is_null() || peer_pubkey_hex.is_null() {
1265 return NetError::NullPointer.into();
1266 }
1267 let h = unsafe { &*handle };
1268 let _op = match h.guard.try_enter() {
1269 Some(op) => op,
1270 None => return NetError::ShuttingDown.into(),
1271 };
1272 let pk = match unsafe { parse_peer_pubkey_hex(peer_pubkey_hex) } {
1273 Ok(pk) => pk,
1274 Err(code) => return code,
1275 };
1276
1277 let node = h.inner.clone();
1278 match block_on(async move { node.connect_direct_auto(peer_node_id, &pk).await }) {
1279 Ok(_) => 0,
1280 Err(e) => traversal_err_to_code(&e),
1281 }
1282}
1283
1284#[repr(C)]
1290pub struct NetTraversalStatsV2 {
1291 pub punches_attempted: u64,
1293 pub punches_succeeded: u64,
1295 pub punches_failed: u64,
1297 pub relay_fallbacks: u64,
1299 pub punch_timeouts: u64,
1301 pub punch_rejections: u64,
1303 pub rendezvous_no_relay: u64,
1305 pub upgrades_attempted: u64,
1307 pub upgrades_succeeded: u64,
1309 pub upgrades_deferred_busy: u64,
1311 pub port_mapping_renewals: u64,
1313 pub port_mapping_active: u8,
1315 pub port_mapping_external: [c_char; 64],
1319}
1320
1321#[cfg(feature = "nat-traversal")]
1325fn fill_traversal_stats_v2(
1326 snap: &crate::adapter::net::traversal::TraversalStatsSnapshot,
1327 out: &mut NetTraversalStatsV2,
1328) {
1329 out.punches_attempted = snap.punches_attempted;
1330 out.punches_succeeded = snap.punches_succeeded;
1331 out.punches_failed = snap.punches_failed;
1332 out.relay_fallbacks = snap.relay_fallbacks;
1333 out.punch_timeouts = snap.punch_timeouts;
1334 out.punch_rejections = snap.punch_rejections;
1335 out.rendezvous_no_relay = snap.rendezvous_no_relay;
1336 out.upgrades_attempted = snap.upgrades_attempted;
1337 out.upgrades_succeeded = snap.upgrades_succeeded;
1338 out.upgrades_deferred_busy = snap.upgrades_deferred_busy;
1339 out.port_mapping_renewals = snap.port_mapping_renewals;
1340 out.port_mapping_active = u8::from(snap.port_mapping_active);
1341 out.port_mapping_external = [0; 64];
1342 if let Some(addr) = snap.port_mapping_external {
1343 let s = addr.to_string();
1344 let n = s.len().min(63);
1349 for (dst, src) in out.port_mapping_external[..n].iter_mut().zip(s.as_bytes()) {
1350 *dst = *src as c_char;
1351 }
1352 }
1353}
1354
1355#[cfg(feature = "nat-traversal")]
1366#[unsafe(no_mangle)]
1367pub unsafe extern "C" fn net_mesh_traversal_stats_v2(
1368 handle: *mut MeshNodeHandle,
1369 out: *mut NetTraversalStatsV2,
1370) -> c_int {
1371 if handle.is_null() || out.is_null() {
1372 return NetError::NullPointer.into();
1373 }
1374 let h = unsafe { &*handle };
1375 let _op = match h.guard.try_enter() {
1376 Some(op) => op,
1377 None => return NetError::ShuttingDown.into(),
1378 };
1379 let snap = h.inner.traversal_stats();
1380 fill_traversal_stats_v2(&snap, unsafe { &mut *out });
1381 0
1382}
1383
1384#[cfg(feature = "nat-traversal")]
1392#[unsafe(no_mangle)]
1393pub unsafe extern "C" fn net_mesh_set_reflex_override(
1394 handle: *mut MeshNodeHandle,
1395 external: *const c_char,
1396) -> c_int {
1397 if handle.is_null() || external.is_null() {
1398 return NetError::NullPointer.into();
1399 }
1400 let h = unsafe { &*handle };
1401 let _op = match h.guard.try_enter() {
1402 Some(op) => op,
1403 None => return NetError::ShuttingDown.into(),
1404 };
1405 let Some(s) = (unsafe { c_str_to_string(external) }) else {
1406 return NetError::InvalidUtf8.into();
1407 };
1408 let Ok(addr) = s.parse::<std::net::SocketAddr>() else {
1409 return NET_ERR_MESH_INIT;
1410 };
1411 h.inner.set_reflex_override(addr);
1412 0
1413}
1414
1415#[cfg(feature = "nat-traversal")]
1423#[unsafe(no_mangle)]
1424pub unsafe extern "C" fn net_mesh_clear_reflex_override(handle: *mut MeshNodeHandle) -> c_int {
1425 if handle.is_null() {
1426 return NetError::NullPointer.into();
1427 }
1428 let h = unsafe { &*handle };
1429 let _op = match h.guard.try_enter() {
1430 Some(op) => op,
1431 None => return NetError::ShuttingDown.into(),
1432 };
1433 h.inner.clear_reflex_override();
1434 0
1435}
1436
1437#[cfg(not(feature = "nat-traversal"))]
1460#[unsafe(no_mangle)]
1461pub unsafe extern "C" fn net_mesh_nat_type(
1462 _handle: *mut MeshNodeHandle,
1463 _out_str: *mut *mut c_char,
1464 _out_len: *mut usize,
1465) -> c_int {
1466 NET_ERR_TRAVERSAL_UNSUPPORTED
1467}
1468
1469#[cfg(not(feature = "nat-traversal"))]
1470#[unsafe(no_mangle)]
1471pub unsafe extern "C" fn net_mesh_reflex_addr(
1472 _handle: *mut MeshNodeHandle,
1473 _out_str: *mut *mut c_char,
1474 _out_len: *mut usize,
1475) -> c_int {
1476 NET_ERR_TRAVERSAL_UNSUPPORTED
1477}
1478
1479#[cfg(not(feature = "nat-traversal"))]
1480#[unsafe(no_mangle)]
1481pub unsafe extern "C" fn net_mesh_peer_nat_type(
1482 _handle: *mut MeshNodeHandle,
1483 _peer_node_id: u64,
1484 _out_str: *mut *mut c_char,
1485 _out_len: *mut usize,
1486) -> c_int {
1487 NET_ERR_TRAVERSAL_UNSUPPORTED
1488}
1489
1490#[cfg(not(feature = "nat-traversal"))]
1491#[unsafe(no_mangle)]
1492pub unsafe extern "C" fn net_mesh_probe_reflex(
1493 _handle: *mut MeshNodeHandle,
1494 _peer_node_id: u64,
1495 _out_str: *mut *mut c_char,
1496 _out_len: *mut usize,
1497) -> c_int {
1498 NET_ERR_TRAVERSAL_UNSUPPORTED
1499}
1500
1501#[cfg(not(feature = "nat-traversal"))]
1502#[unsafe(no_mangle)]
1503pub unsafe extern "C" fn net_mesh_reclassify_nat(_handle: *mut MeshNodeHandle) -> c_int {
1504 NET_ERR_TRAVERSAL_UNSUPPORTED
1505}
1506
1507#[cfg(not(feature = "nat-traversal"))]
1508#[unsafe(no_mangle)]
1509pub unsafe extern "C" fn net_mesh_traversal_stats(
1510 _handle: *mut MeshNodeHandle,
1511 _out_punches_attempted: *mut u64,
1512 _out_punches_succeeded: *mut u64,
1513 _out_relay_fallbacks: *mut u64,
1514) -> c_int {
1515 NET_ERR_TRAVERSAL_UNSUPPORTED
1516}
1517
1518#[cfg(not(feature = "nat-traversal"))]
1519#[unsafe(no_mangle)]
1520pub unsafe extern "C" fn net_mesh_connect_direct(
1521 _handle: *mut MeshNodeHandle,
1522 _peer_node_id: u64,
1523 _peer_pubkey_hex: *const c_char,
1524 _coordinator: u64,
1525) -> c_int {
1526 NET_ERR_TRAVERSAL_UNSUPPORTED
1527}
1528
1529#[cfg(not(feature = "nat-traversal"))]
1530#[unsafe(no_mangle)]
1531pub unsafe extern "C" fn net_mesh_connect_direct_auto(
1532 _handle: *mut MeshNodeHandle,
1533 _peer_node_id: u64,
1534 _peer_pubkey_hex: *const c_char,
1535) -> c_int {
1536 NET_ERR_TRAVERSAL_UNSUPPORTED
1537}
1538
1539#[cfg(not(feature = "nat-traversal"))]
1540#[unsafe(no_mangle)]
1541pub unsafe extern "C" fn net_mesh_traversal_stats_v2(
1542 _handle: *mut MeshNodeHandle,
1543 _out: *mut NetTraversalStatsV2,
1544) -> c_int {
1545 NET_ERR_TRAVERSAL_UNSUPPORTED
1546}
1547
1548#[cfg(not(feature = "nat-traversal"))]
1549#[unsafe(no_mangle)]
1550pub unsafe extern "C" fn net_mesh_set_reflex_override(
1551 _handle: *mut MeshNodeHandle,
1552 _external: *const c_char,
1553) -> c_int {
1554 NET_ERR_TRAVERSAL_UNSUPPORTED
1555}
1556
1557#[cfg(not(feature = "nat-traversal"))]
1558#[unsafe(no_mangle)]
1559pub unsafe extern "C" fn net_mesh_clear_reflex_override(_handle: *mut MeshNodeHandle) -> c_int {
1560 NET_ERR_TRAVERSAL_UNSUPPORTED
1561}
1562
1563#[derive(Deserialize, Default)]
1568struct StreamOpenConfig {
1569 reliability: Option<String>,
1571 window_bytes: Option<u32>,
1574 fairness_weight: Option<u8>,
1575}
1576
1577pub struct MeshStreamHandle {
1592 stream: ManuallyDrop<CoreStream>,
1593 _node: ManuallyDrop<Arc<MeshNode>>,
1596 guard: HandleGuard,
1597}
1598
1599#[unsafe(no_mangle)]
1600pub unsafe extern "C" fn net_mesh_open_stream(
1601 handle: *mut MeshNodeHandle,
1602 peer_node_id: u64,
1603 stream_id: u64,
1604 config_json: *const c_char,
1605 out_stream: *mut *mut MeshStreamHandle,
1606) -> c_int {
1607 if handle.is_null() || out_stream.is_null() {
1608 return NetError::NullPointer.into();
1609 }
1610 let h = unsafe { &*handle };
1611 let _op = match h.guard.try_enter() {
1612 Some(op) => op,
1613 None => return NetError::ShuttingDown.into(),
1614 };
1615 let cfg_json: StreamOpenConfig = if config_json.is_null() {
1616 StreamOpenConfig::default()
1617 } else {
1618 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1619 return NetError::InvalidUtf8.into();
1620 };
1621 match serde_json::from_str(&s) {
1622 Ok(v) => v,
1623 Err(_) => return NetError::InvalidJson.into(),
1624 }
1625 };
1626 let reliability = match cfg_json.reliability.as_deref() {
1627 None | Some("fire_and_forget") => Reliability::FireAndForget,
1628 Some("reliable") => Reliability::Reliable,
1629 Some(_) => return NET_ERR_MESH_TRANSPORT,
1630 };
1631 let window = cfg_json.window_bytes.unwrap_or(DEFAULT_STREAM_WINDOW_BYTES);
1632 let weight = cfg_json.fairness_weight.unwrap_or(1);
1633 let cfg = StreamConfig::new()
1634 .with_reliability(reliability)
1635 .with_window_bytes(window)
1636 .with_fairness_weight(weight);
1637 match h.inner.open_stream(peer_node_id, stream_id, cfg) {
1638 Ok(stream) => {
1639 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
1640 let sh = Box::new(MeshStreamHandle {
1641 stream: ManuallyDrop::new(stream),
1642 _node: ManuallyDrop::new(node_clone),
1643 guard: HandleGuard::new(),
1644 });
1645 unsafe {
1646 *out_stream = Box::into_raw(sh);
1647 }
1648 0
1649 }
1650 Err(e) => adapter_err_to_code(&e),
1651 }
1652}
1653
1654#[unsafe(no_mangle)]
1655pub unsafe extern "C" fn net_mesh_stream_free(handle: *mut MeshStreamHandle) {
1656 if handle.is_null() {
1657 return;
1658 }
1659 let h: &MeshStreamHandle = unsafe { &*handle };
1661 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
1662 unsafe {
1664 let _stream = ManuallyDrop::take(&mut (*handle).stream);
1668 let node = ManuallyDrop::take(&mut (*handle)._node);
1669 drop(node);
1670 }
1671 } else {
1672 tracing::warn!(
1673 "net_mesh_stream_free: in-flight ops did not drain within deadline; \
1674 leaking inner to avoid use-after-free"
1675 );
1676 }
1677}
1678
1679unsafe fn collect_payloads(
1689 payloads: *const *const u8,
1690 lens: *const usize,
1691 count: usize,
1692) -> Option<Vec<Bytes>> {
1693 let mut out = Vec::with_capacity(count);
1694 for i in 0..count {
1695 let ptr = *payloads.add(i);
1696 let len = *lens.add(i);
1697 if ptr.is_null() {
1698 if len == 0 {
1699 out.push(Bytes::new());
1700 continue;
1701 }
1702 return None;
1703 }
1704 if len > isize::MAX as usize {
1708 return None;
1709 }
1710 let slice = std::slice::from_raw_parts(ptr, len);
1711 out.push(Bytes::copy_from_slice(slice));
1712 }
1713 Some(out)
1714}
1715
1716#[inline]
1724fn handles_match(sh: &MeshStreamHandle, nh: &MeshNodeHandle) -> bool {
1725 Arc::ptr_eq(&sh._node, &nh.inner)
1726}
1727
1728#[unsafe(no_mangle)]
1729pub unsafe extern "C" fn net_mesh_send(
1730 handle: *mut MeshStreamHandle,
1731 payloads: *const *const u8,
1732 lens: *const usize,
1733 count: usize,
1734 node_handle: *mut MeshNodeHandle,
1735) -> c_int {
1736 if handle.is_null() || node_handle.is_null() {
1737 return NetError::NullPointer.into();
1738 }
1739 if count > 0 && (payloads.is_null() || lens.is_null()) {
1740 return NetError::NullPointer.into();
1741 }
1742 let sh = unsafe { &*handle };
1743 let nh = unsafe { &*node_handle };
1744 let _sh_op = match sh.guard.try_enter() {
1747 Some(op) => op,
1748 None => return NetError::ShuttingDown.into(),
1749 };
1750 let _nh_op = match nh.guard.try_enter() {
1751 Some(op) => op,
1752 None => return NetError::ShuttingDown.into(),
1753 };
1754 if !handles_match(sh, nh) {
1755 return NetError::MismatchedHandles.into();
1756 }
1757 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1758 Some(v) => v,
1759 None => return NetError::NullPointer.into(),
1760 };
1761 let node = nh.inner.clone();
1762 let stream = sh.stream.clone();
1763 match block_on(async move { node.send_on_stream(&stream, &payloads).await }) {
1764 Ok(()) => 0,
1765 Err(e) => stream_err_to_code(&e),
1766 }
1767}
1768
1769#[unsafe(no_mangle)]
1770pub unsafe extern "C" fn net_mesh_send_with_retry(
1771 handle: *mut MeshStreamHandle,
1772 payloads: *const *const u8,
1773 lens: *const usize,
1774 count: usize,
1775 max_retries: u32,
1776 node_handle: *mut MeshNodeHandle,
1777) -> c_int {
1778 if handle.is_null() || node_handle.is_null() {
1779 return NetError::NullPointer.into();
1780 }
1781 if count > 0 && (payloads.is_null() || lens.is_null()) {
1782 return NetError::NullPointer.into();
1783 }
1784 let sh = unsafe { &*handle };
1785 let nh = unsafe { &*node_handle };
1786 let _sh_op = match sh.guard.try_enter() {
1789 Some(op) => op,
1790 None => return NetError::ShuttingDown.into(),
1791 };
1792 let _nh_op = match nh.guard.try_enter() {
1793 Some(op) => op,
1794 None => return NetError::ShuttingDown.into(),
1795 };
1796 if !handles_match(sh, nh) {
1797 return NetError::MismatchedHandles.into();
1798 }
1799 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1800 Some(v) => v,
1801 None => return NetError::NullPointer.into(),
1802 };
1803 let node = nh.inner.clone();
1804 let stream = sh.stream.clone();
1805 match block_on(async move {
1806 node.send_with_retry(&stream, &payloads, max_retries as usize)
1807 .await
1808 }) {
1809 Ok(()) => 0,
1810 Err(e) => stream_err_to_code(&e),
1811 }
1812}
1813
1814#[unsafe(no_mangle)]
1815pub unsafe extern "C" fn net_mesh_send_blocking(
1816 handle: *mut MeshStreamHandle,
1817 payloads: *const *const u8,
1818 lens: *const usize,
1819 count: usize,
1820 node_handle: *mut MeshNodeHandle,
1821) -> c_int {
1822 if handle.is_null() || node_handle.is_null() {
1823 return NetError::NullPointer.into();
1824 }
1825 if count > 0 && (payloads.is_null() || lens.is_null()) {
1826 return NetError::NullPointer.into();
1827 }
1828 let sh = unsafe { &*handle };
1829 let nh = unsafe { &*node_handle };
1830 let _sh_op = match sh.guard.try_enter() {
1833 Some(op) => op,
1834 None => return NetError::ShuttingDown.into(),
1835 };
1836 let _nh_op = match nh.guard.try_enter() {
1837 Some(op) => op,
1838 None => return NetError::ShuttingDown.into(),
1839 };
1840 if !handles_match(sh, nh) {
1841 return NetError::MismatchedHandles.into();
1842 }
1843 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1844 Some(v) => v,
1845 None => return NetError::NullPointer.into(),
1846 };
1847 let node = nh.inner.clone();
1848 let stream = sh.stream.clone();
1849 match block_on(async move { node.send_blocking(&stream, &payloads).await }) {
1850 Ok(()) => 0,
1851 Err(e) => stream_err_to_code(&e),
1852 }
1853}
1854
1855#[derive(Serialize)]
1856struct StreamStatsJson {
1857 tx_seq: u64,
1858 rx_seq: u64,
1859 inbound_pending: u64,
1860 last_activity_ns: u64,
1861 active: bool,
1862 backpressure_events: u64,
1863 tx_credit_remaining: u32,
1864 tx_window: u32,
1865 credit_grants_received: u64,
1866 credit_grants_sent: u64,
1867}
1868
1869#[unsafe(no_mangle)]
1870pub unsafe extern "C" fn net_mesh_stream_stats(
1871 node_handle: *mut MeshNodeHandle,
1872 peer_node_id: u64,
1873 stream_id: u64,
1874 out_json: *mut *mut c_char,
1875 out_len: *mut usize,
1876) -> c_int {
1877 if node_handle.is_null() || out_json.is_null() || out_len.is_null() {
1878 return NetError::NullPointer.into();
1879 }
1880 let h = unsafe { &*node_handle };
1881 let _op = match h.guard.try_enter() {
1882 Some(op) => op,
1883 None => return NetError::ShuttingDown.into(),
1884 };
1885 match h.inner.stream_stats(peer_node_id, stream_id) {
1886 Some(s) => {
1887 let js = StreamStatsJson {
1888 tx_seq: s.tx_seq,
1889 rx_seq: s.rx_seq,
1890 inbound_pending: s.inbound_pending,
1891 last_activity_ns: s.last_activity_ns,
1892 active: s.active,
1893 backpressure_events: s.backpressure_events,
1894 tx_credit_remaining: s.tx_credit_remaining,
1895 tx_window: s.tx_window,
1896 credit_grants_received: s.credit_grants_received,
1897 credit_grants_sent: s.credit_grants_sent,
1898 };
1899 write_json_out(&js, out_json, out_len)
1900 }
1901 None => {
1902 write_string_out("null".to_string(), out_json, out_len)
1905 }
1906 }
1907}
1908
1909#[derive(Serialize)]
1914struct RecvEventJson {
1915 id: String,
1916 payload_b64: String,
1918 insertion_ts: u64,
1919 shard_id: u16,
1920}
1921
1922#[unsafe(no_mangle)]
1923pub unsafe extern "C" fn net_mesh_recv_shard(
1924 handle: *mut MeshNodeHandle,
1925 shard_id: u16,
1926 limit: u32,
1927 out_json: *mut *mut c_char,
1928 out_len: *mut usize,
1929) -> c_int {
1930 if handle.is_null() || out_json.is_null() || out_len.is_null() {
1931 return NetError::NullPointer.into();
1932 }
1933 let h = unsafe { &*handle };
1934 let _op = match h.guard.try_enter() {
1935 Some(op) => op,
1936 None => return NetError::ShuttingDown.into(),
1937 };
1938 let node = h.inner.clone();
1939 let result = block_on(async move { node.poll_shard(shard_id, None, limit as usize).await });
1940 let result = match result {
1941 Ok(r) => r,
1942 Err(e) => return adapter_err_to_code(&e),
1943 };
1944 let events: Vec<RecvEventJson> = result
1945 .events
1946 .into_iter()
1947 .map(|e| RecvEventJson {
1948 id: e.id,
1949 payload_b64: encode_b64(&e.raw),
1950 insertion_ts: e.insertion_ts,
1951 shard_id: e.shard_id,
1952 })
1953 .collect();
1954 write_json_out(&events, out_json, out_len)
1955}
1956
1957fn encode_b64(bytes: &[u8]) -> String {
1958 const ALPH: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1961 let mut s = String::with_capacity(bytes.len().div_ceil(3) * 4);
1962 let mut i = 0;
1963 while i + 3 <= bytes.len() {
1964 let chunk = &bytes[i..i + 3];
1965 s.push(ALPH[(chunk[0] >> 2) as usize] as char);
1966 s.push(ALPH[(((chunk[0] & 0b11) << 4) | (chunk[1] >> 4)) as usize] as char);
1967 s.push(ALPH[(((chunk[1] & 0b1111) << 2) | (chunk[2] >> 6)) as usize] as char);
1968 s.push(ALPH[(chunk[2] & 0b111111) as usize] as char);
1969 i += 3;
1970 }
1971 let rem = bytes.len() - i;
1972 if rem == 1 {
1973 let b = bytes[i];
1974 s.push(ALPH[(b >> 2) as usize] as char);
1975 s.push(ALPH[((b & 0b11) << 4) as usize] as char);
1976 s.push('=');
1977 s.push('=');
1978 } else if rem == 2 {
1979 let b0 = bytes[i];
1980 let b1 = bytes[i + 1];
1981 s.push(ALPH[(b0 >> 2) as usize] as char);
1982 s.push(ALPH[(((b0 & 0b11) << 4) | (b1 >> 4)) as usize] as char);
1983 s.push(ALPH[((b1 & 0b1111) << 2) as usize] as char);
1984 s.push('=');
1985 }
1986 s
1987}
1988
1989#[derive(Deserialize)]
1994struct ChannelConfigInput {
1995 name: String,
1996 visibility: Option<String>,
1997 reliable: Option<bool>,
1998 require_token: Option<bool>,
1999 token_roots: Option<Vec<String>>,
2005 priority: Option<u8>,
2006 max_rate_pps: Option<u32>,
2007 publish_caps: Option<CapabilityFilterJson>,
2011 subscribe_caps: Option<CapabilityFilterJson>,
2015}
2016
2017fn parse_visibility(s: &str) -> Option<InnerVisibility> {
2018 match s {
2019 "subnet-local" => Some(InnerVisibility::SubnetLocal),
2020 "parent-visible" => Some(InnerVisibility::ParentVisible),
2021 "exported" => Some(InnerVisibility::Exported),
2022 "global" => Some(InnerVisibility::Global),
2023 _ => None,
2024 }
2025}
2026
2027#[unsafe(no_mangle)]
2028pub unsafe extern "C" fn net_mesh_register_channel(
2029 handle: *mut MeshNodeHandle,
2030 config_json: *const c_char,
2031) -> c_int {
2032 if handle.is_null() || config_json.is_null() {
2033 return NetError::NullPointer.into();
2034 }
2035 let h = unsafe { &*handle };
2036 let _op = match h.guard.try_enter() {
2037 Some(op) => op,
2038 None => return NetError::ShuttingDown.into(),
2039 };
2040 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
2041 return NetError::InvalidUtf8.into();
2042 };
2043 let input: ChannelConfigInput = match serde_json::from_str(&s) {
2044 Ok(v) => v,
2045 Err(_) => return NetError::InvalidJson.into(),
2046 };
2047 let name = match InnerChannelName::new(&input.name) {
2048 Ok(n) => n,
2049 Err(_) => return NET_ERR_CHANNEL,
2050 };
2051 let mut cfg = InnerChannelConfig::new(ChannelId::new(name));
2052 if let Some(v) = input.visibility {
2053 let Some(vis) = parse_visibility(&v) else {
2054 return NET_ERR_CHANNEL;
2055 };
2056 cfg = cfg.with_visibility(vis);
2057 }
2058 if let Some(r) = input.reliable {
2059 cfg = cfg.with_reliable(r);
2060 }
2061 if let Some(t) = input.require_token {
2062 cfg = cfg.with_require_token(t);
2063 }
2064 if let Some(roots) = input.token_roots {
2065 let mut parsed = Vec::with_capacity(roots.len());
2066 for hex_id in roots {
2067 let bytes = match hex::decode(&hex_id) {
2068 Ok(b) => b,
2069 Err(_) => return NET_ERR_CHANNEL,
2070 };
2071 let Ok(arr) = <[u8; 32]>::try_from(bytes.as_slice()) else {
2072 return NET_ERR_CHANNEL;
2073 };
2074 parsed.push(EntityId::from_bytes(arr));
2075 }
2076 cfg = cfg.with_token_roots(parsed);
2077 }
2078 if let Some(p) = input.priority {
2079 cfg = cfg.with_priority(p);
2080 }
2081 if let Some(pps) = input.max_rate_pps {
2082 cfg = cfg.with_rate_limit(pps);
2083 }
2084 if let Some(filter_json) = input.publish_caps {
2085 cfg = cfg.with_publish_caps(capability_filter_from_json(filter_json));
2086 }
2087 if let Some(filter_json) = input.subscribe_caps {
2088 cfg = cfg.with_subscribe_caps(capability_filter_from_json(filter_json));
2089 }
2090 h.channel_configs.insert(cfg);
2091 0
2092}
2093
2094#[unsafe(no_mangle)]
2095pub unsafe extern "C" fn net_mesh_subscribe_channel(
2096 handle: *mut MeshNodeHandle,
2097 publisher_node_id: u64,
2098 channel: *const c_char,
2099) -> c_int {
2100 subscribe_or_unsubscribe(handle, publisher_node_id, channel, true)
2101}
2102
2103#[unsafe(no_mangle)]
2104pub unsafe extern "C" fn net_mesh_unsubscribe_channel(
2105 handle: *mut MeshNodeHandle,
2106 publisher_node_id: u64,
2107 channel: *const c_char,
2108) -> c_int {
2109 subscribe_or_unsubscribe(handle, publisher_node_id, channel, false)
2110}
2111
2112#[unsafe(no_mangle)]
2119pub unsafe extern "C" fn net_mesh_subscribe_channel_with_token(
2120 handle: *mut MeshNodeHandle,
2121 publisher_node_id: u64,
2122 channel: *const c_char,
2123 token: *const u8,
2124 token_len: usize,
2125) -> c_int {
2126 if handle.is_null() || channel.is_null() || token.is_null() {
2127 return NetError::NullPointer.into();
2128 }
2129 let h = unsafe { &*handle };
2130 let _op = match h.guard.try_enter() {
2131 Some(op) => op,
2132 None => return NetError::ShuttingDown.into(),
2133 };
2134 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
2135 return NetError::InvalidUtf8.into();
2136 };
2137 let name = match InnerChannelName::new(&s) {
2138 Ok(n) => n,
2139 Err(_) => return NET_ERR_CHANNEL,
2140 };
2141 if token_len > isize::MAX as usize {
2143 return NetError::InvalidJson.into();
2144 }
2145 let slice = unsafe { std::slice::from_raw_parts(token, token_len) };
2146 let parsed = match PermissionToken::from_bytes(slice) {
2147 Ok(t) => t,
2148 Err(e) => return token_err_to_code(&e),
2149 };
2150 let node = h.inner.clone();
2151 match block_on(async move {
2152 node.subscribe_channel_with_token(publisher_node_id, name, parsed)
2153 .await
2154 }) {
2155 Ok(()) => 0,
2156 Err(e) => adapter_err_to_channel_code(&e),
2157 }
2158}
2159
2160fn subscribe_or_unsubscribe(
2161 handle: *mut MeshNodeHandle,
2162 publisher_node_id: u64,
2163 channel: *const c_char,
2164 subscribe: bool,
2165) -> c_int {
2166 if handle.is_null() || channel.is_null() {
2167 return NetError::NullPointer.into();
2168 }
2169 let h = unsafe { &*handle };
2170 let _op = match h.guard.try_enter() {
2171 Some(op) => op,
2172 None => return NetError::ShuttingDown.into(),
2173 };
2174 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
2175 return NetError::InvalidUtf8.into();
2176 };
2177 let name = match InnerChannelName::new(&s) {
2178 Ok(n) => n,
2179 Err(_) => return NET_ERR_CHANNEL,
2180 };
2181 let node = h.inner.clone();
2182 let outcome = if subscribe {
2183 block_on(async move { node.subscribe_channel(publisher_node_id, name).await })
2184 } else {
2185 block_on(async move { node.unsubscribe_channel(publisher_node_id, name).await })
2186 };
2187 match outcome {
2188 Ok(()) => 0,
2189 Err(e) => adapter_err_to_channel_code(&e),
2190 }
2191}
2192
2193fn adapter_err_to_channel_code(err: &AdapterError) -> c_int {
2194 if let AdapterError::Connection(msg) = err {
2195 let prefix = "membership request rejected: ";
2196 if let Some(tail) = msg.strip_prefix(prefix) {
2197 if tail.trim() == "Some(Unauthorized)" {
2198 return NET_ERR_CHANNEL_AUTH;
2199 }
2200 }
2201 }
2202 NET_ERR_CHANNEL
2203}
2204
2205#[derive(Deserialize, Default)]
2206struct PublishConfigInput {
2207 reliability: Option<String>,
2208 on_failure: Option<String>,
2209 max_inflight: Option<u32>,
2210}
2211
2212#[derive(Serialize)]
2213struct PublishReportJson {
2214 attempted: u32,
2215 delivered: u32,
2216 errors: Vec<PublishFailureJson>,
2217}
2218
2219#[derive(Serialize)]
2220struct PublishFailureJson {
2221 node_id: u64,
2222 message: String,
2223}
2224
2225fn to_publish_report_json(r: InnerPublishReport) -> PublishReportJson {
2226 PublishReportJson {
2227 attempted: r.attempted as u32,
2228 delivered: r.delivered as u32,
2229 errors: r
2230 .errors
2231 .into_iter()
2232 .map(|(id, e)| PublishFailureJson {
2233 node_id: id,
2234 message: format!("{}", e),
2235 })
2236 .collect(),
2237 }
2238}
2239
2240#[unsafe(no_mangle)]
2241pub unsafe extern "C" fn net_mesh_publish(
2242 handle: *mut MeshNodeHandle,
2243 channel: *const c_char,
2244 payload: *const u8,
2245 len: usize,
2246 config_json: *const c_char,
2247 out_json: *mut *mut c_char,
2248 out_len: *mut usize,
2249) -> c_int {
2250 if handle.is_null() || channel.is_null() || out_json.is_null() || out_len.is_null() {
2251 return NetError::NullPointer.into();
2252 }
2253 let h = unsafe { &*handle };
2254 let _op = match h.guard.try_enter() {
2255 Some(op) => op,
2256 None => return NetError::ShuttingDown.into(),
2257 };
2258 let Some(ch) = (unsafe { c_str_to_string(channel) }) else {
2259 return NetError::InvalidUtf8.into();
2260 };
2261 let name = match InnerChannelName::new(&ch) {
2262 Ok(n) => n,
2263 Err(_) => return NET_ERR_CHANNEL,
2264 };
2265 let cfg_in: PublishConfigInput = if config_json.is_null() {
2266 PublishConfigInput::default()
2267 } else {
2268 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
2269 return NetError::InvalidUtf8.into();
2270 };
2271 match serde_json::from_str(&s) {
2272 Ok(v) => v,
2273 Err(_) => return NetError::InvalidJson.into(),
2274 }
2275 };
2276 let reliability = match cfg_in.reliability.as_deref() {
2277 None | Some("fire_and_forget") => Reliability::FireAndForget,
2278 Some("reliable") => Reliability::Reliable,
2279 Some(_) => return NET_ERR_CHANNEL,
2280 };
2281 let on_failure = match cfg_in.on_failure.as_deref() {
2282 None | Some("best_effort") => InnerOnFailure::BestEffort,
2283 Some("fail_fast") => InnerOnFailure::FailFast,
2284 Some("collect") => InnerOnFailure::Collect,
2285 Some(_) => return NET_ERR_CHANNEL,
2286 };
2287 let max_inflight = cfg_in.max_inflight.unwrap_or(32) as usize;
2288 let publish_cfg = InnerPublishConfig {
2289 reliability,
2290 on_failure,
2291 max_inflight,
2292 };
2293 let publisher = ChannelPublisher::new(name, publish_cfg);
2294
2295 let bytes = if len == 0 {
2297 Bytes::new()
2298 } else if payload.is_null() {
2299 return NetError::NullPointer.into();
2300 } else if len > isize::MAX as usize {
2301 return NetError::InvalidJson.into();
2303 } else {
2304 Bytes::copy_from_slice(unsafe { std::slice::from_raw_parts(payload, len) })
2305 };
2306
2307 let node = h.inner.clone();
2308 match block_on(async move { node.publish(&publisher, bytes).await }) {
2309 Ok(report) => {
2310 let js = to_publish_report_json(report);
2311 write_json_out(&js, out_json, out_len)
2312 }
2313 Err(e) => adapter_err_to_channel_code(&e),
2314 }
2315}
2316
2317pub struct IdentityHandle {
2331 keypair: ManuallyDrop<Arc<EntityKeypair>>,
2332 cache: ManuallyDrop<Arc<TokenCache>>,
2333 guard: HandleGuard,
2334}
2335
2336fn alloc_bytes(src: &[u8], out_ptr: *mut *mut u8, out_len: *mut usize) -> c_int {
2350 if out_ptr.is_null() || out_len.is_null() {
2351 return NetError::NullPointer.into();
2352 }
2353 let len = src.len();
2354 if len == 0 {
2355 unsafe {
2356 *out_ptr = std::ptr::null_mut();
2357 *out_len = 0;
2358 }
2359 return 0;
2360 }
2361 let layout = match std::alloc::Layout::array::<u8>(len) {
2370 Ok(l) => l,
2371 Err(_) => return NET_ERR_IDENTITY,
2377 };
2378 let ptr = unsafe { std::alloc::alloc(layout) };
2379 if ptr.is_null() {
2380 std::alloc::handle_alloc_error(layout);
2381 }
2382 unsafe {
2383 std::ptr::copy_nonoverlapping(src.as_ptr(), ptr, len);
2384 *out_ptr = ptr;
2385 *out_len = len;
2386 }
2387 0
2388}
2389
2390#[unsafe(no_mangle)]
2405pub unsafe extern "C" fn net_free_bytes(ptr: *mut u8, len: usize) {
2406 if ptr.is_null() || len == 0 {
2407 return;
2408 }
2409 let layout = match std::alloc::Layout::array::<u8>(len) {
2415 Ok(l) => l,
2416 Err(_) => return,
2417 };
2418 unsafe {
2419 std::alloc::dealloc(ptr, layout);
2420 }
2421}
2422
2423fn entity_id_from_bytes(bytes: *const u8, len: usize) -> Option<EntityId> {
2424 if bytes.is_null() || len != 32 {
2425 return None;
2426 }
2427 let slice = unsafe { std::slice::from_raw_parts(bytes, 32) };
2428 let mut arr = [0u8; 32];
2429 arr.copy_from_slice(slice);
2430 Some(EntityId::from_bytes(arr))
2431}
2432
2433fn parse_scope_list(raw: &str) -> Option<TokenScope> {
2434 let values: Vec<String> = serde_json::from_str(raw).ok()?;
2438 let mut acc = TokenScope::NONE;
2439 for s in &values {
2440 acc = acc.union(match s.as_str() {
2441 "publish" => TokenScope::PUBLISH,
2442 "subscribe" => TokenScope::SUBSCRIBE,
2443 "admin" => TokenScope::ADMIN,
2444 "delegate" => TokenScope::DELEGATE,
2445 _ => return None,
2446 });
2447 }
2448 Some(acc)
2449}
2450
2451fn scope_to_strings(scope: TokenScope) -> Vec<&'static str> {
2452 let mut out = Vec::new();
2453 if scope.contains(TokenScope::PUBLISH) {
2454 out.push("publish");
2455 }
2456 if scope.contains(TokenScope::SUBSCRIBE) {
2457 out.push("subscribe");
2458 }
2459 if scope.contains(TokenScope::ADMIN) {
2460 out.push("admin");
2461 }
2462 if scope.contains(TokenScope::DELEGATE) {
2463 out.push("delegate");
2464 }
2465 out
2466}
2467
2468fn channel_name_to_hash(channel: &str) -> Option<ChannelHash> {
2469 InnerChannelName::new(channel).ok().map(|n| n.hash())
2470}
2471
2472#[unsafe(no_mangle)]
2475pub unsafe extern "C" fn net_identity_generate(out_handle: *mut *mut IdentityHandle) -> c_int {
2476 if out_handle.is_null() {
2477 return NetError::NullPointer.into();
2478 }
2479 let handle = Box::new(IdentityHandle {
2480 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::generate())),
2481 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2482 guard: HandleGuard::new(),
2483 });
2484 unsafe {
2485 *out_handle = Box::into_raw(handle);
2486 }
2487 0
2488}
2489
2490#[unsafe(no_mangle)]
2494pub unsafe extern "C" fn net_identity_from_seed(
2495 seed: *const u8,
2496 seed_len: usize,
2497 out_handle: *mut *mut IdentityHandle,
2498) -> c_int {
2499 if seed.is_null() || out_handle.is_null() {
2500 return NetError::NullPointer.into();
2501 }
2502 if seed_len != 32 {
2503 return NET_ERR_IDENTITY;
2504 }
2505 let mut arr = [0u8; 32];
2506 arr.copy_from_slice(unsafe { std::slice::from_raw_parts(seed, 32) });
2507 let handle = Box::new(IdentityHandle {
2508 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::from_bytes(arr))),
2509 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2510 guard: HandleGuard::new(),
2511 });
2512 unsafe {
2513 *out_handle = Box::into_raw(handle);
2514 }
2515 0
2516}
2517
2518#[unsafe(no_mangle)]
2519pub unsafe extern "C" fn net_identity_free(handle: *mut IdentityHandle) {
2520 if handle.is_null() {
2521 return;
2522 }
2523 let h: &IdentityHandle = unsafe { &*handle };
2525 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
2526 unsafe {
2528 let mh = &mut *handle;
2529 let kp = ManuallyDrop::take(&mut mh.keypair);
2530 let cache = ManuallyDrop::take(&mut mh.cache);
2531 drop(kp);
2532 drop(cache);
2533 }
2534 } else {
2535 tracing::warn!(
2536 "net_identity_free: in-flight ops did not drain within deadline; \
2537 leaking inner to avoid use-after-free"
2538 );
2539 }
2540}
2541
2542#[unsafe(no_mangle)]
2545pub unsafe extern "C" fn net_identity_to_seed(handle: *mut IdentityHandle, out: *mut u8) -> c_int {
2546 if handle.is_null() || out.is_null() {
2547 return NetError::NullPointer.into();
2548 }
2549 let h = unsafe { &*handle };
2550 let _op = match h.guard.try_enter() {
2551 Some(op) => op,
2552 None => return NetError::ShuttingDown.into(),
2553 };
2554 let seed = h.keypair.secret_bytes();
2555 unsafe {
2556 std::ptr::copy_nonoverlapping(seed.as_ptr(), out, 32);
2557 }
2558 0
2559}
2560
2561#[unsafe(no_mangle)]
2563pub unsafe extern "C" fn net_identity_entity_id(
2564 handle: *mut IdentityHandle,
2565 out: *mut u8,
2566) -> c_int {
2567 if handle.is_null() || out.is_null() {
2568 return NetError::NullPointer.into();
2569 }
2570 let h = unsafe { &*handle };
2571 let _op = match h.guard.try_enter() {
2572 Some(op) => op,
2573 None => return NetError::ShuttingDown.into(),
2574 };
2575 let id = h.keypair.entity_id().as_bytes();
2576 unsafe {
2577 std::ptr::copy_nonoverlapping(id.as_ptr(), out, 32);
2578 }
2579 0
2580}
2581
2582#[unsafe(no_mangle)]
2583pub unsafe extern "C" fn net_identity_node_id(handle: *mut IdentityHandle) -> u64 {
2584 if handle.is_null() {
2585 return 0;
2586 }
2587 let h = unsafe { &*handle };
2588 let _op = match h.guard.try_enter() {
2590 Some(op) => op,
2591 None => return 0,
2592 };
2593 h.keypair.node_id()
2594}
2595
2596#[unsafe(no_mangle)]
2597pub unsafe extern "C" fn net_identity_origin_hash(handle: *mut IdentityHandle) -> u64 {
2598 if handle.is_null() {
2599 return 0;
2600 }
2601 let h = unsafe { &*handle };
2602 let _op = match h.guard.try_enter() {
2604 Some(op) => op,
2605 None => return 0,
2606 };
2607 h.keypair.origin_hash()
2608}
2609
2610#[unsafe(no_mangle)]
2613pub unsafe extern "C" fn net_identity_sign(
2614 handle: *mut IdentityHandle,
2615 msg: *const u8,
2616 len: usize,
2617 out_sig: *mut u8,
2618) -> c_int {
2619 if handle.is_null() || out_sig.is_null() {
2620 return NetError::NullPointer.into();
2621 }
2622 if len > 0 && msg.is_null() {
2623 return NetError::NullPointer.into();
2624 }
2625 let h = unsafe { &*handle };
2626 let _op = match h.guard.try_enter() {
2627 Some(op) => op,
2628 None => return NetError::ShuttingDown.into(),
2629 };
2630 let slice = if len == 0 {
2631 &[][..]
2632 } else if len > isize::MAX as usize {
2633 return NetError::InvalidJson.into();
2635 } else {
2636 unsafe { std::slice::from_raw_parts(msg, len) }
2637 };
2638 let sig = h.keypair.sign(slice).to_bytes();
2639 unsafe {
2640 std::ptr::copy_nonoverlapping(sig.as_ptr(), out_sig, 64);
2641 }
2642 0
2643}
2644
2645#[unsafe(no_mangle)]
2648pub unsafe extern "C" fn net_identity_issue_token(
2649 signer: *mut IdentityHandle,
2650 subject: *const u8,
2651 subject_len: usize,
2652 scope_json: *const c_char,
2653 channel: *const c_char,
2654 ttl_seconds: u32,
2655 delegation_depth: u8,
2656 out_token: *mut *mut u8,
2657 out_token_len: *mut usize,
2658) -> c_int {
2659 if signer.is_null() || out_token.is_null() || out_token_len.is_null() {
2660 return NetError::NullPointer.into();
2661 }
2662 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
2663 return NET_ERR_IDENTITY;
2664 };
2665 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
2666 return NetError::InvalidUtf8.into();
2667 };
2668 let Some(scope) = parse_scope_list(&scope_s) else {
2669 return NET_ERR_IDENTITY;
2670 };
2671 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
2672 return NetError::InvalidUtf8.into();
2673 };
2674 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
2675 return NET_ERR_IDENTITY;
2676 };
2677 let h = unsafe { &*signer };
2678 let _op = match h.guard.try_enter() {
2682 Some(op) => op,
2683 None => return NetError::ShuttingDown.into(),
2684 };
2685 let token = match PermissionToken::try_issue(
2691 &h.keypair,
2692 subject_id,
2693 scope,
2694 channel_hash,
2695 u64::from(ttl_seconds),
2696 delegation_depth,
2697 ) {
2698 Ok(t) => t,
2699 Err(e) => return token_err_to_code(&e),
2700 };
2701 alloc_bytes(&token.to_bytes(), out_token, out_token_len)
2702}
2703
2704#[unsafe(no_mangle)]
2708pub unsafe extern "C" fn net_identity_install_token(
2709 handle: *mut IdentityHandle,
2710 token: *const u8,
2711 len: usize,
2712) -> c_int {
2713 if handle.is_null() || token.is_null() {
2714 return NetError::NullPointer.into();
2715 }
2716 if len > isize::MAX as usize {
2718 return NetError::InvalidJson.into();
2719 }
2720 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2721 let parsed = match PermissionToken::from_bytes(slice) {
2722 Ok(t) => t,
2723 Err(e) => return token_err_to_code(&e),
2724 };
2725 let h = unsafe { &*handle };
2726 let _op = match h.guard.try_enter() {
2727 Some(op) => op,
2728 None => return NetError::ShuttingDown.into(),
2729 };
2730 match h.cache.insert(parsed) {
2731 Ok(()) => 0,
2732 Err(e) => token_err_to_code(&e),
2733 }
2734}
2735
2736#[unsafe(no_mangle)]
2740pub unsafe extern "C" fn net_identity_lookup_token(
2741 handle: *mut IdentityHandle,
2742 subject: *const u8,
2743 subject_len: usize,
2744 channel: *const c_char,
2745 out_token: *mut *mut u8,
2746 out_token_len: *mut usize,
2747) -> c_int {
2748 if handle.is_null() || out_token.is_null() || out_token_len.is_null() {
2749 return NetError::NullPointer.into();
2750 }
2751 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
2752 return NET_ERR_IDENTITY;
2753 };
2754 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
2755 return NetError::InvalidUtf8.into();
2756 };
2757 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
2758 return NET_ERR_IDENTITY;
2759 };
2760 let h = unsafe { &*handle };
2761 let _op = match h.guard.try_enter() {
2762 Some(op) => op,
2763 None => return NetError::ShuttingDown.into(),
2764 };
2765 match h.cache.get(&subject_id, channel_hash) {
2766 Some(token) => alloc_bytes(&token.to_bytes(), out_token, out_token_len),
2767 None => {
2768 unsafe {
2769 *out_token = std::ptr::null_mut();
2770 *out_token_len = 0;
2771 }
2772 0
2773 }
2774 }
2775}
2776
2777#[unsafe(no_mangle)]
2778pub unsafe extern "C" fn net_identity_token_cache_len(handle: *mut IdentityHandle) -> u32 {
2779 if handle.is_null() {
2780 return 0;
2781 }
2782 let h = unsafe { &*handle };
2783 let _op = match h.guard.try_enter() {
2785 Some(op) => op,
2786 None => return 0,
2787 };
2788 h.cache.len() as u32
2789}
2790
2791#[derive(Serialize)]
2796struct ParsedTokenJson {
2797 issuer_hex: String,
2798 subject_hex: String,
2799 scope: Vec<&'static str>,
2800 channel_hash: ChannelHash,
2801 not_before: u64,
2802 not_after: u64,
2803 delegation_depth: u8,
2804 nonce: u64,
2805 signature_hex: String,
2806}
2807
2808#[unsafe(no_mangle)]
2813pub unsafe extern "C" fn net_parse_token(
2814 token: *const u8,
2815 len: usize,
2816 out_json: *mut *mut c_char,
2817 out_len: *mut usize,
2818) -> c_int {
2819 if token.is_null() || out_json.is_null() || out_len.is_null() {
2820 return NetError::NullPointer.into();
2821 }
2822 if len > isize::MAX as usize {
2824 return NetError::InvalidJson.into();
2825 }
2826 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2827 let parsed = match PermissionToken::from_bytes(slice) {
2828 Ok(t) => t,
2829 Err(e) => return token_err_to_code(&e),
2830 };
2831 let out = ParsedTokenJson {
2832 issuer_hex: hex::encode(parsed.issuer.as_bytes()),
2833 subject_hex: hex::encode(parsed.subject.as_bytes()),
2834 scope: scope_to_strings(parsed.scope),
2835 channel_hash: parsed.channel_hash,
2836 not_before: parsed.not_before,
2837 not_after: parsed.not_after,
2838 delegation_depth: parsed.delegation_depth,
2839 nonce: parsed.nonce,
2840 signature_hex: hex::encode(parsed.signature),
2841 };
2842 write_json_out(&out, out_json, out_len)
2843}
2844
2845#[unsafe(no_mangle)]
2849pub unsafe extern "C" fn net_verify_token(
2850 token: *const u8,
2851 len: usize,
2852 out_ok: *mut c_int,
2853) -> c_int {
2854 if token.is_null() || out_ok.is_null() {
2855 return NetError::NullPointer.into();
2856 }
2857 if len > isize::MAX as usize {
2859 return NetError::InvalidJson.into();
2860 }
2861 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2862 let parsed = match PermissionToken::from_bytes(slice) {
2863 Ok(t) => t,
2864 Err(e) => return token_err_to_code(&e),
2865 };
2866 unsafe {
2867 *out_ok = if parsed.verify().is_ok() { 1 } else { 0 };
2868 }
2869 0
2870}
2871
2872#[unsafe(no_mangle)]
2877pub unsafe extern "C" fn net_token_is_expired(
2878 token: *const u8,
2879 len: usize,
2880 out_expired: *mut c_int,
2881) -> c_int {
2882 if token.is_null() || out_expired.is_null() {
2883 return NetError::NullPointer.into();
2884 }
2885 if len > isize::MAX as usize {
2887 return NetError::InvalidJson.into();
2888 }
2889 let slice = unsafe { std::slice::from_raw_parts(token, len) };
2890 let parsed = match PermissionToken::from_bytes(slice) {
2891 Ok(t) => t,
2892 Err(e) => return token_err_to_code(&e),
2893 };
2894 unsafe {
2895 *out_expired = if parsed.is_expired() { 1 } else { 0 };
2896 }
2897 0
2898}
2899
2900#[unsafe(no_mangle)]
2903pub unsafe extern "C" fn net_delegate_token(
2904 signer: *mut IdentityHandle,
2905 parent: *const u8,
2906 parent_len: usize,
2907 new_subject: *const u8,
2908 new_subject_len: usize,
2909 restricted_scope_json: *const c_char,
2910 out_token: *mut *mut u8,
2911 out_token_len: *mut usize,
2912) -> c_int {
2913 if signer.is_null()
2914 || parent.is_null()
2915 || new_subject.is_null()
2916 || restricted_scope_json.is_null()
2917 || out_token.is_null()
2918 || out_token_len.is_null()
2919 {
2920 return NetError::NullPointer.into();
2921 }
2922 if parent_len > isize::MAX as usize {
2924 return NetError::InvalidJson.into();
2925 }
2926 let parent_slice = unsafe { std::slice::from_raw_parts(parent, parent_len) };
2927 let parent_tok = match PermissionToken::from_bytes(parent_slice) {
2928 Ok(t) => t,
2929 Err(e) => return token_err_to_code(&e),
2930 };
2931 let Some(subject_id) = entity_id_from_bytes(new_subject, new_subject_len) else {
2932 return NET_ERR_IDENTITY;
2933 };
2934 let Some(scope_s) = (unsafe { c_str_to_string(restricted_scope_json) }) else {
2935 return NetError::InvalidUtf8.into();
2936 };
2937 let Some(scope) = parse_scope_list(&scope_s) else {
2938 return NET_ERR_IDENTITY;
2939 };
2940 let h = unsafe { &*signer };
2941 let _op = match h.guard.try_enter() {
2945 Some(op) => op,
2946 None => return NetError::ShuttingDown.into(),
2947 };
2948 match parent_tok.delegate(&h.keypair, subject_id, scope) {
2949 Ok(child) => alloc_bytes(&child.to_bytes(), out_token, out_token_len),
2950 Err(e) => token_err_to_code(&e),
2951 }
2952}
2953
2954#[unsafe(no_mangle)]
2959pub unsafe extern "C" fn net_channel_hash(channel: *const c_char, out_hash: *mut u64) -> c_int {
2960 if channel.is_null() || out_hash.is_null() {
2961 return NetError::NullPointer.into();
2962 }
2963 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
2964 return NetError::InvalidUtf8.into();
2965 };
2966 let Some(hash) = channel_name_to_hash(&s) else {
2967 return NET_ERR_IDENTITY;
2968 };
2969 unsafe {
2970 *out_hash = hash;
2971 }
2972 0
2973}
2974
2975use crate::adapter::net::behavior::capability::{
2982 AcceleratorInfo, AcceleratorType, CapabilityFilter, CapabilitySet, GpuInfo, GpuVendor,
2983 HardwareCapabilities, Modality, ModelCapability, ResourceLimits, SoftwareCapabilities,
2984 ToolCapability, TAG_SCOPE_REGION_PREFIX, TAG_SCOPE_SUBNET_LOCAL, TAG_SCOPE_TENANT_PREFIX,
2985};
2986
2987fn parse_gpu_vendor_cap(s: &str) -> GpuVendor {
2990 match s.to_ascii_lowercase().as_str() {
2991 "nvidia" => GpuVendor::Nvidia,
2992 "amd" => GpuVendor::Amd,
2993 "intel" => GpuVendor::Intel,
2994 "apple" => GpuVendor::Apple,
2995 "qualcomm" => GpuVendor::Qualcomm,
2996 _ => GpuVendor::Unknown,
2997 }
2998}
2999
3000fn gpu_vendor_to_string_cap(v: GpuVendor) -> &'static str {
3001 match v {
3002 GpuVendor::Nvidia => "nvidia",
3003 GpuVendor::Amd => "amd",
3004 GpuVendor::Intel => "intel",
3005 GpuVendor::Apple => "apple",
3006 GpuVendor::Qualcomm => "qualcomm",
3007 GpuVendor::Unknown => "unknown",
3008 }
3009}
3010
3011fn parse_modality_cap(s: &str) -> Option<Modality> {
3012 match s.to_ascii_lowercase().as_str() {
3013 "text" => Some(Modality::Text),
3014 "image" => Some(Modality::Image),
3015 "audio" => Some(Modality::Audio),
3016 "video" => Some(Modality::Video),
3017 "code" => Some(Modality::Code),
3018 "embedding" => Some(Modality::Embedding),
3019 "tool-use" | "tool_use" | "tooluse" => Some(Modality::ToolUse),
3020 _ => None,
3029 }
3030}
3031
3032fn parse_accelerator_type_cap(s: &str) -> AcceleratorType {
3033 match s.to_ascii_lowercase().as_str() {
3034 "tpu" => AcceleratorType::Tpu,
3035 "npu" => AcceleratorType::Npu,
3036 "fpga" => AcceleratorType::Fpga,
3037 "asic" => AcceleratorType::Asic,
3038 "dsp" => AcceleratorType::Dsp,
3039 _ => AcceleratorType::Unknown,
3040 }
3041}
3042
3043#[derive(Deserialize, Default)]
3046struct CapabilitySetJson {
3047 #[serde(default)]
3048 hardware: Option<HardwareJson>,
3049 #[serde(default)]
3050 software: Option<SoftwareJson>,
3051 #[serde(default)]
3052 models: Vec<ModelJson>,
3053 #[serde(default)]
3054 tools: Vec<ToolJson>,
3055 #[serde(default)]
3056 tags: Vec<String>,
3057 #[serde(default)]
3058 limits: Option<LimitsJson>,
3059}
3060
3061#[derive(Deserialize, Default)]
3062struct HardwareJson {
3063 cpu_cores: Option<u32>,
3064 cpu_threads: Option<u32>,
3065 memory_gb: Option<u32>,
3066 gpu: Option<GpuJson>,
3067 #[serde(default)]
3068 additional_gpus: Vec<GpuJson>,
3069 storage_gb: Option<u64>,
3070 network_gbps: Option<u32>,
3071 #[serde(default)]
3072 accelerators: Vec<AcceleratorJson>,
3073}
3074
3075#[derive(Deserialize)]
3076struct GpuJson {
3077 vendor: Option<String>,
3078 #[serde(default)]
3079 model: String,
3080 #[serde(default)]
3081 vram_gb: u32,
3082 compute_units: Option<u32>,
3083 tensor_cores: Option<u32>,
3084 fp16_tflops_x10: Option<u32>,
3085}
3086
3087#[derive(Deserialize)]
3088struct AcceleratorJson {
3089 #[serde(default)]
3090 kind: String,
3091 #[serde(default)]
3092 model: String,
3093 memory_gb: Option<u32>,
3094 tops_x10: Option<u32>,
3095}
3096
3097#[derive(Deserialize, Default)]
3098struct SoftwareJson {
3099 os: Option<String>,
3100 os_version: Option<String>,
3101 #[serde(default)]
3102 runtimes: Vec<Vec<String>>,
3103 #[serde(default)]
3104 frameworks: Vec<Vec<String>>,
3105 cuda_version: Option<String>,
3106 #[serde(default)]
3107 drivers: Vec<Vec<String>>,
3108}
3109
3110#[derive(Deserialize)]
3111struct ModelJson {
3112 #[serde(default)]
3113 model_id: String,
3114 #[serde(default)]
3115 family: String,
3116 parameters_b_x10: Option<u32>,
3117 context_length: Option<u32>,
3118 quantization: Option<String>,
3119 #[serde(default)]
3120 modalities: Vec<String>,
3121 tokens_per_sec: Option<u32>,
3122 loaded: Option<bool>,
3123}
3124
3125#[derive(Deserialize)]
3126struct ToolJson {
3127 #[serde(default)]
3128 tool_id: String,
3129 #[serde(default)]
3130 name: String,
3131 version: Option<String>,
3132 input_schema: Option<String>,
3133 output_schema: Option<String>,
3134 #[serde(default)]
3135 requires: Vec<String>,
3136 estimated_time_ms: Option<u32>,
3137 stateless: Option<bool>,
3138}
3139
3140#[derive(Deserialize, Default)]
3141struct LimitsJson {
3142 max_concurrent_requests: Option<u32>,
3143 max_tokens_per_request: Option<u32>,
3144 rate_limit_rpm: Option<u32>,
3145 max_batch_size: Option<u32>,
3146 max_input_bytes: Option<u32>,
3147 max_output_bytes: Option<u32>,
3148}
3149
3150#[derive(Deserialize, Default)]
3151struct CapabilityFilterJson {
3152 #[serde(default)]
3153 require_tags: Vec<String>,
3154 #[serde(default)]
3155 require_models: Vec<String>,
3156 #[serde(default)]
3157 require_tools: Vec<String>,
3158 min_memory_gb: Option<u32>,
3159 require_gpu: Option<bool>,
3160 gpu_vendor: Option<String>,
3161 min_vram_gb: Option<u32>,
3162 min_context_length: Option<u32>,
3163 #[serde(default)]
3164 require_modalities: Vec<String>,
3165}
3166
3167fn pair_vec(xs: Vec<Vec<String>>) -> Vec<(String, String)> {
3170 xs.into_iter()
3171 .filter_map(|mut p| {
3172 if p.len() >= 2 {
3173 Some((std::mem::take(&mut p[0]), std::mem::take(&mut p[1])))
3174 } else {
3175 None
3176 }
3177 })
3178 .collect()
3179}
3180
3181#[inline]
3187fn saturating_u16_cap(v: u32) -> u16 {
3188 v.min(u16::MAX as u32) as u16
3189}
3190
3191fn gpu_info_from_json(g: GpuJson) -> GpuInfo {
3192 let vendor = g
3193 .vendor
3194 .as_deref()
3195 .map(parse_gpu_vendor_cap)
3196 .unwrap_or(GpuVendor::Unknown);
3197 let mut info = GpuInfo::new(vendor, g.model, g.vram_gb);
3198 if let Some(cu) = g.compute_units {
3199 info = info.with_compute_units(saturating_u16_cap(cu));
3200 }
3201 if let Some(tc) = g.tensor_cores {
3202 info = info.with_tensor_cores(saturating_u16_cap(tc));
3203 }
3204 if let Some(tf) = g.fp16_tflops_x10 {
3205 let tf_capped = saturating_u16_cap(tf);
3219 info = info.with_fp16_tflops(tf_capped as f32 / 10.0);
3220 }
3221 info
3222}
3223
3224fn accelerator_from_json(a: AcceleratorJson) -> AcceleratorInfo {
3225 AcceleratorInfo {
3226 accel_type: parse_accelerator_type_cap(&a.kind),
3227 model: a.model,
3228 memory_gb: a.memory_gb.unwrap_or(0),
3229 tops_x10: a.tops_x10.map(saturating_u16_cap).unwrap_or(0),
3230 }
3231}
3232
3233fn hardware_from_json(h: HardwareJson) -> HardwareCapabilities {
3234 let mut hw = HardwareCapabilities::new();
3235 match (h.cpu_cores, h.cpu_threads) {
3236 (Some(c), Some(t)) => hw = hw.with_cpu(saturating_u16_cap(c), saturating_u16_cap(t)),
3237 (Some(c), None) => {
3238 let c16 = saturating_u16_cap(c);
3239 hw = hw.with_cpu(c16, c16);
3240 }
3241 _ => {}
3242 }
3243 if let Some(mb) = h.memory_gb {
3244 hw = hw.with_memory(mb);
3245 }
3246 if let Some(g) = h.gpu {
3247 hw = hw.with_gpu(gpu_info_from_json(g));
3248 }
3249 for g in h.additional_gpus {
3250 hw = hw.add_gpu(gpu_info_from_json(g));
3251 }
3252 if let Some(mb) = h.storage_gb {
3253 hw = hw.with_storage(mb);
3254 }
3255 if let Some(gbps) = h.network_gbps {
3256 hw = hw.with_network(gbps);
3257 }
3258 for a in h.accelerators {
3259 hw = hw.add_accelerator(accelerator_from_json(a));
3260 }
3261 hw
3262}
3263
3264fn software_from_json(s: SoftwareJson) -> SoftwareCapabilities {
3265 let mut sw = SoftwareCapabilities::new()
3266 .with_os(s.os.unwrap_or_default(), s.os_version.unwrap_or_default());
3267 for (k, v) in pair_vec(s.runtimes) {
3268 sw = sw.add_runtime(k, v);
3269 }
3270 for (k, v) in pair_vec(s.frameworks) {
3271 sw = sw.add_framework(k, v);
3272 }
3273 if let Some(c) = s.cuda_version {
3274 sw = sw.with_cuda(c);
3275 }
3276 sw.drivers = pair_vec(s.drivers);
3277 sw
3278}
3279
3280fn model_from_json(m: ModelJson) -> ModelCapability {
3281 let mut mc = ModelCapability::new(m.model_id, m.family);
3282 if let Some(p) = m.parameters_b_x10 {
3283 mc.parameters_b_x10 = p;
3284 }
3285 if let Some(c) = m.context_length {
3286 mc = mc.with_context_length(c);
3287 }
3288 if let Some(q) = m.quantization {
3289 mc = mc.with_quantization(q);
3290 }
3291 for modality in m.modalities {
3292 match parse_modality_cap(&modality) {
3293 Some(parsed) => mc = mc.add_modality(parsed),
3294 None => {
3295 tracing::warn!(
3296 modality = %modality,
3297 "announce_capabilities: unknown modality string (typo?), \
3298 skipping rather than the pre-fix silent fallback to Text — \
3299 advertising a Text capability the node doesn't actually \
3300 have produced wrong scheduling decisions on the receiver",
3301 );
3302 }
3303 }
3304 }
3305 if let Some(t) = m.tokens_per_sec {
3306 mc = mc.with_tokens_per_sec(t);
3307 }
3308 if let Some(l) = m.loaded {
3309 mc = mc.with_loaded(l);
3310 }
3311 mc
3312}
3313
3314fn tool_from_json(t: ToolJson) -> ToolCapability {
3315 let mut tc = ToolCapability::new(t.tool_id, t.name);
3316 if let Some(v) = t.version {
3317 tc = tc.with_version(v);
3318 }
3319 if let Some(s) = t.input_schema {
3320 tc = tc.with_input_schema(s);
3321 }
3322 if let Some(s) = t.output_schema {
3323 tc = tc.with_output_schema(s);
3324 }
3325 for r in t.requires {
3326 tc = tc.requires(r);
3327 }
3328 if let Some(ms) = t.estimated_time_ms {
3329 tc = tc.with_estimated_time(ms);
3330 }
3331 if let Some(st) = t.stateless {
3332 tc = tc.with_stateless(st);
3333 }
3334 tc
3335}
3336
3337fn limits_from_json(l: LimitsJson) -> ResourceLimits {
3338 let mut rl = ResourceLimits::new();
3339 if let Some(n) = l.max_concurrent_requests {
3340 rl = rl.with_max_concurrent(n);
3341 }
3342 if let Some(n) = l.max_tokens_per_request {
3343 rl = rl.with_max_tokens(n);
3344 }
3345 if let Some(n) = l.rate_limit_rpm {
3346 rl = rl.with_rate_limit(n);
3347 }
3348 if let Some(n) = l.max_batch_size {
3349 rl = rl.with_max_batch(n);
3350 }
3351 if let Some(n) = l.max_input_bytes {
3352 rl.max_input_bytes = n;
3353 }
3354 if let Some(n) = l.max_output_bytes {
3355 rl.max_output_bytes = n;
3356 }
3357 rl
3358}
3359
3360fn capability_set_from_json(caps: CapabilitySetJson) -> CapabilitySet {
3361 let mut cs = CapabilitySet::new();
3362 if let Some(h) = caps.hardware {
3363 cs = cs.with_hardware(hardware_from_json(h));
3364 }
3365 if let Some(s) = caps.software {
3366 cs = cs.with_software(software_from_json(s));
3367 }
3368 for m in caps.models {
3369 cs = cs.add_model(model_from_json(m));
3370 }
3371 for t in caps.tools {
3372 cs = cs.add_tool(tool_from_json(t));
3373 }
3374 for tag in caps.tags {
3382 if tag == TAG_SCOPE_SUBNET_LOCAL {
3383 cs = cs.with_subnet_local_scope();
3384 } else if let Some(id) = tag.strip_prefix(TAG_SCOPE_TENANT_PREFIX) {
3385 cs = cs.with_tenant_scope(id);
3386 } else if let Some(name) = tag.strip_prefix(TAG_SCOPE_REGION_PREFIX) {
3387 cs = cs.with_region_scope(name);
3388 } else {
3389 cs = cs.add_tag(tag);
3390 }
3391 }
3392 if let Some(l) = caps.limits {
3393 cs = cs.with_limits(limits_from_json(l));
3394 }
3395 cs
3396}
3397
3398fn capability_filter_from_json(f: CapabilityFilterJson) -> CapabilityFilter {
3399 let mut cf = CapabilityFilter::new();
3400 for t in f.require_tags {
3401 cf = cf.require_tag(t);
3402 }
3403 for m in f.require_models {
3404 cf = cf.require_model(m);
3405 }
3406 for t in f.require_tools {
3407 cf = cf.require_tool(t);
3408 }
3409 if let Some(mb) = f.min_memory_gb {
3410 cf = cf.with_min_memory(mb);
3411 }
3412 if f.require_gpu.unwrap_or(false) {
3413 cf = cf.require_gpu();
3414 }
3415 if let Some(v) = f.gpu_vendor {
3416 cf = cf.with_gpu_vendor(parse_gpu_vendor_cap(&v));
3417 }
3418 if let Some(mb) = f.min_vram_gb {
3419 cf = cf.with_min_vram(mb);
3420 }
3421 if let Some(n) = f.min_context_length {
3422 cf = cf.with_min_context(n);
3423 }
3424 for m in f.require_modalities {
3425 match parse_modality_cap(&m) {
3426 Some(parsed) => cf = cf.require_modality(parsed),
3427 None => {
3428 tracing::warn!(
3441 modality = %m,
3442 "find_nodes: unknown modality string in require_modalities \
3443 filter (typo?), dropping the constraint; the resulting \
3444 filter is too permissive — pre-fix it was silently \
3445 re-interpreted as `require Text`, which returned the \
3446 wrong nodes",
3447 );
3448 }
3449 }
3450 }
3451 cf
3452}
3453
3454pub(crate) const NET_ERR_CAPABILITY: c_int = -128;
3457
3458#[unsafe(no_mangle)]
3465pub unsafe extern "C" fn net_mesh_announce_capabilities(
3466 handle: *mut MeshNodeHandle,
3467 caps_json: *const c_char,
3468) -> c_int {
3469 if handle.is_null() || caps_json.is_null() {
3470 return NetError::NullPointer.into();
3471 }
3472 let h = unsafe { &*handle };
3473 let _op = match h.guard.try_enter() {
3474 Some(op) => op,
3475 None => return NetError::ShuttingDown.into(),
3476 };
3477 let Some(s) = (unsafe { c_str_to_string(caps_json) }) else {
3478 return NetError::InvalidUtf8.into();
3479 };
3480 let parsed: CapabilitySetJson = match serde_json::from_str(&s) {
3481 Ok(v) => v,
3482 Err(_) => return NetError::InvalidJson.into(),
3483 };
3484 let caps = capability_set_from_json(parsed);
3485 let node = h.inner.clone();
3486 match block_on(async move { node.announce_capabilities(caps).await }) {
3487 Ok(()) => 0,
3488 Err(_) => NET_ERR_CAPABILITY,
3489 }
3490}
3491
3492#[unsafe(no_mangle)]
3495pub unsafe extern "C" fn net_mesh_find_nodes(
3496 handle: *mut MeshNodeHandle,
3497 filter_json: *const c_char,
3498 out_json: *mut *mut c_char,
3499 out_len: *mut usize,
3500) -> c_int {
3501 if handle.is_null() || filter_json.is_null() || out_json.is_null() || out_len.is_null() {
3502 return NetError::NullPointer.into();
3503 }
3504 let h = unsafe { &*handle };
3505 let _op = match h.guard.try_enter() {
3506 Some(op) => op,
3507 None => return NetError::ShuttingDown.into(),
3508 };
3509 let Some(s) = (unsafe { c_str_to_string(filter_json) }) else {
3510 return NetError::InvalidUtf8.into();
3511 };
3512 let parsed: CapabilityFilterJson = match serde_json::from_str(&s) {
3513 Ok(v) => v,
3514 Err(_) => return NetError::InvalidJson.into(),
3515 };
3516 let filter = capability_filter_from_json(parsed);
3517 let ids = h.inner.find_nodes_by_filter(&filter);
3518 write_json_out(&ids, out_json, out_len)
3519}
3520
3521#[derive(serde::Deserialize)]
3540struct ScopeFilterJson {
3541 kind: String,
3542 #[serde(default)]
3543 tenant: Option<String>,
3544 #[serde(default)]
3545 tenants: Option<Vec<String>>,
3546 #[serde(default)]
3547 region: Option<String>,
3548 #[serde(default)]
3549 regions: Option<Vec<String>>,
3550}
3551
3552enum ScopeFilterOwned {
3558 Any,
3559 GlobalOnly,
3560 SameSubnet,
3561 Tenant(String),
3562 Tenants(Vec<String>),
3563 Region(String),
3564 Regions(Vec<String>),
3565}
3566
3567fn scope_filter_from_json(f: ScopeFilterJson) -> Result<ScopeFilterOwned, NetError> {
3586 fn clean(v: Vec<String>) -> Option<Vec<String>> {
3590 let cleaned: Vec<String> = v.into_iter().filter(|s| !s.is_empty()).collect();
3591 (!cleaned.is_empty()).then_some(cleaned)
3592 }
3593 let filter = match f.kind.as_str() {
3594 "any" => ScopeFilterOwned::Any,
3595 "global_only" | "globalOnly" => ScopeFilterOwned::GlobalOnly,
3596 "same_subnet" | "sameSubnet" => ScopeFilterOwned::SameSubnet,
3597 "tenant" => match f.tenant {
3598 Some(t) if !t.is_empty() => ScopeFilterOwned::Tenant(t),
3599 _ => return Err(NetError::InvalidArgument),
3600 },
3601 "tenants" => match f.tenants.and_then(clean) {
3602 Some(ts) => ScopeFilterOwned::Tenants(ts),
3603 None => return Err(NetError::InvalidArgument),
3604 },
3605 "region" => match f.region {
3606 Some(r) if !r.is_empty() => ScopeFilterOwned::Region(r),
3607 _ => return Err(NetError::InvalidArgument),
3608 },
3609 "regions" => match f.regions.and_then(clean) {
3610 Some(rs) => ScopeFilterOwned::Regions(rs),
3611 None => return Err(NetError::InvalidArgument),
3612 },
3613 _ => return Err(NetError::InvalidArgument),
3614 };
3615 Ok(filter)
3616}
3617
3618fn with_scope_filter<R>(
3623 owned: &ScopeFilterOwned,
3624 f: impl FnOnce(&crate::adapter::net::behavior::capability::ScopeFilter<'_>) -> R,
3625) -> R {
3626 use crate::adapter::net::behavior::capability::ScopeFilter as F;
3627 match owned {
3628 ScopeFilterOwned::Any => f(&F::Any),
3629 ScopeFilterOwned::GlobalOnly => f(&F::GlobalOnly),
3630 ScopeFilterOwned::SameSubnet => f(&F::SameSubnet),
3631 ScopeFilterOwned::Tenant(t) => f(&F::Tenant(t.as_str())),
3632 ScopeFilterOwned::Tenants(ts) => {
3633 let refs: Vec<&str> = ts.iter().map(|s| s.as_str()).collect();
3634 f(&F::Tenants(refs.as_slice()))
3635 }
3636 ScopeFilterOwned::Region(r) => f(&F::Region(r.as_str())),
3637 ScopeFilterOwned::Regions(rs) => {
3638 let refs: Vec<&str> = rs.iter().map(|s| s.as_str()).collect();
3639 f(&F::Regions(refs.as_slice()))
3640 }
3641 }
3642}
3643
3644#[unsafe(no_mangle)]
3667pub unsafe extern "C" fn net_mesh_find_nodes_scoped(
3668 handle: *mut MeshNodeHandle,
3669 filter_json: *const c_char,
3670 scope_json: *const c_char,
3671 out_json: *mut *mut c_char,
3672 out_len: *mut usize,
3673) -> c_int {
3674 if handle.is_null()
3675 || filter_json.is_null()
3676 || scope_json.is_null()
3677 || out_json.is_null()
3678 || out_len.is_null()
3679 {
3680 return NetError::NullPointer.into();
3681 }
3682 let h = unsafe { &*handle };
3683 let _op = match h.guard.try_enter() {
3684 Some(op) => op,
3685 None => return NetError::ShuttingDown.into(),
3686 };
3687 let Some(filter_s) = (unsafe { c_str_to_string(filter_json) }) else {
3688 return NetError::InvalidUtf8.into();
3689 };
3690 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
3691 return NetError::InvalidUtf8.into();
3692 };
3693 let parsed_filter: CapabilityFilterJson = match serde_json::from_str(&filter_s) {
3694 Ok(v) => v,
3695 Err(_) => return NetError::InvalidJson.into(),
3696 };
3697 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
3698 Ok(v) => v,
3699 Err(_) => return NetError::InvalidJson.into(),
3700 };
3701 let filter = capability_filter_from_json(parsed_filter);
3702 let owned = match scope_filter_from_json(parsed_scope) {
3703 Ok(v) => v,
3704 Err(e) => return e.into(),
3705 };
3706 let ids = with_scope_filter(&owned, |sf| {
3707 h.inner.find_nodes_by_filter_scoped(&filter, sf)
3708 });
3709 write_json_out(&ids, out_json, out_len)
3710}
3711
3712#[derive(serde::Deserialize)]
3726struct CapabilityRequirementJson {
3727 #[serde(default)]
3728 filter: CapabilityFilterJson,
3729 #[serde(default)]
3730 prefer_more_memory: f32,
3731 #[serde(default)]
3732 prefer_more_vram: f32,
3733 #[serde(default)]
3734 prefer_faster_inference: f32,
3735 #[serde(default)]
3736 prefer_loaded_models: f32,
3737}
3738
3739fn capability_requirement_from_json(
3740 j: CapabilityRequirementJson,
3741) -> crate::adapter::net::behavior::capability::CapabilityRequirement {
3742 crate::adapter::net::behavior::capability::CapabilityRequirement::from_filter(
3743 capability_filter_from_json(j.filter),
3744 )
3745 .prefer_memory(j.prefer_more_memory)
3746 .prefer_vram(j.prefer_more_vram)
3747 .prefer_speed(j.prefer_faster_inference)
3748 .prefer_loaded(j.prefer_loaded_models)
3749}
3750
3751#[unsafe(no_mangle)]
3761pub unsafe extern "C" fn net_mesh_find_best_node(
3762 handle: *mut MeshNodeHandle,
3763 requirement_json: *const c_char,
3764 out_node_id: *mut u64,
3765 out_has_match: *mut c_int,
3766) -> c_int {
3767 if handle.is_null()
3768 || requirement_json.is_null()
3769 || out_node_id.is_null()
3770 || out_has_match.is_null()
3771 {
3772 return NetError::NullPointer.into();
3773 }
3774 let h = unsafe { &*handle };
3775 let _op = match h.guard.try_enter() {
3776 Some(op) => op,
3777 None => return NetError::ShuttingDown.into(),
3778 };
3779 let Some(s) = (unsafe { c_str_to_string(requirement_json) }) else {
3780 return NetError::InvalidUtf8.into();
3781 };
3782 let parsed: CapabilityRequirementJson = match serde_json::from_str(&s) {
3783 Ok(v) => v,
3784 Err(_) => return NetError::InvalidJson.into(),
3785 };
3786 let req = capability_requirement_from_json(parsed);
3787 match h.inner.find_best_node(&req) {
3788 Some(node_id) => unsafe {
3789 *out_node_id = node_id;
3790 *out_has_match = 1;
3791 },
3792 None => unsafe {
3793 *out_has_match = 0;
3794 },
3795 }
3796 0
3797}
3798
3799#[unsafe(no_mangle)]
3808pub unsafe extern "C" fn net_mesh_find_best_node_scoped(
3809 handle: *mut MeshNodeHandle,
3810 requirement_json: *const c_char,
3811 scope_json: *const c_char,
3812 out_node_id: *mut u64,
3813 out_has_match: *mut c_int,
3814) -> c_int {
3815 if handle.is_null()
3816 || requirement_json.is_null()
3817 || scope_json.is_null()
3818 || out_node_id.is_null()
3819 || out_has_match.is_null()
3820 {
3821 return NetError::NullPointer.into();
3822 }
3823 let h = unsafe { &*handle };
3824 let _op = match h.guard.try_enter() {
3825 Some(op) => op,
3826 None => return NetError::ShuttingDown.into(),
3827 };
3828 let Some(req_s) = (unsafe { c_str_to_string(requirement_json) }) else {
3829 return NetError::InvalidUtf8.into();
3830 };
3831 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
3832 return NetError::InvalidUtf8.into();
3833 };
3834 let parsed_req: CapabilityRequirementJson = match serde_json::from_str(&req_s) {
3835 Ok(v) => v,
3836 Err(_) => return NetError::InvalidJson.into(),
3837 };
3838 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
3839 Ok(v) => v,
3840 Err(_) => return NetError::InvalidJson.into(),
3841 };
3842 let req = capability_requirement_from_json(parsed_req);
3843 let owned = match scope_filter_from_json(parsed_scope) {
3844 Ok(v) => v,
3845 Err(e) => return e.into(),
3846 };
3847 let result = with_scope_filter(&owned, |sf| h.inner.find_best_node_scoped(&req, sf));
3848 match result {
3849 Some(node_id) => unsafe {
3850 *out_node_id = node_id;
3851 *out_has_match = 1;
3852 },
3853 None => unsafe {
3854 *out_has_match = 0;
3855 },
3856 }
3857 0
3858}
3859
3860#[unsafe(no_mangle)]
3862pub unsafe extern "C" fn net_normalize_gpu_vendor(
3863 raw: *const c_char,
3864 out_json: *mut *mut c_char,
3865 out_len: *mut usize,
3866) -> c_int {
3867 if raw.is_null() || out_json.is_null() || out_len.is_null() {
3868 return NetError::NullPointer.into();
3869 }
3870 let Some(s) = (unsafe { c_str_to_string(raw) }) else {
3871 return NetError::InvalidUtf8.into();
3872 };
3873 let canonical = gpu_vendor_to_string_cap(parse_gpu_vendor_cap(&s));
3874 write_string_out(canonical.to_string(), out_json, out_len)
3875}
3876
3877pub(crate) const NET_ERR_GANG_INVALID: c_int = -140;
3888
3889#[derive(Deserialize)]
3893struct GangCriteriaJson {
3894 #[serde(default)]
3896 tags_all: Vec<String>,
3897 #[serde(default)]
3898 tags_any: Vec<String>,
3899 #[serde(default)]
3900 tag_groups_all: Vec<Vec<String>>,
3901 #[serde(default)]
3903 region: Option<String>,
3904 #[serde(default)]
3906 min_units: usize,
3907 #[serde(default)]
3908 max_load: Option<f32>,
3909 #[serde(default)]
3910 max_p50_latency_us: Option<u32>,
3911 #[serde(default)]
3912 require_all: Vec<String>,
3913 #[serde(default)]
3914 require_any: Vec<String>,
3915 #[serde(default)]
3916 selection: Option<String>,
3917 #[serde(default)]
3918 load_band_target: Option<f32>,
3919 #[serde(default)]
3920 prefer_capability: Option<String>,
3921}
3922
3923#[derive(Deserialize)]
3926struct IslandRecordJson {
3927 id: u64,
3928 #[serde(default)]
3929 units: Vec<u32>,
3930 #[serde(default)]
3931 capabilities: Vec<String>,
3932 #[serde(default)]
3933 load: f32,
3934 #[serde(default)]
3935 p50_latency_us: u32,
3936}
3937
3938fn build_gang_criteria(
3939 c: GangCriteriaJson,
3940) -> Option<crate::adapter::net::behavior::gang::MatchCriteria> {
3941 use crate::adapter::net::behavior::fold::{CapabilityFilter, CapabilityQuery};
3942 use crate::adapter::net::behavior::gang::{MatchCriteria, NumericFilter, SelectionPolicy};
3943 let selection = match c.selection.as_deref() {
3944 None | Some("least_loaded") => SelectionPolicy::LeastLoaded,
3945 Some("pack") => SelectionPolicy::Pack,
3946 Some("lowest_id") => SelectionPolicy::LowestId,
3947 Some("load_band") => SelectionPolicy::LoadBand(c.load_band_target.unwrap_or(0.5)),
3948 Some(_) => return None,
3949 };
3950 Some(MatchCriteria {
3951 capability: CapabilityQuery::Composite(CapabilityFilter {
3952 tags_all: c.tags_all,
3953 tags_any: c.tags_any,
3954 tag_groups_all: c.tag_groups_all,
3955 region: c.region,
3956 ..Default::default()
3957 }),
3958 numeric: NumericFilter {
3959 min_units: c.min_units,
3960 max_load: c.max_load,
3961 max_p50_latency_us: c.max_p50_latency_us,
3962 require_all: c.require_all,
3963 require_any: c.require_any,
3964 },
3965 selection,
3966 prefer_capability: c.prefer_capability,
3967 })
3968}
3969
3970#[unsafe(no_mangle)]
3975pub unsafe extern "C" fn net_mesh_publish_island_topology(
3976 handle: *mut MeshNodeHandle,
3977 record_json: *const c_char,
3978 out_count: *mut usize,
3979) -> c_int {
3980 if handle.is_null() || record_json.is_null() {
3981 return NetError::NullPointer.into();
3982 }
3983 let h = unsafe { &*handle };
3984 let _op = match h.guard.try_enter() {
3985 Some(op) => op,
3986 None => return NetError::ShuttingDown.into(),
3987 };
3988 let Some(js) = (unsafe { c_str_to_string(record_json) }) else {
3989 return NetError::InvalidUtf8.into();
3990 };
3991 let rec: IslandRecordJson = match serde_json::from_str(&js) {
3992 Ok(r) => r,
3993 Err(_) => return NET_ERR_GANG_INVALID,
3994 };
3995 use crate::adapter::net::behavior::fold::{IslandRecord, UnitSet};
3996 let record = IslandRecord {
3997 id: rec.id,
3998 units: UnitSet::new(rec.units),
3999 host: 0, capabilities: rec.capabilities,
4001 load: rec.load,
4002 p50_latency_us: rec.p50_latency_us,
4003 };
4004 let node = h.inner.clone();
4005 match block_on(async move { node.publish_island_topology(record).await }) {
4006 Ok(n) => {
4007 if !out_count.is_null() {
4008 unsafe {
4009 *out_count = n;
4010 }
4011 }
4012 0
4013 }
4014 Err(e) => adapter_err_to_code(&e),
4015 }
4016}
4017
4018#[unsafe(no_mangle)]
4022pub unsafe extern "C" fn net_mesh_match_islands(
4023 handle: *mut MeshNodeHandle,
4024 criteria_json: *const c_char,
4025 out_ids: *mut u64,
4026 cap: usize,
4027 out_count: *mut usize,
4028) -> c_int {
4029 if handle.is_null() || criteria_json.is_null() || out_count.is_null() {
4030 return NetError::NullPointer.into();
4031 }
4032 let h = unsafe { &*handle };
4033 let _op = match h.guard.try_enter() {
4034 Some(op) => op,
4035 None => return NetError::ShuttingDown.into(),
4036 };
4037 let Some(js) = (unsafe { c_str_to_string(criteria_json) }) else {
4038 return NetError::InvalidUtf8.into();
4039 };
4040 let parsed: GangCriteriaJson = match serde_json::from_str(&js) {
4041 Ok(c) => c,
4042 Err(_) => return NET_ERR_GANG_INVALID,
4043 };
4044 let Some(criteria) = build_gang_criteria(parsed) else {
4045 return NET_ERR_GANG_INVALID;
4046 };
4047 let ids = h.inner.match_islands(&criteria);
4048 unsafe {
4049 *out_count = ids.len();
4050 if !out_ids.is_null() {
4051 let n = ids.len().min(cap);
4052 std::ptr::copy_nonoverlapping(ids.as_ptr(), out_ids, n);
4053 }
4054 }
4055 0
4056}
4057
4058#[unsafe(no_mangle)]
4061pub unsafe extern "C" fn net_mesh_reserve_island(
4062 handle: *mut MeshNodeHandle,
4063 island: u64,
4064 until_unix_us: u64,
4065 out_outcome: *mut c_int,
4066) -> c_int {
4067 if handle.is_null() || out_outcome.is_null() {
4068 return NetError::NullPointer.into();
4069 }
4070 let h = unsafe { &*handle };
4071 let _op = match h.guard.try_enter() {
4072 Some(op) => op,
4073 None => return NetError::ShuttingDown.into(),
4074 };
4075 let node = h.inner.clone();
4076 match block_on(async move { node.reserve_island(island, until_unix_us).await }) {
4077 Ok(outcome) => {
4078 unsafe {
4079 *out_outcome = claim_outcome_code(outcome);
4080 }
4081 0
4082 }
4083 Err(e) => adapter_err_to_code(&e),
4084 }
4085}
4086
4087#[unsafe(no_mangle)]
4090pub unsafe extern "C" fn net_mesh_release_island(
4091 handle: *mut MeshNodeHandle,
4092 island: u64,
4093 out_outcome: *mut c_int,
4094) -> c_int {
4095 if handle.is_null() || out_outcome.is_null() {
4096 return NetError::NullPointer.into();
4097 }
4098 let h = unsafe { &*handle };
4099 let _op = match h.guard.try_enter() {
4100 Some(op) => op,
4101 None => return NetError::ShuttingDown.into(),
4102 };
4103 let node = h.inner.clone();
4104 match block_on(async move { node.release_island(island).await }) {
4105 Ok(outcome) => {
4106 unsafe {
4107 *out_outcome = claim_outcome_code(outcome);
4108 }
4109 0
4110 }
4111 Err(e) => adapter_err_to_code(&e),
4112 }
4113}
4114
4115#[unsafe(no_mangle)]
4119pub unsafe extern "C" fn net_mesh_claim_island(
4120 handle: *mut MeshNodeHandle,
4121 criteria_json: *const c_char,
4122 until_unix_us: u64,
4123 out_found: *mut c_int,
4124 out_island: *mut u64,
4125) -> c_int {
4126 if handle.is_null() || criteria_json.is_null() || out_found.is_null() || out_island.is_null() {
4127 return NetError::NullPointer.into();
4128 }
4129 unsafe {
4134 *out_found = 0;
4135 *out_island = 0;
4136 }
4137 let h = unsafe { &*handle };
4138 let _op = match h.guard.try_enter() {
4139 Some(op) => op,
4140 None => return NetError::ShuttingDown.into(),
4141 };
4142 let Some(js) = (unsafe { c_str_to_string(criteria_json) }) else {
4143 return NetError::InvalidUtf8.into();
4144 };
4145 let parsed: GangCriteriaJson = match serde_json::from_str(&js) {
4146 Ok(c) => c,
4147 Err(_) => return NET_ERR_GANG_INVALID,
4148 };
4149 let Some(criteria) = build_gang_criteria(parsed) else {
4150 return NET_ERR_GANG_INVALID;
4151 };
4152 let node = h.inner.clone();
4153 match block_on(async move { node.claim_island(&criteria, until_unix_us).await }) {
4154 Ok(Some(id)) => {
4155 unsafe {
4156 *out_found = 1;
4157 *out_island = id;
4158 }
4159 0
4160 }
4161 Ok(None) => 0,
4162 Err(e) => adapter_err_to_code(&e),
4163 }
4164}
4165
4166fn claim_outcome_code(o: crate::adapter::net::behavior::gang::ClaimOutcome) -> c_int {
4167 use crate::adapter::net::behavior::gang::ClaimOutcome;
4168 match o {
4169 ClaimOutcome::Won => 0,
4170 ClaimOutcome::Lost => 1,
4171 }
4172}
4173
4174#[cfg(test)]
4175mod tests {
4176 use super::*;
4177
4178 mod scope_filter_rejects_unusable {
4188 use super::super::{scope_filter_from_json, ScopeFilterJson, ScopeFilterOwned};
4189 use crate::ffi::NetError;
4190
4191 fn kind(kind: &str) -> ScopeFilterJson {
4192 ScopeFilterJson {
4193 kind: kind.into(),
4194 tenant: None,
4195 tenants: None,
4196 region: None,
4197 regions: None,
4198 }
4199 }
4200
4201 #[test]
4202 fn unknown_kind_is_invalid_argument() {
4203 assert!(matches!(
4204 scope_filter_from_json(kind("tenat")),
4205 Err(NetError::InvalidArgument)
4206 ));
4207 }
4208
4209 #[test]
4210 fn missing_or_empty_selectors_are_invalid_argument() {
4211 let cases = vec![
4212 kind("tenant"),
4213 ScopeFilterJson {
4214 tenant: Some(String::new()),
4215 ..kind("tenant")
4216 },
4217 kind("tenants"),
4218 ScopeFilterJson {
4219 tenants: Some(vec![String::new()]),
4220 ..kind("tenants")
4221 },
4222 kind("region"),
4223 ScopeFilterJson {
4224 region: Some(String::new()),
4225 ..kind("region")
4226 },
4227 kind("regions"),
4228 ScopeFilterJson {
4229 regions: Some(vec![String::new(), String::new()]),
4230 ..kind("regions")
4231 },
4232 ];
4233 for case in cases {
4234 let label = case.kind.clone();
4235 assert!(
4236 matches!(scope_filter_from_json(case), Err(NetError::InvalidArgument)),
4237 "kind {label:?} with an unusable selector must be \
4238 InvalidArgument, not a silent widen to Any"
4239 );
4240 }
4241 }
4242
4243 #[test]
4246 fn usable_filters_still_convert() {
4247 assert!(matches!(
4248 scope_filter_from_json(kind("any")),
4249 Ok(ScopeFilterOwned::Any)
4250 ));
4251 for k in ["global_only", "globalOnly"] {
4252 assert!(matches!(
4253 scope_filter_from_json(kind(k)),
4254 Ok(ScopeFilterOwned::GlobalOnly)
4255 ));
4256 }
4257 assert!(matches!(
4258 scope_filter_from_json(ScopeFilterJson {
4259 tenants: Some(vec![String::new(), "oem-123".into()]),
4260 ..kind("tenants")
4261 }),
4262 Ok(ScopeFilterOwned::Tenants(ts)) if ts == vec!["oem-123".to_string()]
4263 ));
4264 }
4265 }
4266
4267 #[cfg(feature = "nat-traversal")]
4275 mod traversal_stats_abi {
4276 use super::super::NetTraversalStatsV2;
4277 use std::mem::{align_of, offset_of, size_of};
4278
4279 fn rust_offset(name: &str) -> Option<usize> {
4284 Some(match name {
4285 "punches_attempted" => offset_of!(NetTraversalStatsV2, punches_attempted),
4286 "punches_succeeded" => offset_of!(NetTraversalStatsV2, punches_succeeded),
4287 "punches_failed" => offset_of!(NetTraversalStatsV2, punches_failed),
4288 "relay_fallbacks" => offset_of!(NetTraversalStatsV2, relay_fallbacks),
4289 "punch_timeouts" => offset_of!(NetTraversalStatsV2, punch_timeouts),
4290 "punch_rejections" => offset_of!(NetTraversalStatsV2, punch_rejections),
4291 "rendezvous_no_relay" => offset_of!(NetTraversalStatsV2, rendezvous_no_relay),
4292 "upgrades_attempted" => offset_of!(NetTraversalStatsV2, upgrades_attempted),
4293 "upgrades_succeeded" => offset_of!(NetTraversalStatsV2, upgrades_succeeded),
4294 "upgrades_deferred_busy" => offset_of!(NetTraversalStatsV2, upgrades_deferred_busy),
4295 "port_mapping_renewals" => offset_of!(NetTraversalStatsV2, port_mapping_renewals),
4296 "port_mapping_active" => offset_of!(NetTraversalStatsV2, port_mapping_active),
4297 "port_mapping_external" => offset_of!(NetTraversalStatsV2, port_mapping_external),
4298 _ => return None,
4299 })
4300 }
4301
4302 fn c_type_layout(ctype: &str) -> (usize, usize) {
4312 use std::mem::{align_of, size_of};
4313 use std::os::raw::c_char;
4314 match ctype {
4315 "uint64_t" => (size_of::<u64>(), align_of::<u64>()),
4316 "uint8_t" => (size_of::<u8>(), align_of::<u8>()),
4317 "char[64]" => (size_of::<c_char>() * 64, align_of::<c_char>()),
4318 other => panic!("unhandled C type in net_traversal_stats_v2_t: {other:?}"),
4319 }
4320 }
4321
4322 fn round_up(off: usize, align: usize) -> usize {
4323 off.div_ceil(align) * align
4324 }
4325
4326 fn parse_header_fields(header: &str) -> Vec<(String, String)> {
4330 let end = header
4331 .find("} net_traversal_stats_v2_t;")
4332 .expect("stats typedef present in header");
4333 let open = header[..end].rfind('{').expect("struct open brace");
4334 let mut fields = Vec::new();
4335 for line in header[open + 1..end].lines() {
4336 let line = line.trim();
4337 if line.is_empty()
4338 || line.starts_with("//")
4339 || line.starts_with('*')
4340 || line.starts_with("/*")
4341 {
4342 continue;
4343 }
4344 let decl = line.trim_end_matches(';').trim();
4345 let (ctype, name_arr) = decl
4346 .rsplit_once(char::is_whitespace)
4347 .expect("field decl shaped `type name`");
4348 let (ctype, name_arr) = (ctype.trim(), name_arr.trim());
4349 if let Some((name, arr)) = name_arr.split_once('[') {
4350 fields.push((format!("{ctype}[{arr}"), name.to_string()));
4351 } else {
4352 fields.push((ctype.to_string(), name_arr.to_string()));
4353 }
4354 }
4355 fields
4356 }
4357
4358 #[test]
4359 fn c_header_layout_matches_rust_repr_c() {
4360 let header =
4361 std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/include/net.go.h"))
4362 .expect("read include/net.go.h");
4363 let fields = parse_header_fields(&header);
4364 assert_eq!(
4365 fields.len(),
4366 13,
4367 "expected 13 fields in net_traversal_stats_v2_t, parsed {fields:?}",
4368 );
4369
4370 let mut off = 0usize;
4375 let mut align = 1usize;
4376 for (ctype, name) in &fields {
4377 let (sz, al) = c_type_layout(ctype);
4378 off = round_up(off, al);
4379 align = align.max(al);
4380 let rust = rust_offset(name)
4381 .unwrap_or_else(|| panic!("header field `{name}` has no Rust struct field"));
4382 assert_eq!(
4383 rust, off,
4384 "field `{name}`: Rust offset {rust} != C offset {off}"
4385 );
4386 off += sz;
4387 }
4388 assert_eq!(
4389 size_of::<NetTraversalStatsV2>(),
4390 round_up(off, align),
4391 "net_traversal_stats_v2_t total size drift (Rust vs C header)",
4392 );
4393 assert_eq!(
4394 align_of::<NetTraversalStatsV2>(),
4395 align,
4396 "net_traversal_stats_v2_t alignment drift (Rust vs C header)",
4397 );
4398 }
4399 }
4400
4401 #[test]
4413 fn saturating_u16_cap_clamps_at_u16_max() {
4414 assert_eq!(saturating_u16_cap(0), 0);
4415 assert_eq!(saturating_u16_cap(42), 42);
4416 assert_eq!(saturating_u16_cap(u16::MAX as u32), u16::MAX);
4417 assert_eq!(saturating_u16_cap(u16::MAX as u32 + 1), u16::MAX);
4418 assert_eq!(saturating_u16_cap(u32::MAX), u16::MAX);
4419 }
4420
4421 #[test]
4427 fn parse_peer_pubkey_hex_accepts_valid_and_rejects_malformed() {
4428 use std::ffi::CString;
4429
4430 let valid = CString::new("ab".repeat(32)).unwrap();
4431 let parsed = unsafe { parse_peer_pubkey_hex(valid.as_ptr()) };
4433 assert_eq!(parsed, Ok([0xABu8; 32]), "64-char hex round-trips");
4434
4435 let bad_hex = CString::new("zz".repeat(32)).unwrap();
4436 let err = unsafe { parse_peer_pubkey_hex(bad_hex.as_ptr()) };
4438 assert_eq!(err, Err(NET_ERR_MESH_HANDSHAKE), "non-hex rejects");
4439
4440 let short = CString::new("abcd").unwrap();
4441 let err = unsafe { parse_peer_pubkey_hex(short.as_ptr()) };
4443 assert_eq!(err, Err(NET_ERR_MESH_HANDSHAKE), "wrong length rejects");
4444
4445 let non_utf8 = CString::new(vec![0xFFu8, 0xFEu8]).unwrap();
4447 let err = unsafe { parse_peer_pubkey_hex(non_utf8.as_ptr()) };
4449 assert_eq!(
4450 err,
4451 Err(NetError::InvalidUtf8.into()),
4452 "non-UTF-8 C string rejects with the UTF-8 code",
4453 );
4454 }
4455
4456 #[cfg(feature = "nat-traversal")]
4463 #[test]
4464 fn traversal_stats_v2_fill_maps_all_fields() {
4465 use crate::adapter::net::traversal::TraversalStatsSnapshot;
4466
4467 let snap = TraversalStatsSnapshot {
4468 punches_attempted: 1,
4469 punches_succeeded: 2,
4470 relay_fallbacks: 3,
4471 port_mapping_active: true,
4472 port_mapping_external: Some("203.0.113.5:4321".parse().unwrap()),
4473 port_mapping_renewals: 4,
4474 upgrades_attempted: 5,
4475 upgrades_succeeded: 6,
4476 upgrades_deferred_busy: 7,
4477 punches_failed: 8,
4478 punch_timeouts: 9,
4479 punch_rejections: 10,
4480 rendezvous_no_relay: 11,
4481 };
4482 let mut out = NetTraversalStatsV2 {
4483 punches_attempted: 0,
4484 punches_succeeded: 0,
4485 punches_failed: 0,
4486 relay_fallbacks: 0,
4487 punch_timeouts: 0,
4488 punch_rejections: 0,
4489 rendezvous_no_relay: 0,
4490 upgrades_attempted: 0,
4491 upgrades_succeeded: 0,
4492 upgrades_deferred_busy: 0,
4493 port_mapping_renewals: 0,
4494 port_mapping_active: 0,
4495 port_mapping_external: [0x7F; 64], };
4497 fill_traversal_stats_v2(&snap, &mut out);
4498
4499 assert_eq!(out.punches_attempted, 1);
4500 assert_eq!(out.punches_succeeded, 2);
4501 assert_eq!(out.relay_fallbacks, 3);
4502 assert_eq!(out.port_mapping_renewals, 4);
4503 assert_eq!(out.upgrades_attempted, 5);
4504 assert_eq!(out.upgrades_succeeded, 6);
4505 assert_eq!(out.upgrades_deferred_busy, 7);
4506 assert_eq!(out.punches_failed, 8);
4507 assert_eq!(out.punch_timeouts, 9);
4508 assert_eq!(out.punch_rejections, 10);
4509 assert_eq!(out.rendezvous_no_relay, 11);
4510 assert_eq!(out.port_mapping_active, 1);
4511 let s: String = out
4512 .port_mapping_external
4513 .iter()
4514 .take_while(|&&c| c != 0)
4515 .map(|&c| c as u8 as char)
4516 .collect();
4517 assert_eq!(s, "203.0.113.5:4321");
4518 assert!(out.port_mapping_external.contains(&0));
4520
4521 let snap_off = TraversalStatsSnapshot {
4523 port_mapping_active: false,
4524 port_mapping_external: None,
4525 ..snap
4526 };
4527 fill_traversal_stats_v2(&snap_off, &mut out);
4528 assert_eq!(out.port_mapping_active, 0);
4529 assert_eq!(
4530 out.port_mapping_external[0], 0,
4531 "empty string when inactive"
4532 );
4533 }
4534
4535 #[test]
4544 fn parse_modality_cap_returns_none_on_unknown_strings() {
4545 for (s, expected) in [
4547 ("text", Modality::Text),
4548 ("Text", Modality::Text),
4549 ("TEXT", Modality::Text),
4550 ("image", Modality::Image),
4551 ("audio", Modality::Audio),
4552 ("video", Modality::Video),
4553 ("code", Modality::Code),
4554 ("embedding", Modality::Embedding),
4555 ("tool-use", Modality::ToolUse),
4556 ("tool_use", Modality::ToolUse),
4557 ("tooluse", Modality::ToolUse),
4558 ] {
4559 assert_eq!(
4560 parse_modality_cap(s),
4561 Some(expected),
4562 "known modality `{s}` must parse",
4563 );
4564 }
4565
4566 for s in ["audoi", "imageX", "vidoe", "embeding", "garbage", ""] {
4568 assert_eq!(
4569 parse_modality_cap(s),
4570 None,
4571 "unknown modality `{s}` must return None — pre-fix this \
4572 fell back to Modality::Text, advertising a capability \
4573 the node didn't actually have",
4574 );
4575 }
4576 }
4577
4578 #[test]
4588 fn gpu_info_from_json_saturates_fp16_tflops_to_u16_max() {
4589 let g = GpuJson {
4592 vendor: None,
4593 model: "test".to_string(),
4594 vram_gb: 0,
4595 compute_units: None,
4596 tensor_cores: None,
4597 fp16_tflops_x10: Some(1_000_000_000u32),
4598 };
4599 let info = gpu_info_from_json(g);
4600 assert_eq!(
4604 info.fp16_tflops_x10,
4605 u16::MAX as u32,
4606 "fp16_tflops_x10 must saturate at u16::MAX (65535) instead of \
4607 losing precision through the f32 round-trip; got {}",
4608 info.fp16_tflops_x10,
4609 );
4610
4611 let g_small = GpuJson {
4613 vendor: None,
4614 model: "test".to_string(),
4615 vram_gb: 0,
4616 compute_units: None,
4617 tensor_cores: None,
4618 fp16_tflops_x10: Some(425), };
4620 let info_small = gpu_info_from_json(g_small);
4621 assert_eq!(
4622 info_small.fp16_tflops_x10, 425,
4623 "small fp16_tflops_x10 must round-trip exactly"
4624 );
4625 }
4626
4627 #[test]
4640 fn alloc_bytes_round_trip_across_sizes() {
4641 for size in [0usize, 1, 15, 16, 17, 32, 64, 1024, 8192] {
4642 let src: Vec<u8> = (0..size).map(|i| (i as u8).wrapping_mul(37)).collect();
4643 let mut ptr: *mut u8 = std::ptr::null_mut();
4644 let mut len: usize = 0;
4645 let rc = alloc_bytes(&src, &mut ptr as *mut _, &mut len as *mut _);
4646 assert_eq!(rc, 0);
4647 assert_eq!(len, size);
4648 if size == 0 {
4649 assert!(ptr.is_null());
4650 } else {
4651 assert!(!ptr.is_null());
4652 let observed = unsafe { std::slice::from_raw_parts(ptr, len) };
4653 assert_eq!(observed, &src[..]);
4654 }
4655 unsafe { net_free_bytes(ptr, len) };
4658 }
4659 }
4660
4661 #[test]
4662 fn net_free_bytes_null_and_zero_len_are_noops() {
4663 unsafe { net_free_bytes(std::ptr::null_mut(), 0) };
4665 unsafe { net_free_bytes(std::ptr::null_mut(), 42) };
4666 let mut sentinel: u8 = 0;
4669 unsafe { net_free_bytes(&mut sentinel as *mut u8, 0) };
4670 }
4671
4672 #[test]
4684 fn net_free_bytes_does_not_panic_on_oversized_len() {
4685 let mut sentinel: u8 = 0;
4693 let ptr = &mut sentinel as *mut u8;
4694 unsafe { net_free_bytes(ptr, usize::MAX) };
4697 assert_eq!(sentinel, 0, "sentinel must not have been written through");
4700 }
4701
4702 #[test]
4711 fn net_mesh_shutdown_runs_even_with_outstanding_arc_refs() {
4712 let cfg = serde_json::json!({
4713 "bind_addr": "127.0.0.1:0",
4714 "psk_hex": "0".repeat(64),
4715 });
4716 let cfg_c = CString::new(cfg.to_string()).unwrap();
4717 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
4718 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
4719 assert_eq!(rc, 0, "net_mesh_new failed: {rc}");
4720 assert!(!out.is_null());
4721
4722 let inner_clone = {
4725 let h = unsafe { &*out };
4726 Arc::clone(&h.inner)
4727 };
4728 assert!(Arc::strong_count(&inner_clone) >= 2);
4729 assert!(!inner_clone.is_shutdown());
4730
4731 let rc = unsafe { net_mesh_shutdown(out) };
4732 assert_eq!(rc, 0, "net_mesh_shutdown returned {rc}");
4733 assert!(
4734 inner_clone.is_shutdown(),
4735 "shutdown flag must be set even when extra Arc refs are outstanding"
4736 );
4737
4738 drop(inner_clone);
4739 unsafe { net_mesh_free(out) };
4743 }
4744
4745 #[test]
4754 fn net_mesh_new_records_identity_provenance() {
4755 let cfg = serde_json::json!({
4757 "bind_addr": "127.0.0.1:0",
4758 "psk_hex": "0".repeat(64),
4759 "identity_seed_hex": "7a".repeat(32),
4760 });
4761 let cfg_c = CString::new(cfg.to_string()).unwrap();
4762 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
4763 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) }, 0);
4764 assert!(
4765 unsafe { &*out }.inner.has_configured_identity(),
4766 "a caller-supplied identity_seed_hex must set configured_identity"
4767 );
4768 unsafe { net_mesh_free(out) };
4769
4770 let cfg = serde_json::json!({
4772 "bind_addr": "127.0.0.1:0",
4773 "psk_hex": "0".repeat(64),
4774 });
4775 let cfg_c = CString::new(cfg.to_string()).unwrap();
4776 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
4777 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) }, 0);
4778 assert!(
4779 !unsafe { &*out }.inner.has_configured_identity(),
4780 "a generated ephemeral fallback must leave configured_identity false"
4781 );
4782 unsafe { net_mesh_free(out) };
4783 }
4784
4785 #[test]
4797 fn handles_match_rejects_stream_node_mismatch() {
4798 fn make_node_handle() -> *mut MeshNodeHandle {
4799 let cfg = serde_json::json!({
4800 "bind_addr": "127.0.0.1:0",
4801 "psk_hex": "0".repeat(64),
4802 });
4803 let cfg_c = CString::new(cfg.to_string()).unwrap();
4804 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
4805 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
4806 assert_eq!(rc, 0);
4807 assert!(!out.is_null());
4808 out
4809 }
4810
4811 let nh_a = make_node_handle();
4812 let nh_b = make_node_handle();
4813
4814 let sh_a = {
4822 let h = unsafe { &*nh_a };
4823 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
4824 MeshStreamHandle {
4825 stream: ManuallyDrop::new(CoreStream {
4826 peer_node_id: 0xDEAD,
4827 stream_id: 1,
4828 epoch: 0,
4829 config: StreamConfig::new(),
4830 }),
4831 _node: ManuallyDrop::new(node_clone),
4832 guard: HandleGuard::new(),
4833 }
4834 };
4835
4836 assert!(
4838 handles_match(&sh_a, unsafe { &*nh_a }),
4839 "stream from node_a + node_a handle must match"
4840 );
4841 assert!(
4843 !handles_match(&sh_a, unsafe { &*nh_b }),
4844 "stream from node_a + node_b handle must be rejected (#19)"
4845 );
4846
4847 unsafe {
4856 let mut sh_a = sh_a;
4857 let _ = ManuallyDrop::take(&mut sh_a.stream);
4858 let _ = ManuallyDrop::take(&mut sh_a._node);
4859 }
4860 unsafe { net_mesh_free(nh_a) };
4861 unsafe { net_mesh_free(nh_b) };
4862 }
4863
4864 #[test]
4871 fn net_mesh_free_is_idempotent() {
4872 let cfg = serde_json::json!({
4873 "bind_addr": "127.0.0.1:0",
4874 "psk_hex": "0".repeat(64),
4875 });
4876 let cfg_c = CString::new(cfg.to_string()).unwrap();
4877 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
4878 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
4879 assert!(!nh.is_null());
4880
4881 unsafe { net_mesh_free(nh) };
4882 unsafe { net_mesh_free(nh) };
4886 }
4887
4888 #[test]
4892 fn net_identity_free_is_idempotent() {
4893 let mut h: *mut IdentityHandle = std::ptr::null_mut();
4894 assert_eq!(unsafe { net_identity_generate(&mut h) }, 0);
4895 assert!(!h.is_null());
4896
4897 unsafe { net_identity_free(h) };
4898 unsafe { net_identity_free(h) };
4900 }
4901
4902 #[test]
4914 fn net_mesh_free_waits_for_inflight_op() {
4915 use std::sync::atomic::{AtomicBool, Ordering};
4916 use std::time::{Duration, Instant};
4917
4918 let cfg = serde_json::json!({
4919 "bind_addr": "127.0.0.1:0",
4920 "psk_hex": "0".repeat(64),
4921 });
4922 let cfg_c = CString::new(cfg.to_string()).unwrap();
4923 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
4924 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
4925 assert!(!nh.is_null());
4926
4927 let nh_addr = nh as usize;
4930 let started = Arc::new(AtomicBool::new(false));
4931 let release = Arc::new(AtomicBool::new(false));
4932 let started_w = started.clone();
4933 let release_w = release.clone();
4934
4935 let worker = std::thread::spawn(move || {
4936 let h = unsafe { &*(nh_addr as *mut MeshNodeHandle) };
4937 let op = h.guard.try_enter().expect("entry must succeed pre-free");
4941 started_w.store(true, Ordering::SeqCst);
4942 while !release_w.load(Ordering::SeqCst) {
4943 std::thread::sleep(Duration::from_millis(1));
4944 }
4945 drop(op);
4946 });
4947
4948 while !started.load(Ordering::SeqCst) {
4950 std::thread::yield_now();
4951 }
4952
4953 let release_clone = release.clone();
4956 std::thread::spawn(move || {
4957 std::thread::sleep(Duration::from_millis(50));
4958 release_clone.store(true, Ordering::SeqCst);
4959 });
4960
4961 let t0 = Instant::now();
4963 unsafe { net_mesh_free(nh) };
4964 let elapsed = t0.elapsed();
4965 assert!(
4966 elapsed >= Duration::from_millis(40),
4967 "net_mesh_free returned in {:?} — pre-fix it would have proceeded \
4968 immediately and the worker's subsequent op would UAF",
4969 elapsed,
4970 );
4971 worker.join().unwrap();
4972 }
4973
4974 #[test]
4981 fn net_mesh_stream_stats_returns_shutting_down_after_free() {
4982 let cfg = serde_json::json!({
4983 "bind_addr": "127.0.0.1:0",
4984 "psk_hex": "0".repeat(64),
4985 });
4986 let cfg_c = CString::new(cfg.to_string()).unwrap();
4987 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
4988 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
4989 assert!(!nh.is_null());
4990
4991 unsafe { net_mesh_free(nh) };
4994
4995 let mut out_json: *mut c_char = std::ptr::null_mut();
4996 let mut out_len: usize = 0;
4997 let rc = unsafe { net_mesh_stream_stats(nh, 0xDEAD, 1, &mut out_json, &mut out_len) };
4998 assert_eq!(
4999 rc,
5000 NetError::ShuttingDown as c_int,
5001 "post-free stream_stats must surface ShuttingDown (got {rc})",
5002 );
5003 assert!(
5004 out_json.is_null(),
5005 "no payload may be written after the guard fires",
5006 );
5007 }
5008
5009 #[test]
5014 fn net_identity_issue_token_returns_shutting_down_after_free() {
5015 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
5016 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
5017 assert!(!signer.is_null());
5018 unsafe { net_identity_free(signer) };
5019
5020 let subject = [0u8; 32];
5023 let scope = CString::new("[\"publish\"]").unwrap();
5024 let channel = CString::new("test-channel").unwrap();
5025 let mut out_token: *mut u8 = std::ptr::null_mut();
5026 let mut out_token_len: usize = 0;
5027 let rc = unsafe {
5028 net_identity_issue_token(
5029 signer,
5030 subject.as_ptr(),
5031 subject.len(),
5032 scope.as_ptr(),
5033 channel.as_ptr(),
5034 60,
5035 0,
5036 &mut out_token,
5037 &mut out_token_len,
5038 )
5039 };
5040 assert_eq!(
5041 rc,
5042 NetError::ShuttingDown as c_int,
5043 "post-free issue_token must surface ShuttingDown (got {rc})",
5044 );
5045 assert!(out_token.is_null(), "no token bytes may be allocated");
5046 }
5047
5048 #[test]
5054 fn net_delegate_token_returns_shutting_down_after_free() {
5055 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
5056 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
5057 assert!(!signer.is_null());
5058
5059 let subject = [0u8; 32];
5061 let scope = CString::new("[\"publish\",\"delegate\"]").unwrap();
5062 let channel = CString::new("test-channel").unwrap();
5063 let mut parent_bytes: *mut u8 = std::ptr::null_mut();
5064 let mut parent_len: usize = 0;
5065 assert_eq!(
5066 unsafe {
5067 net_identity_issue_token(
5068 signer,
5069 subject.as_ptr(),
5070 subject.len(),
5071 scope.as_ptr(),
5072 channel.as_ptr(),
5073 60,
5074 1,
5075 &mut parent_bytes,
5076 &mut parent_len,
5077 )
5078 },
5079 0,
5080 );
5081 assert!(!parent_bytes.is_null());
5082
5083 unsafe { net_identity_free(signer) };
5085
5086 let new_subject = [1u8; 32];
5087 let restricted = CString::new("[\"publish\"]").unwrap();
5088 let mut child_bytes: *mut u8 = std::ptr::null_mut();
5089 let mut child_len: usize = 0;
5090 let rc = unsafe {
5091 net_delegate_token(
5092 signer,
5093 parent_bytes,
5094 parent_len,
5095 new_subject.as_ptr(),
5096 new_subject.len(),
5097 restricted.as_ptr(),
5098 &mut child_bytes,
5099 &mut child_len,
5100 )
5101 };
5102 assert_eq!(
5103 rc,
5104 NetError::ShuttingDown as c_int,
5105 "post-free delegate_token must surface ShuttingDown (got {rc})",
5106 );
5107 assert!(child_bytes.is_null(), "no child token may be allocated");
5108
5109 unsafe { net_free_bytes(parent_bytes, parent_len) };
5111 }
5112
5113 #[test]
5114 fn hardware_from_json_saturates_overflow_cpu_fields() {
5115 let h = HardwareJson {
5118 cpu_cores: Some(70_000),
5119 cpu_threads: Some(200_000),
5120 memory_gb: None,
5121 gpu: None,
5122 additional_gpus: Vec::new(),
5123 storage_gb: None,
5124 network_gbps: None,
5125 accelerators: Vec::new(),
5126 };
5127 let hw = hardware_from_json(h);
5128 assert_eq!(hw.cpu_cores, u16::MAX);
5129 assert_eq!(hw.cpu_threads, u16::MAX);
5130 }
5131
5132 #[test]
5139 fn token_entry_points_reject_oversize_len() {
5140 let invalid_json: c_int = NetError::InvalidJson.into();
5141 let mut sentinel: u8 = 0;
5142 let token = &mut sentinel as *mut u8 as *const u8;
5143
5144 let mut out_json: *mut c_char = std::ptr::null_mut();
5145 let mut out_len: usize = 0;
5146 assert_eq!(
5147 unsafe { net_parse_token(token, usize::MAX, &mut out_json, &mut out_len) },
5148 invalid_json,
5149 );
5150 assert!(out_json.is_null());
5151
5152 let mut out_ok: c_int = -42;
5153 assert_eq!(
5154 unsafe { net_verify_token(token, usize::MAX, &mut out_ok) },
5155 invalid_json,
5156 );
5157
5158 let mut out_expired: c_int = -42;
5159 assert_eq!(
5160 unsafe { net_token_is_expired(token, usize::MAX, &mut out_expired) },
5161 invalid_json,
5162 );
5163
5164 assert_eq!(
5165 sentinel, 0,
5166 "sentinel must not be touched: the length guard fires before any deref"
5167 );
5168 }
5169}
5170
5171#[cfg(all(test, not(feature = "nat-traversal")))]
5172mod nat_traversal_stub_tests {
5173 use super::*;
5190 use std::ptr;
5191
5192 #[test]
5193 fn nat_type_stub_returns_unsupported() {
5194 let mut out_str: *mut c_char = ptr::null_mut();
5195 let mut out_len: usize = 0;
5196 let code = unsafe { net_mesh_nat_type(ptr::null_mut(), &mut out_str, &mut out_len) };
5199 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5200 }
5201
5202 #[test]
5203 fn reflex_addr_stub_returns_unsupported() {
5204 let mut out_str: *mut c_char = ptr::null_mut();
5205 let mut out_len: usize = 0;
5206 let code = unsafe { net_mesh_reflex_addr(ptr::null_mut(), &mut out_str, &mut out_len) };
5208 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5209 }
5210
5211 #[test]
5212 fn peer_nat_type_stub_returns_unsupported() {
5213 let mut out_str: *mut c_char = ptr::null_mut();
5214 let mut out_len: usize = 0;
5215 let code =
5217 unsafe { net_mesh_peer_nat_type(ptr::null_mut(), 0, &mut out_str, &mut out_len) };
5218 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5219 }
5220
5221 #[test]
5222 fn probe_reflex_stub_returns_unsupported() {
5223 let mut out_str: *mut c_char = ptr::null_mut();
5224 let mut out_len: usize = 0;
5225 let code = unsafe { net_mesh_probe_reflex(ptr::null_mut(), 0, &mut out_str, &mut out_len) };
5227 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5228 }
5229
5230 #[test]
5231 fn reclassify_nat_stub_returns_unsupported() {
5232 let code = unsafe { net_mesh_reclassify_nat(ptr::null_mut()) };
5234 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5235 }
5236
5237 #[test]
5238 fn traversal_stats_stub_returns_unsupported() {
5239 let mut a: u64 = 0;
5240 let mut b: u64 = 0;
5241 let mut c: u64 = 0;
5242 let code = unsafe { net_mesh_traversal_stats(ptr::null_mut(), &mut a, &mut b, &mut c) };
5244 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5245 }
5246
5247 #[test]
5248 fn connect_direct_stub_returns_unsupported() {
5249 let code = unsafe { net_mesh_connect_direct(ptr::null_mut(), 0, ptr::null(), 0) };
5251 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5252 }
5253
5254 #[test]
5255 fn connect_direct_auto_stub_returns_unsupported() {
5256 let code = unsafe { net_mesh_connect_direct_auto(ptr::null_mut(), 0, ptr::null()) };
5258 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5259 }
5260
5261 #[test]
5262 fn traversal_stats_v2_stub_returns_unsupported() {
5263 let code = unsafe { net_mesh_traversal_stats_v2(ptr::null_mut(), ptr::null_mut()) };
5265 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5266 }
5267
5268 #[test]
5269 fn set_reflex_override_stub_returns_unsupported() {
5270 let code = unsafe { net_mesh_set_reflex_override(ptr::null_mut(), ptr::null()) };
5272 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5273 }
5274
5275 #[test]
5276 fn clear_reflex_override_stub_returns_unsupported() {
5277 let code = unsafe { net_mesh_clear_reflex_override(ptr::null_mut()) };
5279 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5280 }
5281
5282 #[test]
5288 fn unsupported_code_is_stable() {
5289 assert_eq!(NET_ERR_TRAVERSAL_UNSUPPORTED, -137);
5290 }
5291
5292 #[test]
5296 fn capability_set_from_go_marshal_preserves_gpu_vendor() {
5297 let json = r#"{"hardware":{"cpu_cores":16,"memory_gb":64,"gpu":{"vendor":"nvidia","model":"h100","vram_gb":80}},"tags":["gpu"]}"#;
5298 let parsed: CapabilitySetJson = serde_json::from_str(json).expect("JSON should parse");
5299 let caps = capability_set_from_json(parsed);
5300 let views = caps.views();
5304 assert_eq!(
5305 views.hardware().gpu_vendor(),
5306 Some(super::GpuVendor::Nvidia),
5307 "vendor lost in conversion"
5308 );
5309 assert_eq!(views.hardware().memory_gb, 64);
5310 assert_eq!(views.hardware().total_vram_gb(), 80);
5311 assert!(caps.has_tag("gpu"));
5312 }
5313
5314 #[test]
5323 fn collect_payloads_rejects_null_entry_with_nonzero_length() {
5324 let buf_a = b"hello".as_slice();
5325 let buf_b = b"world".as_slice();
5326 let ptrs: [*const u8; 3] = [buf_a.as_ptr(), std::ptr::null(), buf_b.as_ptr()];
5327 let lens: [usize; 3] = [buf_a.len(), 4, buf_b.len()];
5328
5329 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 3) };
5330 assert!(
5331 result.is_none(),
5332 "null entry with non-zero length must reject the whole batch"
5333 );
5334 }
5335
5336 #[test]
5337 fn collect_payloads_allows_null_entry_with_zero_length() {
5338 let buf_a = b"hello".as_slice();
5339 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), std::ptr::null()];
5340 let lens: [usize; 2] = [buf_a.len(), 0];
5341
5342 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
5343 .expect("zero-length null is treated as empty payload");
5344 assert_eq!(result.len(), 2);
5345 assert_eq!(&result[0][..], b"hello");
5346 assert!(result[1].is_empty());
5347 }
5348
5349 #[test]
5350 fn collect_payloads_happy_path() {
5351 let buf_a = b"abc".as_slice();
5352 let buf_b = b"defg".as_slice();
5353 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), buf_b.as_ptr()];
5354 let lens: [usize; 2] = [buf_a.len(), buf_b.len()];
5355
5356 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
5357 .expect("non-null entries should succeed");
5358 assert_eq!(result.len(), 2);
5359 assert_eq!(&result[0][..], b"abc");
5360 assert_eq!(&result[1][..], b"defg");
5361 }
5362}
5363
5364#[cfg(all(test, feature = "net"))]
5365mod subnet_authority_config_tests {
5366 use super::*;
5379
5380 const AUTHORITY: &str = "d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7";
5381 const ROOT: &str = "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1";
5382
5383 fn parse(json: &str) -> Result<MeshNewConfig, serde_json::Error> {
5384 serde_json::from_str(json)
5385 }
5386
5387 #[test]
5390 fn trust_anchor_fields_parse_and_convert() {
5391 let cfg = parse(&format!(
5392 r#"{{"bind_addr":"127.0.0.1:0","psk_hex":"{psk}",
5393 "subnet_authorities":[{{"authority_hex":"{AUTHORITY}",
5394 "root_hexes":["{ROOT}"],"maximum_grant_lifetime_secs":604800}}],
5395 "subnet_attachment":[3,9],
5396 "subnet_control_channel":"subnet.control"}}"#,
5397 psk = "42".repeat(32),
5398 ))
5399 .expect("config parses");
5400
5401 let authorities = cfg.subnet_authorities.expect("authorities present");
5402 assert_eq!(authorities.len(), 1);
5403 let core = authorities[0].to_core().expect("converts");
5404 assert_eq!(core.maximum_grant_lifetime_secs, 604_800);
5405 assert_eq!(core.roots.len(), 1);
5406 assert!(
5407 crate::adapter::net::subnet::provision::validate_subnet_authorities(&[core]).is_ok(),
5408 "a well-formed anchor must validate",
5409 );
5410
5411 assert_eq!(cfg.subnet_attachment.as_deref(), Some(&[3u8, 9][..]));
5412 assert_eq!(
5413 cfg.subnet_control_channel.as_deref(),
5414 Some("subnet.control")
5415 );
5416 }
5417
5418 #[test]
5422 fn trust_anchor_fields_are_optional() {
5423 let cfg = parse(&format!(
5424 r#"{{"bind_addr":"127.0.0.1:0","psk_hex":"{}"}}"#,
5425 "42".repeat(32)
5426 ))
5427 .expect("config parses without any subnet authority field");
5428 assert!(cfg.subnet_authorities.is_none());
5429 assert!(cfg.subnet_attachment.is_none());
5430 assert!(cfg.subnet_control_channel.is_none());
5431 }
5432
5433 #[test]
5438 fn configuration_mistakes_are_refused() {
5439 use crate::adapter::net::subnet::provision::{
5440 dto::SubnetAuthorityConfigDto, validate_subnet_authorities,
5441 };
5442
5443 let good = SubnetAuthorityConfigDto {
5444 authority_hex: AUTHORITY.to_string(),
5445 root_hexes: vec![ROOT.to_string()],
5446 maximum_grant_lifetime_secs: 604_800,
5447 };
5448
5449 let bad_hex = SubnetAuthorityConfigDto {
5451 authority_hex: "not-hex".to_string(),
5452 ..good.clone()
5453 };
5454 assert!(bad_hex.to_core().is_err(), "a malformed id must be refused");
5455
5456 let empty_roots = SubnetAuthorityConfigDto {
5458 root_hexes: Vec::new(),
5459 ..good.clone()
5460 };
5461 assert!(validate_subnet_authorities(&[empty_roots.to_core().expect("converts")]).is_err());
5462
5463 let zero_life = SubnetAuthorityConfigDto {
5465 maximum_grant_lifetime_secs: 0,
5466 ..good.clone()
5467 };
5468 assert!(validate_subnet_authorities(&[zero_life.to_core().expect("converts")]).is_err());
5469
5470 let one = good.to_core().expect("converts");
5472 let two = good.to_core().expect("converts");
5473 assert!(validate_subnet_authorities(&[one, two]).is_err());
5474 }
5475
5476 #[test]
5490 fn the_go_bindings_emitted_config_deserializes() {
5491 const GO_EMITTED: &str = r#"{"bind_addr":"127.0.0.1:0","psk_hex":"4242424242424242424242424242424242424242424242424242424242424242","subnet_exports":[{"name":"factory-export","access":"granted","binding":{"subnet":{"authority_hex":"d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7","path":{"levels":[3,9]}},"topology_epoch":0}}],"subnet_authorities":[{"authority_hex":"d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7","root_hexes":["d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7"],"maximum_grant_lifetime_secs":604800}],"subnet_attachment":[3]}"#;
5492
5493 let cfg = parse(GO_EMITTED).expect("the Go binding's own JSON must deserialize");
5494 assert_eq!(cfg.subnet_attachment.as_deref(), Some(&[3u8][..]));
5495 let exports = cfg.subnet_exports.expect("exports present");
5496 assert_eq!(exports.len(), 1);
5497 let export = exports[0].to_core().expect("export converts");
5498 assert_eq!(export.name, "factory-export");
5499 let authorities = cfg.subnet_authorities.expect("authorities present");
5500 assert!(authorities[0].to_core().is_ok());
5501 }
5502
5503 #[test]
5506 fn a_base64_level_array_is_refused() {
5507 let base64_attachment =
5508 r#"{"bind_addr":"127.0.0.1:0","psk_hex":"42","subnet_attachment":"Awk="}"#;
5509 assert!(
5510 parse(base64_attachment).is_err(),
5511 "a base64 attachment must be refused, not silently accepted",
5512 );
5513 }
5514
5515 #[test]
5518 fn an_over_deep_attachment_is_refused() {
5519 use crate::adapter::net::subnet::provision::dto::SubnetPathDto;
5520 assert!(SubnetPathDto {
5521 levels: vec![1, 2, 3, 4, 5]
5522 }
5523 .to_core()
5524 .is_err());
5525 assert!(SubnetPathDto {
5526 levels: vec![1, 2, 3, 4]
5527 }
5528 .to_core()
5529 .is_ok());
5530 assert!(SubnetPathDto { levels: vec![] }.to_core().is_ok());
5531 }
5532}
5533
5534#[cfg(all(test, feature = "net"))]
5535mod named_export_construction_tests {
5536 use super::*;
5545 use crate::adapter::net::identity::EntityKeypair;
5546 use crate::adapter::net::subnet::provision::{NamedSubnetExport, SubnetExportAccess};
5547 use crate::adapter::net::subnet::{SubnetRef, TopologySubnetId};
5548
5549 fn export(name: &str) -> NamedSubnetExport {
5550 NamedSubnetExport {
5551 name: name.to_string(),
5552 access: SubnetExportAccess::Granted,
5553 subnet: SubnetRef {
5554 authority: EntityKeypair::from_bytes([0x11; 32]).entity_id().clone(),
5555 path: TopologySubnetId::new(&[3, 9]),
5556 },
5557 topology_epoch: 0,
5558 }
5559 }
5560
5561 async fn build(exports: Vec<NamedSubnetExport>) -> Result<MeshNode, AdapterError> {
5562 let mut cfg = MeshNodeConfig::new("127.0.0.1:0".parse().expect("addr"), [0u8; 32]);
5563 for e in exports {
5564 cfg = cfg.with_subnet_export(e);
5565 }
5566 MeshNode::new(EntityKeypair::generate(), cfg).await
5567 }
5568
5569 #[tokio::test]
5571 async fn configured_exports_are_resolvable_from_the_node() {
5572 let node = build(vec![export("factory-export"), export("lab-export")])
5573 .await
5574 .expect("distinct names construct");
5575 let map = node.subnet_exports();
5576 assert!(map.resolve("factory-export").is_some());
5577 assert!(map.resolve("lab-export").is_some());
5578 assert!(
5579 map.resolve("no-such-export").is_none(),
5580 "an unconfigured name must not resolve",
5581 );
5582 }
5583
5584 #[tokio::test]
5587 async fn a_duplicate_export_name_refuses_construction() {
5588 let Err(err) = build(vec![export("dup"), export("dup")]).await else {
5589 panic!("a duplicate label must refuse construction");
5590 };
5591 assert!(
5592 err.to_string().contains("duplicate_export_name"),
5593 "expected the stable kind in the refusal, got {err}",
5594 );
5595 }
5596
5597 #[tokio::test]
5599 async fn an_empty_export_name_refuses_construction() {
5600 let Err(err) = build(vec![export("")]).await else {
5601 panic!("an empty label must refuse construction");
5602 };
5603 assert!(
5604 err.to_string().contains("empty_export_name"),
5605 "expected the stable kind in the refusal, got {err}",
5606 );
5607 }
5608
5609 #[tokio::test]
5612 async fn no_exports_is_valid_and_resolves_nothing() {
5613 let node = build(Vec::new()).await.expect("no exports constructs");
5614 assert!(node.subnet_exports().is_empty());
5615 assert!(node.subnet_exports().resolve("anything").is_none());
5616 }
5617}