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, IdentityState as InnerIdentityState, PermissionToken, TokenCache,
63 TokenError as CoreTokenError, TokenScope, IDENTITY_STATE_SIZE,
64};
65use crate::adapter::net::{
66 ChannelConfig as InnerChannelConfig, ChannelConfigRegistry, ChannelHash, ChannelId,
67 ChannelName as InnerChannelName, ChannelPublisher, EntityKeypair, MeshNode, MeshNodeConfig,
68 OnFailure as InnerOnFailure, PublishConfig as InnerPublishConfig,
69 PublishReport as InnerPublishReport, Reliability, Stream as CoreStream, StreamConfig,
70 StreamError, Visibility as InnerVisibility, DEFAULT_STREAM_WINDOW_BYTES,
71};
72use crate::adapter::net::{SubnetId, SubnetPolicy, SubnetRule};
73use crate::adapter::Adapter;
74use crate::error::AdapterError;
75
76use super::handle_guard::{HandleGuard, FFI_HANDLE_FREE_DEADLINE};
77use super::NetError;
78
79pub(crate) const NET_ERR_MESH_INIT: c_int = -110;
85pub(crate) const NET_ERR_MESH_HANDSHAKE: c_int = -111;
86pub(crate) const NET_ERR_MESH_BACKPRESSURE: c_int = -112;
87pub(crate) const NET_ERR_MESH_NOT_CONNECTED: c_int = -113;
88pub(crate) const NET_ERR_MESH_TRANSPORT: c_int = -114;
89pub(crate) const NET_ERR_CHANNEL: c_int = -115;
90pub(crate) const NET_ERR_CHANNEL_AUTH: c_int = -116;
91
92pub(crate) const NET_ERR_IDENTITY: c_int = -120;
97pub(crate) const NET_ERR_TOKEN_INVALID_FORMAT: c_int = -121;
98pub(crate) const NET_ERR_TOKEN_INVALID_SIGNATURE: c_int = -122;
99pub(crate) const NET_ERR_TOKEN_EXPIRED: c_int = -123;
100pub(crate) const NET_ERR_TOKEN_NOT_YET_VALID: c_int = -124;
101pub(crate) const NET_ERR_TOKEN_DELEGATION_EXHAUSTED: c_int = -125;
102pub(crate) const NET_ERR_TOKEN_DELEGATION_NOT_ALLOWED: c_int = -126;
103pub(crate) const NET_ERR_TOKEN_NOT_AUTHORIZED: c_int = -127;
104
105#[cfg(feature = "nat-traversal")]
118pub(crate) const NET_ERR_TRAVERSAL_REFLEX_TIMEOUT: c_int = -130;
119#[cfg(feature = "nat-traversal")]
120pub(crate) const NET_ERR_TRAVERSAL_PEER_NOT_REACHABLE: c_int = -131;
121#[cfg(feature = "nat-traversal")]
122pub(crate) const NET_ERR_TRAVERSAL_TRANSPORT: c_int = -132;
123#[cfg(feature = "nat-traversal")]
124pub(crate) const NET_ERR_TRAVERSAL_RENDEZVOUS_NO_RELAY: c_int = -133;
125#[cfg(feature = "nat-traversal")]
126pub(crate) const NET_ERR_TRAVERSAL_RENDEZVOUS_REJECTED: c_int = -134;
127#[cfg(feature = "nat-traversal")]
128pub(crate) const NET_ERR_TRAVERSAL_PUNCH_FAILED: c_int = -135;
129#[cfg(feature = "nat-traversal")]
130pub(crate) const NET_ERR_TRAVERSAL_PORT_MAP_UNAVAILABLE: c_int = -136;
131pub(crate) const NET_ERR_TRAVERSAL_UNSUPPORTED: c_int = -137;
137
138#[cfg(feature = "nat-traversal")]
139fn traversal_err_to_code(e: &crate::adapter::net::traversal::TraversalError) -> c_int {
140 use crate::adapter::net::traversal::TraversalError;
141 match e {
142 TraversalError::ReflexTimeout => NET_ERR_TRAVERSAL_REFLEX_TIMEOUT,
143 TraversalError::PeerNotReachable => NET_ERR_TRAVERSAL_PEER_NOT_REACHABLE,
144 TraversalError::Transport(_) => NET_ERR_TRAVERSAL_TRANSPORT,
145 TraversalError::RendezvousNoRelay => NET_ERR_TRAVERSAL_RENDEZVOUS_NO_RELAY,
146 TraversalError::RendezvousRejected(_) => NET_ERR_TRAVERSAL_RENDEZVOUS_REJECTED,
147 TraversalError::PunchFailed => NET_ERR_TRAVERSAL_PUNCH_FAILED,
148 TraversalError::PortMapUnavailable => NET_ERR_TRAVERSAL_PORT_MAP_UNAVAILABLE,
149 TraversalError::Unsupported => NET_ERR_TRAVERSAL_UNSUPPORTED,
150 }
151}
152
153#[cfg(feature = "nat-traversal")]
157fn nat_class_to_str(class: crate::adapter::net::traversal::classify::NatClass) -> &'static str {
158 use crate::adapter::net::traversal::classify::NatClass;
159 match class {
160 NatClass::Open => "open",
161 NatClass::Cone => "cone",
162 NatClass::Symmetric => "symmetric",
163 NatClass::Unknown => "unknown",
164 }
165}
166
167fn token_err_to_code(e: &CoreTokenError) -> c_int {
168 match e {
169 CoreTokenError::InvalidFormat => NET_ERR_TOKEN_INVALID_FORMAT,
170 CoreTokenError::InvalidSignature => NET_ERR_TOKEN_INVALID_SIGNATURE,
171 CoreTokenError::Expired => NET_ERR_TOKEN_EXPIRED,
172 CoreTokenError::NotYetValid => NET_ERR_TOKEN_NOT_YET_VALID,
173 CoreTokenError::DelegationExhausted => NET_ERR_TOKEN_DELEGATION_EXHAUSTED,
174 CoreTokenError::DelegationNotAllowed => NET_ERR_TOKEN_DELEGATION_NOT_ALLOWED,
175 CoreTokenError::NotAuthorized => NET_ERR_TOKEN_NOT_AUTHORIZED,
176 CoreTokenError::Revoked => NET_ERR_TOKEN_NOT_AUTHORIZED,
181 CoreTokenError::ReadOnly => NET_ERR_IDENTITY,
186 CoreTokenError::ZeroTtl => NET_ERR_TOKEN_INVALID_FORMAT,
193 CoreTokenError::TtlTooLong => NET_ERR_TOKEN_INVALID_FORMAT,
197 }
198}
199
200fn runtime() -> &'static Arc<Runtime> {
216 use std::sync::OnceLock;
217 static RT: OnceLock<Arc<Runtime>> = OnceLock::new();
218 RT.get_or_init(|| {
219 match tokio::runtime::Builder::new_multi_thread()
220 .enable_all()
221 .build()
222 {
223 Ok(rt) => Arc::new(rt),
224 Err(e) => {
225 eprintln!(
226 "FATAL: mesh FFI tokio runtime build failure ({e:?}); aborting to avoid panic across the FFI boundary"
227 );
228 std::process::abort();
229 }
230 }
231 })
232}
233
234pub(super) fn block_on<F: std::future::Future>(future: F) -> F::Output {
254 if tokio::runtime::Handle::try_current().is_ok() {
255 eprintln!(
256 "FATAL: mesh FFI called from inside a tokio runtime context; \
257 aborting to avoid runtime-in-runtime panic across the FFI boundary"
258 );
259 std::process::abort();
260 }
261 runtime().block_on(future)
262}
263
264#[inline]
287pub(super) unsafe fn c_str_to_string(p: *const c_char) -> Option<String> {
288 if p.is_null() {
289 return None;
290 }
291 CStr::from_ptr(p).to_str().ok().map(str::to_owned)
292}
293
294fn write_json_out<T: Serialize>(
300 value: &T,
301 out_ptr: *mut *mut c_char,
302 out_len: *mut usize,
303) -> c_int {
304 if out_ptr.is_null() || out_len.is_null() {
305 return NetError::NullPointer.into();
306 }
307 let Ok(s) = serde_json::to_string(value) else {
308 return NetError::Unknown.into();
309 };
310 let len = s.len();
311 let Ok(cs) = CString::new(s) else {
312 return NetError::Unknown.into();
313 };
314 unsafe {
315 *out_ptr = cs.into_raw();
316 *out_len = len;
317 }
318 0
319}
320
321pub(super) fn write_string_out(s: String, out_ptr: *mut *mut c_char, out_len: *mut usize) -> c_int {
322 if out_ptr.is_null() || out_len.is_null() {
323 return NetError::NullPointer.into();
324 }
325 let len = s.len();
326 let Ok(cs) = CString::new(s) else {
327 return NetError::Unknown.into();
328 };
329 unsafe {
330 *out_ptr = cs.into_raw();
331 *out_len = len;
332 }
333 0
334}
335
336fn adapter_err_to_code(err: &AdapterError) -> c_int {
337 match err {
338 AdapterError::Connection(_) => NET_ERR_MESH_HANDSHAKE,
339 _ => NET_ERR_MESH_TRANSPORT,
340 }
341}
342
343fn stream_err_to_code(err: &StreamError) -> c_int {
344 match err {
345 StreamError::Backpressure => NET_ERR_MESH_BACKPRESSURE,
346 StreamError::NotConnected => NET_ERR_MESH_NOT_CONNECTED,
347 StreamError::Transport(_) => NET_ERR_MESH_TRANSPORT,
348 }
349}
350
351#[derive(Deserialize)]
356struct SubnetPolicyJson {
357 #[serde(default)]
358 rules: Vec<SubnetRuleJson>,
359}
360
361#[derive(Deserialize)]
362struct SubnetRuleJson {
363 tag_prefix: String,
364 level: u32,
365 #[serde(default)]
366 values: std::collections::HashMap<String, u32>,
367}
368
369fn u8_from_u32(value: u32) -> Option<u8> {
370 if value > 255 {
371 None
372 } else {
373 Some(value as u8)
374 }
375}
376
377fn subnet_id_from_json(levels: Vec<u32>) -> Option<SubnetId> {
378 if levels.is_empty() || levels.len() > 4 {
379 return None;
380 }
381 let mut bytes = [0u8; 4];
382 for (i, raw) in levels.iter().enumerate() {
383 bytes[i] = u8_from_u32(*raw)?;
384 }
385 Some(SubnetId::new(&bytes[..levels.len()]))
386}
387
388fn subnet_policy_from_json(p: SubnetPolicyJson) -> Option<SubnetPolicy> {
389 let mut policy = SubnetPolicy::new();
390 for rule_json in p.rules {
391 let level = u8_from_u32(rule_json.level)?;
392 if level > 3 {
393 return None;
394 }
395 let mut rule = SubnetRule::new(rule_json.tag_prefix, level);
396 for (tag_value, raw_val) in rule_json.values {
397 let v = u8_from_u32(raw_val)?;
398 if v == 0 {
404 return None;
405 }
406 rule = rule.map(tag_value, v);
407 }
408 policy = policy.add_rule(rule);
409 }
410 Some(policy)
411}
412
413#[derive(Deserialize)]
414struct MeshNewConfig {
415 bind_addr: String,
416 psk_hex: String,
418 heartbeat_ms: Option<u64>,
419 session_timeout_ms: Option<u64>,
420 num_shards: Option<u16>,
421 capability_gc_interval_ms: Option<u64>,
424 require_signed_capabilities: Option<bool>,
427 subnet: Option<Vec<u32>>,
429 subnet_policy: Option<SubnetPolicyJson>,
431 #[serde(default)]
442 subnet_authorities:
443 Option<Vec<crate::adapter::net::subnet::provision::dto::SubnetAuthorityConfigDto>>,
444 #[serde(default)]
450 subnet_attachment: Option<Vec<u8>>,
451 #[serde(default)]
455 subnet_control_channel: Option<String>,
456 #[serde(default)]
468 subnet_exports: Option<Vec<crate::adapter::net::subnet::provision::dto::SubnetNamedExportDto>>,
469 identity_seed_hex: Option<String>,
474 #[serde(default)]
480 reflex_override: Option<String>,
481 #[serde(default)]
485 try_port_mapping: bool,
486 #[serde(default)]
497 auto_direct_upgrade: Option<bool>,
498}
499
500pub struct MeshNodeHandle {
513 inner: ManuallyDrop<Arc<MeshNode>>,
514 channel_configs: ManuallyDrop<Arc<ChannelConfigRegistry>>,
515 guard: HandleGuard,
516}
517
518#[unsafe(no_mangle)]
533pub unsafe extern "C" fn net_mesh_new(
534 config_json: *const c_char,
535 out_handle: *mut *mut MeshNodeHandle,
536) -> c_int {
537 if config_json.is_null() || out_handle.is_null() {
538 return NetError::NullPointer.into();
539 }
540 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
541 return NetError::InvalidUtf8.into();
542 };
543 let cfg: MeshNewConfig = match serde_json::from_str(&s) {
544 Ok(v) => v,
545 Err(_) => return NetError::InvalidJson.into(),
546 };
547 let bind_addr: std::net::SocketAddr = match cfg.bind_addr.parse() {
548 Ok(a) => a,
549 Err(_) => return NET_ERR_MESH_INIT,
550 };
551 let psk_bytes = match hex::decode(&cfg.psk_hex) {
552 Ok(b) => b,
553 Err(_) => return NET_ERR_MESH_INIT,
554 };
555 if psk_bytes.len() != 32 {
556 return NET_ERR_MESH_INIT;
557 }
558 let mut psk = [0u8; 32];
559 psk.copy_from_slice(&psk_bytes);
560
561 let mut node_cfg = MeshNodeConfig::new(bind_addr, psk);
562 if let Some(ms) = cfg.heartbeat_ms {
570 if ms == 0 {
571 return NetError::InvalidJson.into();
572 }
573 node_cfg = node_cfg.with_heartbeat_interval(std::time::Duration::from_millis(ms));
574 }
575 if let Some(ms) = cfg.session_timeout_ms {
576 if ms == 0 {
577 return NetError::InvalidJson.into();
578 }
579 node_cfg = node_cfg.with_session_timeout(std::time::Duration::from_millis(ms));
580 }
581 if let Some(n) = cfg.num_shards {
582 node_cfg = node_cfg.with_num_shards(n);
583 }
584 if let Some(ms) = cfg.capability_gc_interval_ms {
585 node_cfg = node_cfg.with_capability_gc_interval(std::time::Duration::from_millis(ms));
586 }
587 if let Some(b) = cfg.require_signed_capabilities {
588 node_cfg = node_cfg.with_require_signed_capabilities(b);
589 }
590 if let Some(levels) = cfg.subnet {
591 let Some(id) = subnet_id_from_json(levels) else {
592 return NET_ERR_MESH_INIT;
593 };
594 node_cfg = node_cfg.with_subnet(id);
595 }
596 if let Some(policy_js) = cfg.subnet_policy {
597 let Some(policy) = subnet_policy_from_json(policy_js) else {
598 return NET_ERR_MESH_INIT;
599 };
600 node_cfg = node_cfg.with_subnet_policy(Arc::new(policy));
601 }
602 {
610 use crate::adapter::net::subnet::provision;
611 let authorities = cfg.subnet_authorities.unwrap_or_default();
612 let mut core_authorities = Vec::with_capacity(authorities.len());
613 for dto in &authorities {
614 let Ok(a) = dto.to_core() else {
615 return NET_ERR_MESH_INIT;
616 };
617 core_authorities.push(a);
618 }
619 if provision::validate_subnet_authorities(&core_authorities).is_err() {
620 return NET_ERR_MESH_INIT;
621 }
622 for authority in core_authorities {
623 node_cfg = node_cfg.with_subnet_authority(authority);
624 }
625 if let Some(levels) = cfg.subnet_attachment {
626 let Ok(path) = (provision::dto::SubnetPathDto { levels }).to_core() else {
627 return NET_ERR_MESH_INIT;
628 };
629 node_cfg.subnet_attachment = Some(path);
633 }
634 if let Some(name) = cfg.subnet_control_channel {
635 let Ok(channel) = crate::adapter::net::ChannelName::new(&name) else {
636 return NET_ERR_MESH_INIT;
637 };
638 node_cfg = node_cfg.with_subnet_control_channel(channel);
639 }
640 for dto in cfg.subnet_exports.unwrap_or_default().iter() {
641 let Ok(export) = dto.to_core() else {
642 return NET_ERR_MESH_INIT;
643 };
644 node_cfg = node_cfg.with_subnet_export(export);
645 }
646 }
649 #[cfg(feature = "nat-traversal")]
650 if let Some(external_str) = cfg.reflex_override.as_deref() {
651 let Ok(external) = external_str.parse::<std::net::SocketAddr>() else {
652 return NET_ERR_MESH_INIT;
653 };
654 node_cfg = node_cfg.with_reflex_override(external);
655 }
656 #[cfg(not(feature = "nat-traversal"))]
660 let _ = cfg.reflex_override;
661 #[cfg(feature = "port-mapping")]
662 if cfg.try_port_mapping {
663 node_cfg = node_cfg.with_try_port_mapping(true);
664 }
665 #[cfg(not(feature = "port-mapping"))]
667 let _ = cfg.try_port_mapping;
668 #[cfg(feature = "nat-traversal")]
669 if let Some(enabled) = cfg.auto_direct_upgrade {
670 node_cfg = node_cfg.with_auto_direct_upgrade(enabled);
671 }
672 #[cfg(not(feature = "nat-traversal"))]
674 let _ = cfg.auto_direct_upgrade;
675
676 node_cfg.configured_identity = cfg.identity_seed_hex.is_some();
683
684 let identity = match cfg.identity_seed_hex {
685 Some(seed_hex) => {
686 let bytes = match hex::decode(&seed_hex) {
687 Ok(b) => b,
688 Err(_) => return NET_ERR_MESH_INIT,
689 };
690 if bytes.len() != 32 {
691 return NET_ERR_MESH_INIT;
692 }
693 let mut arr = [0u8; 32];
694 arr.copy_from_slice(&bytes);
695 EntityKeypair::from_bytes(arr)
696 }
697 None => EntityKeypair::generate(),
698 };
699 let result = block_on(async move { MeshNode::new(identity, node_cfg).await });
700 match result {
701 Ok(mut node) => {
702 let channel_configs = Arc::new(ChannelConfigRegistry::new());
703 node.set_channel_configs(channel_configs.clone());
704 node.set_token_cache(Arc::new(TokenCache::new()));
711 let handle = Box::new(MeshNodeHandle {
712 inner: ManuallyDrop::new(Arc::new(node)),
713 channel_configs: ManuallyDrop::new(channel_configs),
714 guard: HandleGuard::new(),
715 });
716 unsafe {
717 *out_handle = Box::into_raw(handle);
718 }
719 0
720 }
721 Err(_) => NET_ERR_MESH_INIT,
722 }
723}
724
725#[unsafe(no_mangle)]
726pub unsafe extern "C" fn net_mesh_free(handle: *mut MeshNodeHandle) {
727 if handle.is_null() {
728 return;
729 }
730 let h: &MeshNodeHandle = unsafe { &*handle };
735 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
736 unsafe {
738 let mh = &mut *handle;
739 let inner = ManuallyDrop::take(&mut mh.inner);
740 let configs = ManuallyDrop::take(&mut mh.channel_configs);
741 drop(inner);
742 drop(configs);
743 }
744 } else {
745 tracing::warn!(
746 "net_mesh_free: in-flight ops did not drain within deadline; \
747 leaking inner to avoid use-after-free"
748 );
749 }
750}
751
752#[cfg(any(feature = "cortex", feature = "dataforts"))]
770pub(super) fn mesh_node_arc(h: &MeshNodeHandle) -> Option<Arc<MeshNode>> {
771 let _op = h.guard.try_enter()?;
772 Some(Arc::clone(&h.inner))
773}
774
775#[unsafe(no_mangle)]
783pub unsafe extern "C" fn net_mesh_arc_clone(handle: *mut MeshNodeHandle) -> *mut Arc<MeshNode> {
784 if handle.is_null() {
785 return std::ptr::null_mut();
786 }
787 let h = unsafe { &*handle };
788 let _op = match h.guard.try_enter() {
790 Some(op) => op,
791 None => return std::ptr::null_mut(),
792 };
793 let cloned: Arc<MeshNode> = Arc::clone(&h.inner);
794 Box::into_raw(Box::new(cloned))
795}
796
797#[unsafe(no_mangle)]
804pub unsafe extern "C" fn net_mesh_channel_configs_arc_clone(
805 handle: *mut MeshNodeHandle,
806) -> *mut Arc<ChannelConfigRegistry> {
807 if handle.is_null() {
808 return std::ptr::null_mut();
809 }
810 let h = unsafe { &*handle };
811 let _op = match h.guard.try_enter() {
813 Some(op) => op,
814 None => return std::ptr::null_mut(),
815 };
816 let cloned: Arc<ChannelConfigRegistry> = Arc::clone(&h.channel_configs);
817 Box::into_raw(Box::new(cloned))
818}
819
820#[unsafe(no_mangle)]
823pub unsafe extern "C" fn net_mesh_arc_free(p: *mut Arc<MeshNode>) {
824 if p.is_null() {
825 return;
826 }
827 unsafe {
828 drop(Box::from_raw(p));
829 }
830}
831
832#[unsafe(no_mangle)]
835pub unsafe extern "C" fn net_mesh_channel_configs_arc_free(p: *mut Arc<ChannelConfigRegistry>) {
836 if p.is_null() {
837 return;
838 }
839 unsafe {
840 drop(Box::from_raw(p));
841 }
842}
843
844#[unsafe(no_mangle)]
847pub unsafe extern "C" fn net_mesh_public_key_hex(
848 handle: *mut MeshNodeHandle,
849 out_ptr: *mut *mut c_char,
850 out_len: *mut usize,
851) -> c_int {
852 if handle.is_null() || out_ptr.is_null() || out_len.is_null() {
853 return NetError::NullPointer.into();
854 }
855 let h = unsafe { &*handle };
856 let _op = match h.guard.try_enter() {
857 Some(op) => op,
858 None => return NetError::ShuttingDown.into(),
859 };
860 let s = hex::encode(h.inner.public_key());
861 write_string_out(s, out_ptr, out_len)
862}
863
864#[unsafe(no_mangle)]
865pub unsafe extern "C" fn net_mesh_node_id(handle: *mut MeshNodeHandle) -> u64 {
866 if handle.is_null() {
867 return 0;
868 }
869 let h = unsafe { &*handle };
870 let _op = match h.guard.try_enter() {
872 Some(op) => op,
873 None => return 0,
874 };
875 h.inner.node_id()
876}
877
878#[unsafe(no_mangle)]
882pub unsafe extern "C" fn net_mesh_entity_id(handle: *mut MeshNodeHandle, out: *mut u8) -> c_int {
883 if handle.is_null() || out.is_null() {
884 return NetError::NullPointer.into();
885 }
886 let h = unsafe { &*handle };
887 let _op = match h.guard.try_enter() {
888 Some(op) => op,
889 None => return NetError::ShuttingDown.into(),
890 };
891 let bytes = h.inner.entity_id().as_bytes();
892 unsafe {
893 std::ptr::copy_nonoverlapping(bytes.as_ptr(), out, 32);
894 }
895 0
896}
897unsafe fn parse_peer_pubkey_hex(peer_pubkey_hex: *const c_char) -> Result<[u8; 32], c_int> {
910 let Some(pk_s) = (unsafe { c_str_to_string(peer_pubkey_hex) }) else {
911 return Err(NetError::InvalidUtf8.into());
912 };
913 let pk_bytes = match hex::decode(pk_s) {
914 Ok(b) => b,
915 Err(_) => return Err(NET_ERR_MESH_HANDSHAKE),
916 };
917 if pk_bytes.len() != 32 {
918 return Err(NET_ERR_MESH_HANDSHAKE);
919 }
920 let mut pk = [0u8; 32];
921 pk.copy_from_slice(&pk_bytes);
922 Ok(pk)
923}
924
925#[unsafe(no_mangle)]
927pub unsafe extern "C" fn net_mesh_connect(
928 handle: *mut MeshNodeHandle,
929 peer_addr: *const c_char,
930 peer_pubkey_hex: *const c_char,
931 peer_node_id: u64,
932) -> c_int {
933 if handle.is_null() || peer_addr.is_null() || peer_pubkey_hex.is_null() {
934 return NetError::NullPointer.into();
935 }
936 let h = unsafe { &*handle };
937 let _op = match h.guard.try_enter() {
938 Some(op) => op,
939 None => return NetError::ShuttingDown.into(),
940 };
941 let Some(addr_s) = (unsafe { c_str_to_string(peer_addr) }) else {
942 return NetError::InvalidUtf8.into();
943 };
944 let addr: std::net::SocketAddr = match addr_s.parse() {
945 Ok(a) => a,
946 Err(_) => return NET_ERR_MESH_HANDSHAKE,
947 };
948 let pk = match unsafe { parse_peer_pubkey_hex(peer_pubkey_hex) } {
949 Ok(pk) => pk,
950 Err(code) => return code,
951 };
952
953 let node = h.inner.clone();
954 match block_on(async move { node.connect(addr, &pk, peer_node_id).await }) {
955 Ok(_) => 0,
956 Err(e) => adapter_err_to_code(&e),
957 }
958}
959
960#[unsafe(no_mangle)]
963pub unsafe extern "C" fn net_mesh_accept(
964 handle: *mut MeshNodeHandle,
965 peer_node_id: u64,
966 out_addr: *mut *mut c_char,
967 out_len: *mut usize,
968) -> c_int {
969 if handle.is_null() || out_addr.is_null() || out_len.is_null() {
970 return NetError::NullPointer.into();
971 }
972 let h = unsafe { &*handle };
973 let _op = match h.guard.try_enter() {
974 Some(op) => op,
975 None => return NetError::ShuttingDown.into(),
976 };
977 let node = h.inner.clone();
978 match block_on(async move { node.accept(peer_node_id).await }) {
979 Ok((addr, _)) => write_string_out(addr.to_string(), out_addr, out_len),
980 Err(e) => adapter_err_to_code(&e),
981 }
982}
983
984#[unsafe(no_mangle)]
985pub unsafe extern "C" fn net_mesh_start(handle: *mut MeshNodeHandle) -> c_int {
986 if handle.is_null() {
987 return NetError::NullPointer.into();
988 }
989 let h = unsafe { &*handle };
990 let _op = match h.guard.try_enter() {
991 Some(op) => op,
992 None => return NetError::ShuttingDown.into(),
993 };
994 let node = h.inner.clone();
995 block_on(async move { node.start_arc() });
999 0
1000}
1001
1002#[unsafe(no_mangle)]
1014pub unsafe extern "C" fn net_mesh_shutdown(handle: *mut MeshNodeHandle) -> c_int {
1015 if handle.is_null() {
1016 return NetError::NullPointer.into();
1017 }
1018 let h = unsafe { &*handle };
1019 let _op = match h.guard.try_enter() {
1020 Some(op) => op,
1021 None => return NetError::ShuttingDown.into(),
1022 };
1023 match block_on(async { h.inner.shutdown().await }) {
1024 Ok(()) => 0,
1025 Err(e) => adapter_err_to_code(&e),
1026 }
1027}
1028
1029#[cfg(feature = "nat-traversal")]
1051#[unsafe(no_mangle)]
1052pub unsafe extern "C" fn net_mesh_nat_type(
1053 handle: *mut MeshNodeHandle,
1054 out_str: *mut *mut c_char,
1055 out_len: *mut usize,
1056) -> c_int {
1057 if handle.is_null() || out_str.is_null() || out_len.is_null() {
1058 return NetError::NullPointer.into();
1059 }
1060 let h = unsafe { &*handle };
1061 let _op = match h.guard.try_enter() {
1062 Some(op) => op,
1063 None => return NetError::ShuttingDown.into(),
1064 };
1065 write_string_out(
1066 nat_class_to_str(h.inner.nat_class()).to_string(),
1067 out_str,
1068 out_len,
1069 )
1070}
1071
1072#[cfg(feature = "nat-traversal")]
1077#[unsafe(no_mangle)]
1078pub unsafe extern "C" fn net_mesh_reflex_addr(
1079 handle: *mut MeshNodeHandle,
1080 out_str: *mut *mut c_char,
1081 out_len: *mut usize,
1082) -> c_int {
1083 if handle.is_null() || out_str.is_null() || out_len.is_null() {
1084 return NetError::NullPointer.into();
1085 }
1086 let h = unsafe { &*handle };
1087 let _op = match h.guard.try_enter() {
1088 Some(op) => op,
1089 None => return NetError::ShuttingDown.into(),
1090 };
1091 let s = h
1092 .inner
1093 .reflex_addr()
1094 .map(|a| a.to_string())
1095 .unwrap_or_default();
1096 write_string_out(s, out_str, out_len)
1097}
1098
1099#[cfg(feature = "nat-traversal")]
1103#[unsafe(no_mangle)]
1104pub unsafe extern "C" fn net_mesh_peer_nat_type(
1105 handle: *mut MeshNodeHandle,
1106 peer_node_id: u64,
1107 out_str: *mut *mut c_char,
1108 out_len: *mut usize,
1109) -> c_int {
1110 if handle.is_null() || out_str.is_null() || out_len.is_null() {
1111 return NetError::NullPointer.into();
1112 }
1113 let h = unsafe { &*handle };
1114 let _op = match h.guard.try_enter() {
1115 Some(op) => op,
1116 None => return NetError::ShuttingDown.into(),
1117 };
1118 write_string_out(
1119 nat_class_to_str(h.inner.peer_nat_class(peer_node_id)).to_string(),
1120 out_str,
1121 out_len,
1122 )
1123}
1124
1125#[cfg(feature = "nat-traversal")]
1134#[unsafe(no_mangle)]
1135pub unsafe extern "C" fn net_mesh_probe_reflex(
1136 handle: *mut MeshNodeHandle,
1137 peer_node_id: u64,
1138 out_str: *mut *mut c_char,
1139 out_len: *mut usize,
1140) -> c_int {
1141 if handle.is_null() || out_str.is_null() || out_len.is_null() {
1142 return NetError::NullPointer.into();
1143 }
1144 let h = unsafe { &*handle };
1145 let _op = match h.guard.try_enter() {
1146 Some(op) => op,
1147 None => return NetError::ShuttingDown.into(),
1148 };
1149 let node = h.inner.clone();
1150 match block_on(async move { node.probe_reflex(peer_node_id).await }) {
1151 Ok(addr) => write_string_out(addr.to_string(), out_str, out_len),
1152 Err(e) => traversal_err_to_code(&e),
1153 }
1154}
1155
1156#[cfg(feature = "nat-traversal")]
1161#[unsafe(no_mangle)]
1162pub unsafe extern "C" fn net_mesh_reclassify_nat(handle: *mut MeshNodeHandle) -> c_int {
1163 if handle.is_null() {
1164 return NetError::NullPointer.into();
1165 }
1166 let h = unsafe { &*handle };
1167 let _op = match h.guard.try_enter() {
1168 Some(op) => op,
1169 None => return NetError::ShuttingDown.into(),
1170 };
1171 let node = h.inner.clone();
1172 block_on(async move { node.reclassify_nat().await });
1173 0
1174}
1175
1176#[cfg(feature = "nat-traversal")]
1181#[unsafe(no_mangle)]
1182pub unsafe extern "C" fn net_mesh_traversal_stats(
1183 handle: *mut MeshNodeHandle,
1184 out_punches_attempted: *mut u64,
1185 out_punches_succeeded: *mut u64,
1186 out_relay_fallbacks: *mut u64,
1187) -> c_int {
1188 if handle.is_null() {
1189 return NetError::NullPointer.into();
1190 }
1191 let h = unsafe { &*handle };
1192 let _op = match h.guard.try_enter() {
1193 Some(op) => op,
1194 None => return NetError::ShuttingDown.into(),
1195 };
1196 let snap = h.inner.traversal_stats();
1197 unsafe {
1198 if !out_punches_attempted.is_null() {
1199 *out_punches_attempted = snap.punches_attempted;
1200 }
1201 if !out_punches_succeeded.is_null() {
1202 *out_punches_succeeded = snap.punches_succeeded;
1203 }
1204 if !out_relay_fallbacks.is_null() {
1205 *out_relay_fallbacks = snap.relay_fallbacks;
1206 }
1207 }
1208 0
1209}
1210
1211#[cfg(feature = "nat-traversal")]
1223#[unsafe(no_mangle)]
1224pub unsafe extern "C" fn net_mesh_connect_direct(
1225 handle: *mut MeshNodeHandle,
1226 peer_node_id: u64,
1227 peer_pubkey_hex: *const c_char,
1228 coordinator: u64,
1229) -> c_int {
1230 if handle.is_null() || peer_pubkey_hex.is_null() {
1231 return NetError::NullPointer.into();
1232 }
1233 let h = unsafe { &*handle };
1234 let _op = match h.guard.try_enter() {
1235 Some(op) => op,
1236 None => return NetError::ShuttingDown.into(),
1237 };
1238 let pk = match unsafe { parse_peer_pubkey_hex(peer_pubkey_hex) } {
1239 Ok(pk) => pk,
1240 Err(code) => return code,
1241 };
1242
1243 let node = h.inner.clone();
1244 match block_on(async move { node.connect_direct(peer_node_id, &pk, coordinator).await }) {
1245 Ok(_) => 0,
1246 Err(e) => traversal_err_to_code(&e),
1247 }
1248}
1249
1250#[cfg(feature = "nat-traversal")]
1259#[unsafe(no_mangle)]
1260pub unsafe extern "C" fn net_mesh_connect_direct_auto(
1261 handle: *mut MeshNodeHandle,
1262 peer_node_id: u64,
1263 peer_pubkey_hex: *const c_char,
1264) -> c_int {
1265 if handle.is_null() || peer_pubkey_hex.is_null() {
1266 return NetError::NullPointer.into();
1267 }
1268 let h = unsafe { &*handle };
1269 let _op = match h.guard.try_enter() {
1270 Some(op) => op,
1271 None => return NetError::ShuttingDown.into(),
1272 };
1273 let pk = match unsafe { parse_peer_pubkey_hex(peer_pubkey_hex) } {
1274 Ok(pk) => pk,
1275 Err(code) => return code,
1276 };
1277
1278 let node = h.inner.clone();
1279 match block_on(async move { node.connect_direct_auto(peer_node_id, &pk).await }) {
1280 Ok(_) => 0,
1281 Err(e) => traversal_err_to_code(&e),
1282 }
1283}
1284
1285#[repr(C)]
1291pub struct NetTraversalStatsV2 {
1292 pub punches_attempted: u64,
1294 pub punches_succeeded: u64,
1296 pub punches_failed: u64,
1298 pub relay_fallbacks: u64,
1300 pub punch_timeouts: u64,
1302 pub punch_rejections: u64,
1304 pub rendezvous_no_relay: u64,
1306 pub upgrades_attempted: u64,
1308 pub upgrades_succeeded: u64,
1310 pub upgrades_deferred_busy: u64,
1312 pub port_mapping_renewals: u64,
1314 pub port_mapping_active: u8,
1316 pub port_mapping_external: [c_char; 64],
1320}
1321
1322#[cfg(feature = "nat-traversal")]
1326fn fill_traversal_stats_v2(
1327 snap: &crate::adapter::net::traversal::TraversalStatsSnapshot,
1328 out: &mut NetTraversalStatsV2,
1329) {
1330 out.punches_attempted = snap.punches_attempted;
1331 out.punches_succeeded = snap.punches_succeeded;
1332 out.punches_failed = snap.punches_failed;
1333 out.relay_fallbacks = snap.relay_fallbacks;
1334 out.punch_timeouts = snap.punch_timeouts;
1335 out.punch_rejections = snap.punch_rejections;
1336 out.rendezvous_no_relay = snap.rendezvous_no_relay;
1337 out.upgrades_attempted = snap.upgrades_attempted;
1338 out.upgrades_succeeded = snap.upgrades_succeeded;
1339 out.upgrades_deferred_busy = snap.upgrades_deferred_busy;
1340 out.port_mapping_renewals = snap.port_mapping_renewals;
1341 out.port_mapping_active = u8::from(snap.port_mapping_active);
1342 out.port_mapping_external = [0; 64];
1343 if let Some(addr) = snap.port_mapping_external {
1344 let s = addr.to_string();
1345 let n = s.len().min(63);
1350 for (dst, src) in out.port_mapping_external[..n].iter_mut().zip(s.as_bytes()) {
1351 *dst = *src as c_char;
1352 }
1353 }
1354}
1355
1356#[cfg(feature = "nat-traversal")]
1367#[unsafe(no_mangle)]
1368pub unsafe extern "C" fn net_mesh_traversal_stats_v2(
1369 handle: *mut MeshNodeHandle,
1370 out: *mut NetTraversalStatsV2,
1371) -> c_int {
1372 if handle.is_null() || out.is_null() {
1373 return NetError::NullPointer.into();
1374 }
1375 let h = unsafe { &*handle };
1376 let _op = match h.guard.try_enter() {
1377 Some(op) => op,
1378 None => return NetError::ShuttingDown.into(),
1379 };
1380 let snap = h.inner.traversal_stats();
1381 fill_traversal_stats_v2(&snap, unsafe { &mut *out });
1382 0
1383}
1384
1385#[cfg(feature = "nat-traversal")]
1393#[unsafe(no_mangle)]
1394pub unsafe extern "C" fn net_mesh_set_reflex_override(
1395 handle: *mut MeshNodeHandle,
1396 external: *const c_char,
1397) -> c_int {
1398 if handle.is_null() || external.is_null() {
1399 return NetError::NullPointer.into();
1400 }
1401 let h = unsafe { &*handle };
1402 let _op = match h.guard.try_enter() {
1403 Some(op) => op,
1404 None => return NetError::ShuttingDown.into(),
1405 };
1406 let Some(s) = (unsafe { c_str_to_string(external) }) else {
1407 return NetError::InvalidUtf8.into();
1408 };
1409 let Ok(addr) = s.parse::<std::net::SocketAddr>() else {
1410 return NET_ERR_MESH_INIT;
1411 };
1412 h.inner.set_reflex_override(addr);
1413 0
1414}
1415
1416#[cfg(feature = "nat-traversal")]
1424#[unsafe(no_mangle)]
1425pub unsafe extern "C" fn net_mesh_clear_reflex_override(handle: *mut MeshNodeHandle) -> c_int {
1426 if handle.is_null() {
1427 return NetError::NullPointer.into();
1428 }
1429 let h = unsafe { &*handle };
1430 let _op = match h.guard.try_enter() {
1431 Some(op) => op,
1432 None => return NetError::ShuttingDown.into(),
1433 };
1434 h.inner.clear_reflex_override();
1435 0
1436}
1437
1438#[cfg(not(feature = "nat-traversal"))]
1461#[unsafe(no_mangle)]
1462pub unsafe extern "C" fn net_mesh_nat_type(
1463 _handle: *mut MeshNodeHandle,
1464 _out_str: *mut *mut c_char,
1465 _out_len: *mut usize,
1466) -> c_int {
1467 NET_ERR_TRAVERSAL_UNSUPPORTED
1468}
1469
1470#[cfg(not(feature = "nat-traversal"))]
1471#[unsafe(no_mangle)]
1472pub unsafe extern "C" fn net_mesh_reflex_addr(
1473 _handle: *mut MeshNodeHandle,
1474 _out_str: *mut *mut c_char,
1475 _out_len: *mut usize,
1476) -> c_int {
1477 NET_ERR_TRAVERSAL_UNSUPPORTED
1478}
1479
1480#[cfg(not(feature = "nat-traversal"))]
1481#[unsafe(no_mangle)]
1482pub unsafe extern "C" fn net_mesh_peer_nat_type(
1483 _handle: *mut MeshNodeHandle,
1484 _peer_node_id: u64,
1485 _out_str: *mut *mut c_char,
1486 _out_len: *mut usize,
1487) -> c_int {
1488 NET_ERR_TRAVERSAL_UNSUPPORTED
1489}
1490
1491#[cfg(not(feature = "nat-traversal"))]
1492#[unsafe(no_mangle)]
1493pub unsafe extern "C" fn net_mesh_probe_reflex(
1494 _handle: *mut MeshNodeHandle,
1495 _peer_node_id: u64,
1496 _out_str: *mut *mut c_char,
1497 _out_len: *mut usize,
1498) -> c_int {
1499 NET_ERR_TRAVERSAL_UNSUPPORTED
1500}
1501
1502#[cfg(not(feature = "nat-traversal"))]
1503#[unsafe(no_mangle)]
1504pub unsafe extern "C" fn net_mesh_reclassify_nat(_handle: *mut MeshNodeHandle) -> c_int {
1505 NET_ERR_TRAVERSAL_UNSUPPORTED
1506}
1507
1508#[cfg(not(feature = "nat-traversal"))]
1509#[unsafe(no_mangle)]
1510pub unsafe extern "C" fn net_mesh_traversal_stats(
1511 _handle: *mut MeshNodeHandle,
1512 _out_punches_attempted: *mut u64,
1513 _out_punches_succeeded: *mut u64,
1514 _out_relay_fallbacks: *mut u64,
1515) -> c_int {
1516 NET_ERR_TRAVERSAL_UNSUPPORTED
1517}
1518
1519#[cfg(not(feature = "nat-traversal"))]
1520#[unsafe(no_mangle)]
1521pub unsafe extern "C" fn net_mesh_connect_direct(
1522 _handle: *mut MeshNodeHandle,
1523 _peer_node_id: u64,
1524 _peer_pubkey_hex: *const c_char,
1525 _coordinator: u64,
1526) -> c_int {
1527 NET_ERR_TRAVERSAL_UNSUPPORTED
1528}
1529
1530#[cfg(not(feature = "nat-traversal"))]
1531#[unsafe(no_mangle)]
1532pub unsafe extern "C" fn net_mesh_connect_direct_auto(
1533 _handle: *mut MeshNodeHandle,
1534 _peer_node_id: u64,
1535 _peer_pubkey_hex: *const c_char,
1536) -> c_int {
1537 NET_ERR_TRAVERSAL_UNSUPPORTED
1538}
1539
1540#[cfg(not(feature = "nat-traversal"))]
1541#[unsafe(no_mangle)]
1542pub unsafe extern "C" fn net_mesh_traversal_stats_v2(
1543 _handle: *mut MeshNodeHandle,
1544 _out: *mut NetTraversalStatsV2,
1545) -> c_int {
1546 NET_ERR_TRAVERSAL_UNSUPPORTED
1547}
1548
1549#[cfg(not(feature = "nat-traversal"))]
1550#[unsafe(no_mangle)]
1551pub unsafe extern "C" fn net_mesh_set_reflex_override(
1552 _handle: *mut MeshNodeHandle,
1553 _external: *const c_char,
1554) -> c_int {
1555 NET_ERR_TRAVERSAL_UNSUPPORTED
1556}
1557
1558#[cfg(not(feature = "nat-traversal"))]
1559#[unsafe(no_mangle)]
1560pub unsafe extern "C" fn net_mesh_clear_reflex_override(_handle: *mut MeshNodeHandle) -> c_int {
1561 NET_ERR_TRAVERSAL_UNSUPPORTED
1562}
1563
1564#[derive(Deserialize, Default)]
1569struct StreamOpenConfig {
1570 reliability: Option<String>,
1572 window_bytes: Option<u32>,
1575 fairness_weight: Option<u8>,
1576}
1577
1578pub struct MeshStreamHandle {
1593 stream: ManuallyDrop<CoreStream>,
1594 _node: ManuallyDrop<Arc<MeshNode>>,
1597 guard: HandleGuard,
1598}
1599
1600#[unsafe(no_mangle)]
1601pub unsafe extern "C" fn net_mesh_open_stream(
1602 handle: *mut MeshNodeHandle,
1603 peer_node_id: u64,
1604 stream_id: u64,
1605 config_json: *const c_char,
1606 out_stream: *mut *mut MeshStreamHandle,
1607) -> c_int {
1608 if handle.is_null() || out_stream.is_null() {
1609 return NetError::NullPointer.into();
1610 }
1611 let h = unsafe { &*handle };
1612 let _op = match h.guard.try_enter() {
1613 Some(op) => op,
1614 None => return NetError::ShuttingDown.into(),
1615 };
1616 let cfg_json: StreamOpenConfig = if config_json.is_null() {
1617 StreamOpenConfig::default()
1618 } else {
1619 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
1620 return NetError::InvalidUtf8.into();
1621 };
1622 match serde_json::from_str(&s) {
1623 Ok(v) => v,
1624 Err(_) => return NetError::InvalidJson.into(),
1625 }
1626 };
1627 let reliability = match cfg_json.reliability.as_deref() {
1628 None | Some("fire_and_forget") => Reliability::FireAndForget,
1629 Some("reliable") => Reliability::Reliable,
1630 Some(_) => return NET_ERR_MESH_TRANSPORT,
1631 };
1632 let window = cfg_json.window_bytes.unwrap_or(DEFAULT_STREAM_WINDOW_BYTES);
1633 let weight = cfg_json.fairness_weight.unwrap_or(1);
1634 let cfg = StreamConfig::new()
1635 .with_reliability(reliability)
1636 .with_window_bytes(window)
1637 .with_fairness_weight(weight);
1638 match h.inner.open_stream(peer_node_id, stream_id, cfg) {
1639 Ok(stream) => {
1640 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
1641 let sh = Box::new(MeshStreamHandle {
1642 stream: ManuallyDrop::new(stream),
1643 _node: ManuallyDrop::new(node_clone),
1644 guard: HandleGuard::new(),
1645 });
1646 unsafe {
1647 *out_stream = Box::into_raw(sh);
1648 }
1649 0
1650 }
1651 Err(e) => adapter_err_to_code(&e),
1652 }
1653}
1654
1655#[unsafe(no_mangle)]
1672pub unsafe extern "C" fn net_mesh_close_stream(handle: *mut MeshStreamHandle) -> c_int {
1673 if handle.is_null() {
1674 return NetError::NullPointer.into();
1675 }
1676 let h: &MeshStreamHandle = unsafe { &*handle };
1677 {
1678 let _op = match h.guard.try_enter() {
1695 Some(op) => op,
1696 None => return NetError::ShuttingDown.into(),
1697 };
1698 h._node
1699 .close_stream(h.stream.peer_node_id(), h.stream.stream_id());
1700 }
1701 unsafe { net_mesh_stream_free(handle) };
1702 0
1703}
1704
1705#[unsafe(no_mangle)]
1706pub unsafe extern "C" fn net_mesh_stream_free(handle: *mut MeshStreamHandle) {
1707 if handle.is_null() {
1708 return;
1709 }
1710 let h: &MeshStreamHandle = unsafe { &*handle };
1712 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
1713 unsafe {
1715 let _stream = ManuallyDrop::take(&mut (*handle).stream);
1719 let node = ManuallyDrop::take(&mut (*handle)._node);
1720 drop(node);
1721 }
1722 } else {
1723 tracing::warn!(
1724 "net_mesh_stream_free: in-flight ops did not drain within deadline; \
1725 leaking inner to avoid use-after-free"
1726 );
1727 }
1728}
1729
1730unsafe fn collect_payloads(
1740 payloads: *const *const u8,
1741 lens: *const usize,
1742 count: usize,
1743) -> Option<Vec<Bytes>> {
1744 let mut out = Vec::with_capacity(count);
1745 for i in 0..count {
1746 let ptr = *payloads.add(i);
1747 let len = *lens.add(i);
1748 if ptr.is_null() {
1749 if len == 0 {
1750 out.push(Bytes::new());
1751 continue;
1752 }
1753 return None;
1754 }
1755 if len > isize::MAX as usize {
1759 return None;
1760 }
1761 let slice = std::slice::from_raw_parts(ptr, len);
1762 out.push(Bytes::copy_from_slice(slice));
1763 }
1764 Some(out)
1765}
1766
1767#[inline]
1775fn handles_match(sh: &MeshStreamHandle, nh: &MeshNodeHandle) -> bool {
1776 Arc::ptr_eq(&sh._node, &nh.inner)
1777}
1778
1779#[unsafe(no_mangle)]
1780pub unsafe extern "C" fn net_mesh_send(
1781 handle: *mut MeshStreamHandle,
1782 payloads: *const *const u8,
1783 lens: *const usize,
1784 count: usize,
1785 node_handle: *mut MeshNodeHandle,
1786) -> c_int {
1787 if handle.is_null() || node_handle.is_null() {
1788 return NetError::NullPointer.into();
1789 }
1790 if count > 0 && (payloads.is_null() || lens.is_null()) {
1791 return NetError::NullPointer.into();
1792 }
1793 let sh = unsafe { &*handle };
1794 let nh = unsafe { &*node_handle };
1795 let _sh_op = match sh.guard.try_enter() {
1798 Some(op) => op,
1799 None => return NetError::ShuttingDown.into(),
1800 };
1801 let _nh_op = match nh.guard.try_enter() {
1802 Some(op) => op,
1803 None => return NetError::ShuttingDown.into(),
1804 };
1805 if !handles_match(sh, nh) {
1806 return NetError::MismatchedHandles.into();
1807 }
1808 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1809 Some(v) => v,
1810 None => return NetError::NullPointer.into(),
1811 };
1812 let node = nh.inner.clone();
1813 let stream = sh.stream.clone();
1814 match block_on(async move { node.send_on_stream(&stream, &payloads).await }) {
1815 Ok(()) => 0,
1816 Err(e) => stream_err_to_code(&e),
1817 }
1818}
1819
1820#[unsafe(no_mangle)]
1821pub unsafe extern "C" fn net_mesh_send_with_retry(
1822 handle: *mut MeshStreamHandle,
1823 payloads: *const *const u8,
1824 lens: *const usize,
1825 count: usize,
1826 max_retries: u32,
1827 node_handle: *mut MeshNodeHandle,
1828) -> c_int {
1829 if handle.is_null() || node_handle.is_null() {
1830 return NetError::NullPointer.into();
1831 }
1832 if count > 0 && (payloads.is_null() || lens.is_null()) {
1833 return NetError::NullPointer.into();
1834 }
1835 let sh = unsafe { &*handle };
1836 let nh = unsafe { &*node_handle };
1837 let _sh_op = match sh.guard.try_enter() {
1840 Some(op) => op,
1841 None => return NetError::ShuttingDown.into(),
1842 };
1843 let _nh_op = match nh.guard.try_enter() {
1844 Some(op) => op,
1845 None => return NetError::ShuttingDown.into(),
1846 };
1847 if !handles_match(sh, nh) {
1848 return NetError::MismatchedHandles.into();
1849 }
1850 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1851 Some(v) => v,
1852 None => return NetError::NullPointer.into(),
1853 };
1854 let node = nh.inner.clone();
1855 let stream = sh.stream.clone();
1856 match block_on(async move {
1857 node.send_with_retry(&stream, &payloads, max_retries as usize)
1858 .await
1859 }) {
1860 Ok(()) => 0,
1861 Err(e) => stream_err_to_code(&e),
1862 }
1863}
1864
1865#[unsafe(no_mangle)]
1866pub unsafe extern "C" fn net_mesh_send_blocking(
1867 handle: *mut MeshStreamHandle,
1868 payloads: *const *const u8,
1869 lens: *const usize,
1870 count: usize,
1871 node_handle: *mut MeshNodeHandle,
1872) -> c_int {
1873 if handle.is_null() || node_handle.is_null() {
1874 return NetError::NullPointer.into();
1875 }
1876 if count > 0 && (payloads.is_null() || lens.is_null()) {
1877 return NetError::NullPointer.into();
1878 }
1879 let sh = unsafe { &*handle };
1880 let nh = unsafe { &*node_handle };
1881 let _sh_op = match sh.guard.try_enter() {
1884 Some(op) => op,
1885 None => return NetError::ShuttingDown.into(),
1886 };
1887 let _nh_op = match nh.guard.try_enter() {
1888 Some(op) => op,
1889 None => return NetError::ShuttingDown.into(),
1890 };
1891 if !handles_match(sh, nh) {
1892 return NetError::MismatchedHandles.into();
1893 }
1894 let payloads = match unsafe { collect_payloads(payloads, lens, count) } {
1895 Some(v) => v,
1896 None => return NetError::NullPointer.into(),
1897 };
1898 let node = nh.inner.clone();
1899 let stream = sh.stream.clone();
1900 match block_on(async move { node.send_blocking(&stream, &payloads).await }) {
1901 Ok(()) => 0,
1902 Err(e) => stream_err_to_code(&e),
1903 }
1904}
1905
1906#[derive(Serialize)]
1907struct StreamStatsJson {
1908 tx_seq: u64,
1909 rx_seq: u64,
1910 inbound_pending: u64,
1911 last_activity_ns: u64,
1912 active: bool,
1913 backpressure_events: u64,
1914 tx_credit_remaining: u32,
1915 tx_window: u32,
1916 credit_grants_received: u64,
1917 credit_grants_sent: u64,
1918}
1919
1920#[unsafe(no_mangle)]
1921pub unsafe extern "C" fn net_mesh_stream_stats(
1922 node_handle: *mut MeshNodeHandle,
1923 peer_node_id: u64,
1924 stream_id: u64,
1925 out_json: *mut *mut c_char,
1926 out_len: *mut usize,
1927) -> c_int {
1928 if node_handle.is_null() || out_json.is_null() || out_len.is_null() {
1929 return NetError::NullPointer.into();
1930 }
1931 let h = unsafe { &*node_handle };
1932 let _op = match h.guard.try_enter() {
1933 Some(op) => op,
1934 None => return NetError::ShuttingDown.into(),
1935 };
1936 match h.inner.stream_stats(peer_node_id, stream_id) {
1937 Some(s) => {
1938 let js = StreamStatsJson {
1939 tx_seq: s.tx_seq,
1940 rx_seq: s.rx_seq,
1941 inbound_pending: s.inbound_pending,
1942 last_activity_ns: s.last_activity_ns,
1943 active: s.active,
1944 backpressure_events: s.backpressure_events,
1945 tx_credit_remaining: s.tx_credit_remaining,
1946 tx_window: s.tx_window,
1947 credit_grants_received: s.credit_grants_received,
1948 credit_grants_sent: s.credit_grants_sent,
1949 };
1950 write_json_out(&js, out_json, out_len)
1951 }
1952 None => {
1953 write_string_out("null".to_string(), out_json, out_len)
1956 }
1957 }
1958}
1959
1960#[derive(Serialize)]
1965struct RecvEventJson {
1966 id: String,
1967 payload_b64: String,
1969 insertion_ts: u64,
1970 shard_id: u16,
1971}
1972
1973#[unsafe(no_mangle)]
1974pub unsafe extern "C" fn net_mesh_recv_shard(
1975 handle: *mut MeshNodeHandle,
1976 shard_id: u16,
1977 limit: u32,
1978 out_json: *mut *mut c_char,
1979 out_len: *mut usize,
1980) -> c_int {
1981 if handle.is_null() || out_json.is_null() || out_len.is_null() {
1982 return NetError::NullPointer.into();
1983 }
1984 let h = unsafe { &*handle };
1985 let _op = match h.guard.try_enter() {
1986 Some(op) => op,
1987 None => return NetError::ShuttingDown.into(),
1988 };
1989 let node = h.inner.clone();
1990 let result = block_on(async move { node.poll_shard(shard_id, None, limit as usize).await });
1991 let result = match result {
1992 Ok(r) => r,
1993 Err(e) => return adapter_err_to_code(&e),
1994 };
1995 let events: Vec<RecvEventJson> = result
1996 .events
1997 .into_iter()
1998 .map(|e| RecvEventJson {
1999 id: e.id,
2000 payload_b64: encode_b64(&e.raw),
2001 insertion_ts: e.insertion_ts,
2002 shard_id: e.shard_id,
2003 })
2004 .collect();
2005 write_json_out(&events, out_json, out_len)
2006}
2007
2008fn encode_b64(bytes: &[u8]) -> String {
2009 const ALPH: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2012 let mut s = String::with_capacity(bytes.len().div_ceil(3) * 4);
2013 let mut i = 0;
2014 while i + 3 <= bytes.len() {
2015 let chunk = &bytes[i..i + 3];
2016 s.push(ALPH[(chunk[0] >> 2) as usize] as char);
2017 s.push(ALPH[(((chunk[0] & 0b11) << 4) | (chunk[1] >> 4)) as usize] as char);
2018 s.push(ALPH[(((chunk[1] & 0b1111) << 2) | (chunk[2] >> 6)) as usize] as char);
2019 s.push(ALPH[(chunk[2] & 0b111111) as usize] as char);
2020 i += 3;
2021 }
2022 let rem = bytes.len() - i;
2023 if rem == 1 {
2024 let b = bytes[i];
2025 s.push(ALPH[(b >> 2) as usize] as char);
2026 s.push(ALPH[((b & 0b11) << 4) as usize] as char);
2027 s.push('=');
2028 s.push('=');
2029 } else if rem == 2 {
2030 let b0 = bytes[i];
2031 let b1 = bytes[i + 1];
2032 s.push(ALPH[(b0 >> 2) as usize] as char);
2033 s.push(ALPH[(((b0 & 0b11) << 4) | (b1 >> 4)) as usize] as char);
2034 s.push(ALPH[((b1 & 0b1111) << 2) as usize] as char);
2035 s.push('=');
2036 }
2037 s
2038}
2039
2040#[derive(Deserialize)]
2045struct ChannelConfigInput {
2046 name: String,
2047 visibility: Option<String>,
2048 reliable: Option<bool>,
2049 require_token: Option<bool>,
2050 token_roots: Option<Vec<String>>,
2056 priority: Option<u8>,
2057 max_rate_pps: Option<u32>,
2058 publish_caps: Option<CapabilityFilterJson>,
2062 subscribe_caps: Option<CapabilityFilterJson>,
2066}
2067
2068fn parse_visibility(s: &str) -> Option<InnerVisibility> {
2069 match s {
2070 "subnet-local" => Some(InnerVisibility::SubnetLocal),
2071 "parent-visible" => Some(InnerVisibility::ParentVisible),
2072 "exported" => Some(InnerVisibility::Exported),
2073 "global" => Some(InnerVisibility::Global),
2074 _ => None,
2075 }
2076}
2077
2078#[unsafe(no_mangle)]
2079pub unsafe extern "C" fn net_mesh_register_channel(
2080 handle: *mut MeshNodeHandle,
2081 config_json: *const c_char,
2082) -> c_int {
2083 if handle.is_null() || config_json.is_null() {
2084 return NetError::NullPointer.into();
2085 }
2086 let h = unsafe { &*handle };
2087 let _op = match h.guard.try_enter() {
2088 Some(op) => op,
2089 None => return NetError::ShuttingDown.into(),
2090 };
2091 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
2092 return NetError::InvalidUtf8.into();
2093 };
2094 let input: ChannelConfigInput = match serde_json::from_str(&s) {
2095 Ok(v) => v,
2096 Err(_) => return NetError::InvalidJson.into(),
2097 };
2098 let name = match InnerChannelName::new(&input.name) {
2099 Ok(n) => n,
2100 Err(_) => return NET_ERR_CHANNEL,
2101 };
2102 let mut cfg = InnerChannelConfig::new(ChannelId::new(name));
2103 if let Some(v) = input.visibility {
2104 let Some(vis) = parse_visibility(&v) else {
2105 return NET_ERR_CHANNEL;
2106 };
2107 cfg = cfg.with_visibility(vis);
2108 }
2109 if let Some(r) = input.reliable {
2110 cfg = cfg.with_reliable(r);
2111 }
2112 if let Some(t) = input.require_token {
2113 cfg = cfg.with_require_token(t);
2114 }
2115 if let Some(roots) = input.token_roots {
2116 let mut parsed = Vec::with_capacity(roots.len());
2117 for hex_id in roots {
2118 let bytes = match hex::decode(&hex_id) {
2119 Ok(b) => b,
2120 Err(_) => return NET_ERR_CHANNEL,
2121 };
2122 let Ok(arr) = <[u8; 32]>::try_from(bytes.as_slice()) else {
2123 return NET_ERR_CHANNEL;
2124 };
2125 parsed.push(EntityId::from_bytes(arr));
2126 }
2127 cfg = cfg.with_token_roots(parsed);
2128 }
2129 if let Some(p) = input.priority {
2130 cfg = cfg.with_priority(p);
2131 }
2132 if let Some(pps) = input.max_rate_pps {
2133 cfg = cfg.with_rate_limit(pps);
2134 }
2135 if let Some(filter_json) = input.publish_caps {
2136 cfg = match capability_filter_from_json(filter_json) {
2137 Ok(f) => cfg.with_publish_caps(f),
2138 Err(_) => return NetError::InvalidJson.into(),
2139 };
2140 }
2141 if let Some(filter_json) = input.subscribe_caps {
2142 cfg = match capability_filter_from_json(filter_json) {
2143 Ok(f) => cfg.with_subscribe_caps(f),
2144 Err(_) => return NetError::InvalidJson.into(),
2145 };
2146 }
2147 h.channel_configs.insert(cfg);
2148 0
2149}
2150
2151#[unsafe(no_mangle)]
2152pub unsafe extern "C" fn net_mesh_subscribe_channel(
2153 handle: *mut MeshNodeHandle,
2154 publisher_node_id: u64,
2155 channel: *const c_char,
2156) -> c_int {
2157 subscribe_or_unsubscribe(handle, publisher_node_id, channel, true)
2158}
2159
2160#[unsafe(no_mangle)]
2161pub unsafe extern "C" fn net_mesh_unsubscribe_channel(
2162 handle: *mut MeshNodeHandle,
2163 publisher_node_id: u64,
2164 channel: *const c_char,
2165) -> c_int {
2166 subscribe_or_unsubscribe(handle, publisher_node_id, channel, false)
2167}
2168
2169#[unsafe(no_mangle)]
2176pub unsafe extern "C" fn net_mesh_subscribe_channel_with_token(
2177 handle: *mut MeshNodeHandle,
2178 publisher_node_id: u64,
2179 channel: *const c_char,
2180 token: *const u8,
2181 token_len: usize,
2182) -> c_int {
2183 if handle.is_null() || channel.is_null() || token.is_null() {
2184 return NetError::NullPointer.into();
2185 }
2186 let h = unsafe { &*handle };
2187 let _op = match h.guard.try_enter() {
2188 Some(op) => op,
2189 None => return NetError::ShuttingDown.into(),
2190 };
2191 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
2192 return NetError::InvalidUtf8.into();
2193 };
2194 let name = match InnerChannelName::new(&s) {
2195 Ok(n) => n,
2196 Err(_) => return NET_ERR_CHANNEL,
2197 };
2198 if token_len > isize::MAX as usize {
2200 return NetError::InvalidJson.into();
2201 }
2202 let slice = unsafe { std::slice::from_raw_parts(token, token_len) };
2203 let parsed = match PermissionToken::from_bytes(slice) {
2204 Ok(t) => t,
2205 Err(e) => return token_err_to_code(&e),
2206 };
2207 let node = h.inner.clone();
2208 match block_on(async move {
2209 node.subscribe_channel_with_token(publisher_node_id, name, parsed)
2210 .await
2211 }) {
2212 Ok(()) => 0,
2213 Err(e) => adapter_err_to_channel_code(&e),
2214 }
2215}
2216
2217fn subscribe_or_unsubscribe(
2218 handle: *mut MeshNodeHandle,
2219 publisher_node_id: u64,
2220 channel: *const c_char,
2221 subscribe: bool,
2222) -> c_int {
2223 if handle.is_null() || channel.is_null() {
2224 return NetError::NullPointer.into();
2225 }
2226 let h = unsafe { &*handle };
2227 let _op = match h.guard.try_enter() {
2228 Some(op) => op,
2229 None => return NetError::ShuttingDown.into(),
2230 };
2231 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
2232 return NetError::InvalidUtf8.into();
2233 };
2234 let name = match InnerChannelName::new(&s) {
2235 Ok(n) => n,
2236 Err(_) => return NET_ERR_CHANNEL,
2237 };
2238 let node = h.inner.clone();
2239 let outcome = if subscribe {
2240 block_on(async move { node.subscribe_channel(publisher_node_id, name).await })
2241 } else {
2242 block_on(async move { node.unsubscribe_channel(publisher_node_id, name).await })
2243 };
2244 match outcome {
2245 Ok(()) => 0,
2246 Err(e) => adapter_err_to_channel_code(&e),
2247 }
2248}
2249
2250fn adapter_err_to_channel_code(err: &AdapterError) -> c_int {
2251 if let AdapterError::Connection(msg) = err {
2252 let prefix = "membership request rejected: ";
2253 if let Some(tail) = msg.strip_prefix(prefix) {
2254 if tail.trim() == "Some(Unauthorized)" {
2255 return NET_ERR_CHANNEL_AUTH;
2256 }
2257 }
2258 }
2259 NET_ERR_CHANNEL
2260}
2261
2262#[derive(Deserialize, Default)]
2263struct PublishConfigInput {
2264 reliability: Option<String>,
2265 on_failure: Option<String>,
2266 max_inflight: Option<u32>,
2267}
2268
2269#[derive(Serialize)]
2270struct PublishReportJson {
2271 attempted: u32,
2272 delivered: u32,
2273 errors: Vec<PublishFailureJson>,
2274}
2275
2276#[derive(Serialize)]
2277struct PublishFailureJson {
2278 node_id: u64,
2279 message: String,
2280}
2281
2282fn to_publish_report_json(r: InnerPublishReport) -> PublishReportJson {
2283 PublishReportJson {
2284 attempted: r.attempted as u32,
2285 delivered: r.delivered as u32,
2286 errors: r
2287 .errors
2288 .into_iter()
2289 .map(|(id, e)| PublishFailureJson {
2290 node_id: id,
2291 message: format!("{}", e),
2292 })
2293 .collect(),
2294 }
2295}
2296
2297#[unsafe(no_mangle)]
2298pub unsafe extern "C" fn net_mesh_publish(
2299 handle: *mut MeshNodeHandle,
2300 channel: *const c_char,
2301 payload: *const u8,
2302 len: usize,
2303 config_json: *const c_char,
2304 out_json: *mut *mut c_char,
2305 out_len: *mut usize,
2306) -> c_int {
2307 if handle.is_null() || channel.is_null() || out_json.is_null() || out_len.is_null() {
2308 return NetError::NullPointer.into();
2309 }
2310 let h = unsafe { &*handle };
2311 let _op = match h.guard.try_enter() {
2312 Some(op) => op,
2313 None => return NetError::ShuttingDown.into(),
2314 };
2315 let Some(ch) = (unsafe { c_str_to_string(channel) }) else {
2316 return NetError::InvalidUtf8.into();
2317 };
2318 let name = match InnerChannelName::new(&ch) {
2319 Ok(n) => n,
2320 Err(_) => return NET_ERR_CHANNEL,
2321 };
2322 let cfg_in: PublishConfigInput = if config_json.is_null() {
2323 PublishConfigInput::default()
2324 } else {
2325 let Some(s) = (unsafe { c_str_to_string(config_json) }) else {
2326 return NetError::InvalidUtf8.into();
2327 };
2328 match serde_json::from_str(&s) {
2329 Ok(v) => v,
2330 Err(_) => return NetError::InvalidJson.into(),
2331 }
2332 };
2333 let reliability = match cfg_in.reliability.as_deref() {
2334 None | Some("fire_and_forget") => Reliability::FireAndForget,
2335 Some("reliable") => Reliability::Reliable,
2336 Some(_) => return NET_ERR_CHANNEL,
2337 };
2338 let on_failure = match cfg_in.on_failure.as_deref() {
2339 None | Some("best_effort") => InnerOnFailure::BestEffort,
2340 Some("fail_fast") => InnerOnFailure::FailFast,
2341 Some("collect") => InnerOnFailure::Collect,
2342 Some(_) => return NET_ERR_CHANNEL,
2343 };
2344 let max_inflight = cfg_in.max_inflight.unwrap_or(32) as usize;
2345 let publish_cfg = InnerPublishConfig {
2346 reliability,
2347 on_failure,
2348 max_inflight,
2349 };
2350 let publisher = ChannelPublisher::new(name, publish_cfg);
2351
2352 let bytes = if len == 0 {
2354 Bytes::new()
2355 } else if payload.is_null() {
2356 return NetError::NullPointer.into();
2357 } else if len > isize::MAX as usize {
2358 return NetError::InvalidJson.into();
2360 } else {
2361 Bytes::copy_from_slice(unsafe { std::slice::from_raw_parts(payload, len) })
2362 };
2363
2364 let node = h.inner.clone();
2365 match block_on(async move { node.publish(&publisher, bytes).await }) {
2366 Ok(report) => {
2367 let js = to_publish_report_json(report);
2368 write_json_out(&js, out_json, out_len)
2369 }
2370 Err(e) => adapter_err_to_channel_code(&e),
2371 }
2372}
2373
2374pub struct IdentityHandle {
2388 keypair: ManuallyDrop<Arc<EntityKeypair>>,
2389 cache: ManuallyDrop<Arc<TokenCache>>,
2390 generation: u32,
2396 guard: HandleGuard,
2397}
2398
2399fn alloc_bytes(src: &[u8], out_ptr: *mut *mut u8, out_len: *mut usize) -> c_int {
2413 if out_ptr.is_null() || out_len.is_null() {
2414 return NetError::NullPointer.into();
2415 }
2416 let len = src.len();
2417 if len == 0 {
2418 unsafe {
2419 *out_ptr = std::ptr::null_mut();
2420 *out_len = 0;
2421 }
2422 return 0;
2423 }
2424 let layout = match std::alloc::Layout::array::<u8>(len) {
2433 Ok(l) => l,
2434 Err(_) => return NET_ERR_IDENTITY,
2440 };
2441 let ptr = unsafe { std::alloc::alloc(layout) };
2442 if ptr.is_null() {
2443 std::alloc::handle_alloc_error(layout);
2444 }
2445 unsafe {
2446 std::ptr::copy_nonoverlapping(src.as_ptr(), ptr, len);
2447 *out_ptr = ptr;
2448 *out_len = len;
2449 }
2450 0
2451}
2452
2453#[unsafe(no_mangle)]
2468pub unsafe extern "C" fn net_free_bytes(ptr: *mut u8, len: usize) {
2469 if ptr.is_null() || len == 0 {
2470 return;
2471 }
2472 let layout = match std::alloc::Layout::array::<u8>(len) {
2478 Ok(l) => l,
2479 Err(_) => return,
2480 };
2481 unsafe {
2482 std::alloc::dealloc(ptr, layout);
2483 }
2484}
2485
2486fn entity_id_from_bytes(bytes: *const u8, len: usize) -> Option<EntityId> {
2487 if bytes.is_null() || len != 32 {
2488 return None;
2489 }
2490 let slice = unsafe { std::slice::from_raw_parts(bytes, 32) };
2491 let mut arr = [0u8; 32];
2492 arr.copy_from_slice(slice);
2493 Some(EntityId::from_bytes(arr))
2494}
2495
2496fn parse_scope_list(raw: &str) -> Option<TokenScope> {
2497 let values: Vec<String> = serde_json::from_str(raw).ok()?;
2501 let mut acc = TokenScope::NONE;
2502 for s in &values {
2503 acc = acc.union(match s.as_str() {
2504 "publish" => TokenScope::PUBLISH,
2505 "subscribe" => TokenScope::SUBSCRIBE,
2506 "admin" => TokenScope::ADMIN,
2507 "delegate" => TokenScope::DELEGATE,
2508 "wildcard" => TokenScope::WILDCARD,
2516 _ => return None,
2517 });
2518 }
2519 Some(acc)
2520}
2521
2522fn scope_to_strings(scope: TokenScope) -> Vec<&'static str> {
2523 let mut out = Vec::new();
2524 if scope.contains(TokenScope::PUBLISH) {
2525 out.push("publish");
2526 }
2527 if scope.contains(TokenScope::SUBSCRIBE) {
2528 out.push("subscribe");
2529 }
2530 if scope.contains(TokenScope::ADMIN) {
2531 out.push("admin");
2532 }
2533 if scope.contains(TokenScope::DELEGATE) {
2534 out.push("delegate");
2535 }
2536 if scope.contains(TokenScope::WILDCARD) {
2539 out.push("wildcard");
2540 }
2541 out
2542}
2543
2544fn channel_name_to_hash(channel: &str) -> Option<ChannelHash> {
2545 InnerChannelName::new(channel).ok().map(|n| n.hash())
2546}
2547
2548#[unsafe(no_mangle)]
2551pub unsafe extern "C" fn net_identity_generate(out_handle: *mut *mut IdentityHandle) -> c_int {
2552 if out_handle.is_null() {
2553 return NetError::NullPointer.into();
2554 }
2555 let handle = Box::new(IdentityHandle {
2556 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::generate())),
2557 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2558 generation: 0,
2560 guard: HandleGuard::new(),
2561 });
2562 unsafe {
2563 *out_handle = Box::into_raw(handle);
2564 }
2565 0
2566}
2567
2568#[unsafe(no_mangle)]
2572pub unsafe extern "C" fn net_identity_from_seed(
2573 seed: *const u8,
2574 seed_len: usize,
2575 out_handle: *mut *mut IdentityHandle,
2576) -> c_int {
2577 if seed.is_null() || out_handle.is_null() {
2578 return NetError::NullPointer.into();
2579 }
2580 if seed_len != 32 {
2581 return NET_ERR_IDENTITY;
2582 }
2583 let mut arr = [0u8; 32];
2584 arr.copy_from_slice(unsafe { std::slice::from_raw_parts(seed, 32) });
2585 let handle = Box::new(IdentityHandle {
2586 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::from_bytes(arr))),
2587 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2588 generation: 0,
2593 guard: HandleGuard::new(),
2594 });
2595 unsafe {
2596 *out_handle = Box::into_raw(handle);
2597 }
2598 0
2599}
2600
2601#[unsafe(no_mangle)]
2602pub unsafe extern "C" fn net_identity_free(handle: *mut IdentityHandle) {
2603 if handle.is_null() {
2604 return;
2605 }
2606 let h: &IdentityHandle = unsafe { &*handle };
2608 if h.guard.begin_free(FFI_HANDLE_FREE_DEADLINE) {
2609 unsafe {
2611 let mh = &mut *handle;
2612 let kp = ManuallyDrop::take(&mut mh.keypair);
2613 let cache = ManuallyDrop::take(&mut mh.cache);
2614 drop(kp);
2615 drop(cache);
2616 }
2617 } else {
2618 tracing::warn!(
2619 "net_identity_free: in-flight ops did not drain within deadline; \
2620 leaking inner to avoid use-after-free"
2621 );
2622 }
2623}
2624
2625#[unsafe(no_mangle)]
2632pub extern "C" fn net_identity_state_size() -> usize {
2633 IDENTITY_STATE_SIZE
2634}
2635
2636#[unsafe(no_mangle)]
2644pub unsafe extern "C" fn net_identity_generation(handle: *mut IdentityHandle) -> u32 {
2645 if handle.is_null() {
2646 return 0;
2647 }
2648 let h = unsafe { &*handle };
2649 let Some(_op) = h.guard.try_enter() else {
2650 return 0;
2651 };
2652 h.generation
2653}
2654
2655#[unsafe(no_mangle)]
2679pub unsafe extern "C" fn net_identity_at_generation(
2680 handle: *mut IdentityHandle,
2681 next: u32,
2682 out_handle: *mut *mut IdentityHandle,
2683) -> c_int {
2684 if handle.is_null() || out_handle.is_null() {
2685 return NetError::NullPointer.into();
2686 }
2687 let h = unsafe { &*handle };
2688 let _op = match h.guard.try_enter() {
2689 Some(op) => op,
2690 None => return NetError::ShuttingDown.into(),
2691 };
2692 let Ok(generation) = InnerIdentityState::check_rotation(h.generation, next) else {
2693 return NET_ERR_IDENTITY;
2694 };
2695 let rotated = Box::new(IdentityHandle {
2696 keypair: ManuallyDrop::new(Arc::clone(&h.keypair)),
2697 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2698 generation,
2699 guard: HandleGuard::new(),
2700 });
2701 unsafe {
2702 *out_handle = Box::into_raw(rotated);
2703 }
2704 0
2705}
2706
2707#[unsafe(no_mangle)]
2714pub unsafe extern "C" fn net_identity_to_state(handle: *mut IdentityHandle, out: *mut u8) -> c_int {
2715 if handle.is_null() || out.is_null() {
2716 return NetError::NullPointer.into();
2717 }
2718 let h = unsafe { &*handle };
2719 let _op = match h.guard.try_enter() {
2720 Some(op) => op,
2721 None => return NetError::ShuttingDown.into(),
2722 };
2723 let bytes = InnerIdentityState {
2724 seed: *h.keypair.secret_bytes(),
2725 generation: h.generation,
2726 }
2727 .to_bytes();
2728 unsafe {
2729 std::ptr::copy_nonoverlapping(bytes.as_ptr(), out, bytes.len());
2730 }
2731 0
2732}
2733
2734#[unsafe(no_mangle)]
2744pub unsafe extern "C" fn net_identity_from_state(
2745 state: *const u8,
2746 state_len: usize,
2747 out_handle: *mut *mut IdentityHandle,
2748) -> c_int {
2749 if state.is_null() || out_handle.is_null() {
2750 return NetError::NullPointer.into();
2751 }
2752 let bytes = unsafe { std::slice::from_raw_parts(state, state_len) };
2753 let Ok(parsed) = InnerIdentityState::from_bytes(bytes) else {
2754 return NET_ERR_IDENTITY;
2755 };
2756 let handle = Box::new(IdentityHandle {
2757 keypair: ManuallyDrop::new(Arc::new(EntityKeypair::from_bytes(parsed.seed))),
2758 cache: ManuallyDrop::new(Arc::new(TokenCache::new())),
2759 generation: parsed.generation,
2760 guard: HandleGuard::new(),
2761 });
2762 unsafe {
2763 *out_handle = Box::into_raw(handle);
2764 }
2765 0
2766}
2767
2768#[unsafe(no_mangle)]
2771pub unsafe extern "C" fn net_identity_to_seed(handle: *mut IdentityHandle, out: *mut u8) -> c_int {
2772 if handle.is_null() || out.is_null() {
2773 return NetError::NullPointer.into();
2774 }
2775 let h = unsafe { &*handle };
2776 let _op = match h.guard.try_enter() {
2777 Some(op) => op,
2778 None => return NetError::ShuttingDown.into(),
2779 };
2780 let seed = h.keypair.secret_bytes();
2781 unsafe {
2782 std::ptr::copy_nonoverlapping(seed.as_ptr(), out, 32);
2783 }
2784 0
2785}
2786
2787#[unsafe(no_mangle)]
2789pub unsafe extern "C" fn net_identity_entity_id(
2790 handle: *mut IdentityHandle,
2791 out: *mut u8,
2792) -> c_int {
2793 if handle.is_null() || out.is_null() {
2794 return NetError::NullPointer.into();
2795 }
2796 let h = unsafe { &*handle };
2797 let _op = match h.guard.try_enter() {
2798 Some(op) => op,
2799 None => return NetError::ShuttingDown.into(),
2800 };
2801 let id = h.keypair.entity_id().as_bytes();
2802 unsafe {
2803 std::ptr::copy_nonoverlapping(id.as_ptr(), out, 32);
2804 }
2805 0
2806}
2807
2808#[unsafe(no_mangle)]
2809pub unsafe extern "C" fn net_identity_node_id(handle: *mut IdentityHandle) -> u64 {
2810 if handle.is_null() {
2811 return 0;
2812 }
2813 let h = unsafe { &*handle };
2814 let _op = match h.guard.try_enter() {
2816 Some(op) => op,
2817 None => return 0,
2818 };
2819 h.keypair.node_id()
2820}
2821
2822#[unsafe(no_mangle)]
2823pub unsafe extern "C" fn net_identity_origin_hash(handle: *mut IdentityHandle) -> u64 {
2824 if handle.is_null() {
2825 return 0;
2826 }
2827 let h = unsafe { &*handle };
2828 let _op = match h.guard.try_enter() {
2830 Some(op) => op,
2831 None => return 0,
2832 };
2833 h.keypair.origin_hash()
2834}
2835
2836#[unsafe(no_mangle)]
2839pub unsafe extern "C" fn net_identity_sign(
2840 handle: *mut IdentityHandle,
2841 msg: *const u8,
2842 len: usize,
2843 out_sig: *mut u8,
2844) -> c_int {
2845 if handle.is_null() || out_sig.is_null() {
2846 return NetError::NullPointer.into();
2847 }
2848 if len > 0 && msg.is_null() {
2849 return NetError::NullPointer.into();
2850 }
2851 let h = unsafe { &*handle };
2852 let _op = match h.guard.try_enter() {
2853 Some(op) => op,
2854 None => return NetError::ShuttingDown.into(),
2855 };
2856 let slice = if len == 0 {
2857 &[][..]
2858 } else if len > isize::MAX as usize {
2859 return NetError::InvalidJson.into();
2861 } else {
2862 unsafe { std::slice::from_raw_parts(msg, len) }
2863 };
2864 let sig = h.keypair.sign(slice).to_bytes();
2865 unsafe {
2866 std::ptr::copy_nonoverlapping(sig.as_ptr(), out_sig, 64);
2867 }
2868 0
2869}
2870
2871#[unsafe(no_mangle)]
2889pub unsafe extern "C" fn net_verify_signature(
2890 entity_id: *const u8,
2891 entity_id_len: usize,
2892 msg: *const u8,
2893 msg_len: usize,
2894 signature: *const u8,
2895 signature_len: usize,
2896 out_valid: *mut c_int,
2897) -> c_int {
2898 if out_valid.is_null() {
2899 return NetError::NullPointer.into();
2900 }
2901 if (msg_len > 0 && msg.is_null()) || signature.is_null() {
2902 return NetError::NullPointer.into();
2903 }
2904 let Some(id) = entity_id_from_bytes(entity_id, entity_id_len) else {
2905 return NET_ERR_IDENTITY;
2906 };
2907 if signature_len != 64 {
2908 return NET_ERR_IDENTITY;
2909 }
2910 if msg_len > isize::MAX as usize {
2912 return NetError::InvalidJson.into();
2913 }
2914 let msg_slice = if msg_len == 0 {
2915 &[][..]
2916 } else {
2917 unsafe { std::slice::from_raw_parts(msg, msg_len) }
2918 };
2919 let sig_slice = unsafe { std::slice::from_raw_parts(signature, 64) };
2920 let Ok(sig) = <[u8; 64]>::try_from(sig_slice) else {
2921 return NET_ERR_IDENTITY;
2922 };
2923 let valid = id.verify_bytes(msg_slice, &sig).is_ok();
2924 unsafe {
2925 *out_valid = c_int::from(valid);
2926 }
2927 0
2928}
2929
2930#[unsafe(no_mangle)]
2933pub unsafe extern "C" fn net_identity_issue_token(
2934 signer: *mut IdentityHandle,
2935 subject: *const u8,
2936 subject_len: usize,
2937 scope_json: *const c_char,
2938 channel: *const c_char,
2939 ttl_seconds: u32,
2940 delegation_depth: u8,
2941 out_token: *mut *mut u8,
2942 out_token_len: *mut usize,
2943) -> c_int {
2944 if signer.is_null() || out_token.is_null() || out_token_len.is_null() {
2945 return NetError::NullPointer.into();
2946 }
2947 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
2948 return NET_ERR_IDENTITY;
2949 };
2950 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
2951 return NetError::InvalidUtf8.into();
2952 };
2953 let Some(scope) = parse_scope_list(&scope_s) else {
2954 return NET_ERR_IDENTITY;
2955 };
2956 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
2957 return NetError::InvalidUtf8.into();
2958 };
2959 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
2960 return NET_ERR_IDENTITY;
2961 };
2962 let h = unsafe { &*signer };
2963 let _op = match h.guard.try_enter() {
2967 Some(op) => op,
2968 None => return NetError::ShuttingDown.into(),
2969 };
2970 let token = match PermissionToken::try_issue_with_generation(
2976 &h.keypair,
2977 h.generation,
2978 subject_id,
2979 scope,
2980 channel_hash,
2981 u64::from(ttl_seconds),
2982 delegation_depth,
2983 ) {
2984 Ok(t) => t,
2985 Err(e) => return token_err_to_code(&e),
2986 };
2987 alloc_bytes(&token.to_bytes(), out_token, out_token_len)
2988}
2989
2990#[unsafe(no_mangle)]
2994pub unsafe extern "C" fn net_identity_install_token(
2995 handle: *mut IdentityHandle,
2996 token: *const u8,
2997 len: usize,
2998) -> c_int {
2999 if handle.is_null() || token.is_null() {
3000 return NetError::NullPointer.into();
3001 }
3002 if len > isize::MAX as usize {
3004 return NetError::InvalidJson.into();
3005 }
3006 let slice = unsafe { std::slice::from_raw_parts(token, len) };
3007 let parsed = match PermissionToken::from_bytes(slice) {
3008 Ok(t) => t,
3009 Err(e) => return token_err_to_code(&e),
3010 };
3011 let h = unsafe { &*handle };
3012 let _op = match h.guard.try_enter() {
3013 Some(op) => op,
3014 None => return NetError::ShuttingDown.into(),
3015 };
3016 match h.cache.insert(parsed) {
3017 Ok(()) => 0,
3018 Err(e) => token_err_to_code(&e),
3019 }
3020}
3021
3022#[unsafe(no_mangle)]
3026pub unsafe extern "C" fn net_identity_lookup_token(
3027 handle: *mut IdentityHandle,
3028 subject: *const u8,
3029 subject_len: usize,
3030 channel: *const c_char,
3031 out_token: *mut *mut u8,
3032 out_token_len: *mut usize,
3033) -> c_int {
3034 if handle.is_null() || out_token.is_null() || out_token_len.is_null() {
3035 return NetError::NullPointer.into();
3036 }
3037 let Some(subject_id) = entity_id_from_bytes(subject, subject_len) else {
3038 return NET_ERR_IDENTITY;
3039 };
3040 let Some(channel_s) = (unsafe { c_str_to_string(channel) }) else {
3041 return NetError::InvalidUtf8.into();
3042 };
3043 let Some(channel_hash) = channel_name_to_hash(&channel_s) else {
3044 return NET_ERR_IDENTITY;
3045 };
3046 let h = unsafe { &*handle };
3047 let _op = match h.guard.try_enter() {
3048 Some(op) => op,
3049 None => return NetError::ShuttingDown.into(),
3050 };
3051 match h.cache.get(&subject_id, channel_hash) {
3052 Some(token) => alloc_bytes(&token.to_bytes(), out_token, out_token_len),
3053 None => {
3054 unsafe {
3055 *out_token = std::ptr::null_mut();
3056 *out_token_len = 0;
3057 }
3058 0
3059 }
3060 }
3061}
3062
3063#[unsafe(no_mangle)]
3064pub unsafe extern "C" fn net_identity_token_cache_len(handle: *mut IdentityHandle) -> u32 {
3065 if handle.is_null() {
3066 return 0;
3067 }
3068 let h = unsafe { &*handle };
3069 let _op = match h.guard.try_enter() {
3071 Some(op) => op,
3072 None => return 0,
3073 };
3074 h.cache.len() as u32
3075}
3076
3077#[derive(Serialize)]
3082struct ParsedTokenJson {
3083 issuer_hex: String,
3084 subject_hex: String,
3085 scope: Vec<&'static str>,
3086 channel_hash: ChannelHash,
3087 not_before: u64,
3088 not_after: u64,
3089 delegation_depth: u8,
3090 issuer_generation: u32,
3096 nonce: u64,
3097 signature_hex: String,
3098}
3099
3100#[unsafe(no_mangle)]
3105pub unsafe extern "C" fn net_parse_token(
3106 token: *const u8,
3107 len: usize,
3108 out_json: *mut *mut c_char,
3109 out_len: *mut usize,
3110) -> c_int {
3111 if token.is_null() || out_json.is_null() || out_len.is_null() {
3112 return NetError::NullPointer.into();
3113 }
3114 if len > isize::MAX as usize {
3116 return NetError::InvalidJson.into();
3117 }
3118 let slice = unsafe { std::slice::from_raw_parts(token, len) };
3119 let parsed = match PermissionToken::from_bytes(slice) {
3120 Ok(t) => t,
3121 Err(e) => return token_err_to_code(&e),
3122 };
3123 let out = ParsedTokenJson {
3124 issuer_hex: hex::encode(parsed.issuer.as_bytes()),
3125 subject_hex: hex::encode(parsed.subject.as_bytes()),
3126 scope: scope_to_strings(parsed.scope),
3127 channel_hash: parsed.channel_hash,
3128 not_before: parsed.not_before,
3129 not_after: parsed.not_after,
3130 delegation_depth: parsed.delegation_depth,
3131 issuer_generation: parsed.issuer_generation,
3132 nonce: parsed.nonce,
3133 signature_hex: hex::encode(parsed.signature),
3134 };
3135 write_json_out(&out, out_json, out_len)
3136}
3137
3138#[unsafe(no_mangle)]
3142pub unsafe extern "C" fn net_verify_token(
3143 token: *const u8,
3144 len: usize,
3145 out_ok: *mut c_int,
3146) -> c_int {
3147 if token.is_null() || out_ok.is_null() {
3148 return NetError::NullPointer.into();
3149 }
3150 if len > isize::MAX as usize {
3152 return NetError::InvalidJson.into();
3153 }
3154 let slice = unsafe { std::slice::from_raw_parts(token, len) };
3155 let parsed = match PermissionToken::from_bytes(slice) {
3156 Ok(t) => t,
3157 Err(e) => return token_err_to_code(&e),
3158 };
3159 unsafe {
3160 *out_ok = if parsed.verify().is_ok() { 1 } else { 0 };
3161 }
3162 0
3163}
3164
3165#[unsafe(no_mangle)]
3170pub unsafe extern "C" fn net_token_is_expired(
3171 token: *const u8,
3172 len: usize,
3173 out_expired: *mut c_int,
3174) -> c_int {
3175 if token.is_null() || out_expired.is_null() {
3176 return NetError::NullPointer.into();
3177 }
3178 if len > isize::MAX as usize {
3180 return NetError::InvalidJson.into();
3181 }
3182 let slice = unsafe { std::slice::from_raw_parts(token, len) };
3183 let parsed = match PermissionToken::from_bytes(slice) {
3184 Ok(t) => t,
3185 Err(e) => return token_err_to_code(&e),
3186 };
3187 unsafe {
3188 *out_expired = if parsed.is_expired() { 1 } else { 0 };
3189 }
3190 0
3191}
3192
3193#[unsafe(no_mangle)]
3196pub unsafe extern "C" fn net_delegate_token(
3197 signer: *mut IdentityHandle,
3198 parent: *const u8,
3199 parent_len: usize,
3200 new_subject: *const u8,
3201 new_subject_len: usize,
3202 restricted_scope_json: *const c_char,
3203 out_token: *mut *mut u8,
3204 out_token_len: *mut usize,
3205) -> c_int {
3206 if signer.is_null()
3207 || parent.is_null()
3208 || new_subject.is_null()
3209 || restricted_scope_json.is_null()
3210 || out_token.is_null()
3211 || out_token_len.is_null()
3212 {
3213 return NetError::NullPointer.into();
3214 }
3215 if parent_len > isize::MAX as usize {
3217 return NetError::InvalidJson.into();
3218 }
3219 let parent_slice = unsafe { std::slice::from_raw_parts(parent, parent_len) };
3220 let parent_tok = match PermissionToken::from_bytes(parent_slice) {
3221 Ok(t) => t,
3222 Err(e) => return token_err_to_code(&e),
3223 };
3224 let Some(subject_id) = entity_id_from_bytes(new_subject, new_subject_len) else {
3225 return NET_ERR_IDENTITY;
3226 };
3227 let Some(scope_s) = (unsafe { c_str_to_string(restricted_scope_json) }) else {
3228 return NetError::InvalidUtf8.into();
3229 };
3230 let Some(scope) = parse_scope_list(&scope_s) else {
3231 return NET_ERR_IDENTITY;
3232 };
3233 let h = unsafe { &*signer };
3234 let _op = match h.guard.try_enter() {
3238 Some(op) => op,
3239 None => return NetError::ShuttingDown.into(),
3240 };
3241 match parent_tok.delegate(&h.keypair, subject_id, scope) {
3242 Ok(child) => alloc_bytes(&child.to_bytes(), out_token, out_token_len),
3243 Err(e) => token_err_to_code(&e),
3244 }
3245}
3246
3247#[unsafe(no_mangle)]
3252pub unsafe extern "C" fn net_channel_hash(channel: *const c_char, out_hash: *mut u64) -> c_int {
3253 if channel.is_null() || out_hash.is_null() {
3254 return NetError::NullPointer.into();
3255 }
3256 let Some(s) = (unsafe { c_str_to_string(channel) }) else {
3257 return NetError::InvalidUtf8.into();
3258 };
3259 let Some(hash) = channel_name_to_hash(&s) else {
3260 return NET_ERR_IDENTITY;
3261 };
3262 unsafe {
3263 *out_hash = hash;
3264 }
3265 0
3266}
3267
3268use crate::adapter::net::behavior::capability::{
3275 AcceleratorInfo, AcceleratorType, CapabilityFilter, CapabilitySet, GpuInfo, GpuVendor,
3276 HardwareCapabilities, Modality, ModelCapability, ResourceLimits, SoftwareCapabilities,
3277 ToolCapability, TAG_SCOPE_REGION_PREFIX, TAG_SCOPE_SUBNET_LOCAL, TAG_SCOPE_TENANT_PREFIX,
3278};
3279
3280fn parse_gpu_vendor_cap(s: &str) -> GpuVendor {
3283 match s.to_ascii_lowercase().as_str() {
3284 "nvidia" => GpuVendor::Nvidia,
3285 "amd" => GpuVendor::Amd,
3286 "intel" => GpuVendor::Intel,
3287 "apple" => GpuVendor::Apple,
3288 "qualcomm" => GpuVendor::Qualcomm,
3289 _ => GpuVendor::Unknown,
3290 }
3291}
3292
3293fn gpu_vendor_to_string_cap(v: GpuVendor) -> &'static str {
3294 match v {
3295 GpuVendor::Nvidia => "nvidia",
3296 GpuVendor::Amd => "amd",
3297 GpuVendor::Intel => "intel",
3298 GpuVendor::Apple => "apple",
3299 GpuVendor::Qualcomm => "qualcomm",
3300 GpuVendor::Unknown => "unknown",
3301 }
3302}
3303
3304fn parse_modality_cap(s: &str) -> Option<Modality> {
3305 match s.to_ascii_lowercase().as_str() {
3306 "text" => Some(Modality::Text),
3307 "image" => Some(Modality::Image),
3308 "audio" => Some(Modality::Audio),
3309 "video" => Some(Modality::Video),
3310 "code" => Some(Modality::Code),
3311 "embedding" => Some(Modality::Embedding),
3312 "tool-use" | "tool_use" | "tooluse" => Some(Modality::ToolUse),
3313 _ => None,
3322 }
3323}
3324
3325fn parse_accelerator_type_cap(s: &str) -> AcceleratorType {
3326 match s.to_ascii_lowercase().as_str() {
3327 "tpu" => AcceleratorType::Tpu,
3328 "npu" => AcceleratorType::Npu,
3329 "fpga" => AcceleratorType::Fpga,
3330 "asic" => AcceleratorType::Asic,
3331 "dsp" => AcceleratorType::Dsp,
3332 _ => AcceleratorType::Unknown,
3333 }
3334}
3335
3336#[derive(Deserialize, Default)]
3339struct CapabilitySetJson {
3340 #[serde(default)]
3341 hardware: Option<HardwareJson>,
3342 #[serde(default)]
3343 software: Option<SoftwareJson>,
3344 #[serde(default)]
3345 models: Vec<ModelJson>,
3346 #[serde(default)]
3347 tools: Vec<ToolJson>,
3348 #[serde(default)]
3349 tags: Vec<String>,
3350 #[serde(default)]
3351 limits: Option<LimitsJson>,
3352}
3353
3354#[derive(Deserialize, Default)]
3355struct HardwareJson {
3356 cpu_cores: Option<u32>,
3357 cpu_threads: Option<u32>,
3358 memory_gb: Option<u32>,
3359 gpu: Option<GpuJson>,
3360 #[serde(default)]
3361 additional_gpus: Vec<GpuJson>,
3362 storage_gb: Option<u64>,
3363 network_gbps: Option<u32>,
3364 #[serde(default)]
3365 accelerators: Vec<AcceleratorJson>,
3366}
3367
3368#[derive(Deserialize)]
3369struct GpuJson {
3370 vendor: Option<String>,
3371 #[serde(default)]
3372 model: String,
3373 #[serde(default)]
3374 vram_gb: u32,
3375 compute_units: Option<u32>,
3376 tensor_cores: Option<u32>,
3377 fp16_tflops_x10: Option<u32>,
3378}
3379
3380#[derive(Deserialize)]
3381struct AcceleratorJson {
3382 #[serde(default)]
3383 kind: String,
3384 #[serde(default)]
3385 model: String,
3386 memory_gb: Option<u32>,
3387 tops_x10: Option<u32>,
3388}
3389
3390#[derive(Deserialize, Default)]
3391struct SoftwareJson {
3392 os: Option<String>,
3393 os_version: Option<String>,
3394 #[serde(default)]
3395 runtimes: Vec<Vec<String>>,
3396 #[serde(default)]
3397 frameworks: Vec<Vec<String>>,
3398 cuda_version: Option<String>,
3399 #[serde(default)]
3400 drivers: Vec<Vec<String>>,
3401}
3402
3403#[derive(Deserialize)]
3404struct ModelJson {
3405 #[serde(default)]
3406 model_id: String,
3407 #[serde(default)]
3408 family: String,
3409 parameters_b_x10: Option<u32>,
3410 context_length: Option<u32>,
3411 quantization: Option<String>,
3412 #[serde(default)]
3413 modalities: Vec<String>,
3414 tokens_per_sec: Option<u32>,
3415 loaded: Option<bool>,
3416}
3417
3418#[derive(Deserialize)]
3419struct ToolJson {
3420 #[serde(default)]
3421 tool_id: String,
3422 #[serde(default)]
3423 name: String,
3424 version: Option<String>,
3425 input_schema: Option<String>,
3426 output_schema: Option<String>,
3427 #[serde(default)]
3428 requires: Vec<String>,
3429 estimated_time_ms: Option<u32>,
3430 stateless: Option<bool>,
3431}
3432
3433#[derive(Deserialize, Default)]
3434struct LimitsJson {
3435 max_concurrent_requests: Option<u32>,
3436 max_tokens_per_request: Option<u32>,
3437 rate_limit_rpm: Option<u32>,
3438 max_batch_size: Option<u32>,
3439 max_input_bytes: Option<u32>,
3440 max_output_bytes: Option<u32>,
3441}
3442
3443#[derive(Deserialize, Default)]
3444struct CapabilityFilterJson {
3445 #[serde(default)]
3446 require_tags: Vec<String>,
3447 #[serde(default)]
3448 require_models: Vec<String>,
3449 #[serde(default)]
3450 require_tools: Vec<String>,
3451 min_memory_gb: Option<u32>,
3452 require_gpu: Option<bool>,
3453 gpu_vendor: Option<String>,
3454 min_vram_gb: Option<u32>,
3455 min_context_length: Option<u32>,
3456 #[serde(default)]
3457 require_modalities: Vec<String>,
3458}
3459
3460fn pair_vec(xs: Vec<Vec<String>>) -> Vec<(String, String)> {
3463 xs.into_iter()
3464 .filter_map(|mut p| {
3465 if p.len() >= 2 {
3466 Some((std::mem::take(&mut p[0]), std::mem::take(&mut p[1])))
3467 } else {
3468 None
3469 }
3470 })
3471 .collect()
3472}
3473
3474#[inline]
3480fn saturating_u16_cap(v: u32) -> u16 {
3481 v.min(u16::MAX as u32) as u16
3482}
3483
3484fn gpu_info_from_json(g: GpuJson) -> GpuInfo {
3485 let vendor = g
3486 .vendor
3487 .as_deref()
3488 .map(parse_gpu_vendor_cap)
3489 .unwrap_or(GpuVendor::Unknown);
3490 let mut info = GpuInfo::new(vendor, g.model, g.vram_gb);
3491 if let Some(cu) = g.compute_units {
3492 info = info.with_compute_units(saturating_u16_cap(cu));
3493 }
3494 if let Some(tc) = g.tensor_cores {
3495 info = info.with_tensor_cores(saturating_u16_cap(tc));
3496 }
3497 if let Some(tf) = g.fp16_tflops_x10 {
3498 info.fp16_tflops_x10 = tf;
3520 }
3521 info
3522}
3523
3524fn accelerator_from_json(a: AcceleratorJson) -> AcceleratorInfo {
3525 AcceleratorInfo {
3526 accel_type: parse_accelerator_type_cap(&a.kind),
3527 model: a.model,
3528 memory_gb: a.memory_gb.unwrap_or(0),
3529 tops_x10: a.tops_x10.map(saturating_u16_cap).unwrap_or(0),
3530 }
3531}
3532
3533fn hardware_from_json(h: HardwareJson) -> HardwareCapabilities {
3534 let mut hw = HardwareCapabilities::new();
3535 match (h.cpu_cores, h.cpu_threads) {
3536 (Some(c), Some(t)) => hw = hw.with_cpu(saturating_u16_cap(c), saturating_u16_cap(t)),
3537 (Some(c), None) => {
3538 let c16 = saturating_u16_cap(c);
3539 hw = hw.with_cpu(c16, c16);
3540 }
3541 _ => {}
3542 }
3543 if let Some(mb) = h.memory_gb {
3544 hw = hw.with_memory(mb);
3545 }
3546 if let Some(g) = h.gpu {
3547 hw = hw.with_gpu(gpu_info_from_json(g));
3548 }
3549 for g in h.additional_gpus {
3550 hw = hw.add_gpu(gpu_info_from_json(g));
3551 }
3552 if let Some(mb) = h.storage_gb {
3553 hw = hw.with_storage(mb);
3554 }
3555 if let Some(gbps) = h.network_gbps {
3556 hw = hw.with_network(gbps);
3557 }
3558 for a in h.accelerators {
3559 hw = hw.add_accelerator(accelerator_from_json(a));
3560 }
3561 hw
3562}
3563
3564fn software_from_json(s: SoftwareJson) -> SoftwareCapabilities {
3565 let mut sw = SoftwareCapabilities::new()
3566 .with_os(s.os.unwrap_or_default(), s.os_version.unwrap_or_default());
3567 for (k, v) in pair_vec(s.runtimes) {
3568 sw = sw.add_runtime(k, v);
3569 }
3570 for (k, v) in pair_vec(s.frameworks) {
3571 sw = sw.add_framework(k, v);
3572 }
3573 if let Some(c) = s.cuda_version {
3574 sw = sw.with_cuda(c);
3575 }
3576 sw.drivers = pair_vec(s.drivers);
3577 sw
3578}
3579
3580fn model_from_json(m: ModelJson) -> Result<ModelCapability, String> {
3581 let mut mc = ModelCapability::new(m.model_id, m.family);
3582 if let Some(p) = m.parameters_b_x10 {
3583 mc.parameters_b_x10 = p;
3584 }
3585 if let Some(c) = m.context_length {
3586 mc = mc.with_context_length(c);
3587 }
3588 if let Some(q) = m.quantization {
3589 mc = mc.with_quantization(q);
3590 }
3591 for modality in m.modalities {
3592 match parse_modality_cap(&modality) {
3600 Some(parsed) => mc = mc.add_modality(parsed),
3601 None => return Err(modality),
3602 }
3603 }
3604 if let Some(t) = m.tokens_per_sec {
3605 mc = mc.with_tokens_per_sec(t);
3606 }
3607 if let Some(l) = m.loaded {
3608 mc = mc.with_loaded(l);
3609 }
3610 Ok(mc)
3611}
3612
3613fn tool_from_json(t: ToolJson) -> ToolCapability {
3614 let mut tc = ToolCapability::new(t.tool_id, t.name);
3615 if let Some(v) = t.version {
3616 tc = tc.with_version(v);
3617 }
3618 if let Some(s) = t.input_schema {
3619 tc = tc.with_input_schema(s);
3620 }
3621 if let Some(s) = t.output_schema {
3622 tc = tc.with_output_schema(s);
3623 }
3624 for r in t.requires {
3625 tc = tc.requires(r);
3626 }
3627 if let Some(ms) = t.estimated_time_ms {
3628 tc = tc.with_estimated_time(ms);
3629 }
3630 if let Some(st) = t.stateless {
3631 tc = tc.with_stateless(st);
3632 }
3633 tc
3634}
3635
3636fn limits_from_json(l: LimitsJson) -> ResourceLimits {
3637 let mut rl = ResourceLimits::new();
3638 if let Some(n) = l.max_concurrent_requests {
3639 rl = rl.with_max_concurrent(n);
3640 }
3641 if let Some(n) = l.max_tokens_per_request {
3642 rl = rl.with_max_tokens(n);
3643 }
3644 if let Some(n) = l.rate_limit_rpm {
3645 rl = rl.with_rate_limit(n);
3646 }
3647 if let Some(n) = l.max_batch_size {
3648 rl = rl.with_max_batch(n);
3649 }
3650 if let Some(n) = l.max_input_bytes {
3651 rl.max_input_bytes = n;
3652 }
3653 if let Some(n) = l.max_output_bytes {
3654 rl.max_output_bytes = n;
3655 }
3656 rl
3657}
3658
3659fn capability_set_from_json(caps: CapabilitySetJson) -> Result<CapabilitySet, String> {
3660 let mut cs = CapabilitySet::new();
3661 if let Some(h) = caps.hardware {
3662 cs = cs.with_hardware(hardware_from_json(h));
3663 }
3664 if let Some(s) = caps.software {
3665 cs = cs.with_software(software_from_json(s));
3666 }
3667 for m in caps.models {
3668 cs = cs.add_model(model_from_json(m)?);
3669 }
3670 for t in caps.tools {
3671 cs = cs.add_tool(tool_from_json(t));
3672 }
3673 for tag in caps.tags {
3681 if tag == TAG_SCOPE_SUBNET_LOCAL {
3682 cs = cs.with_subnet_local_scope();
3683 } else if let Some(id) = tag.strip_prefix(TAG_SCOPE_TENANT_PREFIX) {
3684 cs = cs.with_tenant_scope(id);
3685 } else if let Some(name) = tag.strip_prefix(TAG_SCOPE_REGION_PREFIX) {
3686 cs = cs.with_region_scope(name);
3687 } else {
3688 cs = cs.add_tag(tag);
3689 }
3690 }
3691 if let Some(l) = caps.limits {
3692 cs = cs.with_limits(limits_from_json(l));
3693 }
3694 Ok(cs)
3695}
3696
3697fn capability_filter_from_json(f: CapabilityFilterJson) -> Result<CapabilityFilter, String> {
3698 let mut cf = CapabilityFilter::new();
3699 for t in f.require_tags {
3700 cf = cf.require_tag(t);
3701 }
3702 for m in f.require_models {
3703 cf = cf.require_model(m);
3704 }
3705 for t in f.require_tools {
3706 cf = cf.require_tool(t);
3707 }
3708 if let Some(mb) = f.min_memory_gb {
3709 cf = cf.with_min_memory(mb);
3710 }
3711 if f.require_gpu.unwrap_or(false) {
3712 cf = cf.require_gpu();
3713 }
3714 if let Some(v) = f.gpu_vendor {
3715 cf = cf.with_gpu_vendor(parse_gpu_vendor_cap(&v));
3716 }
3717 if let Some(mb) = f.min_vram_gb {
3718 cf = cf.with_min_vram(mb);
3719 }
3720 if let Some(n) = f.min_context_length {
3721 cf = cf.with_min_context(n);
3722 }
3723 for m in f.require_modalities {
3724 match parse_modality_cap(&m) {
3734 Some(parsed) => cf = cf.require_modality(parsed),
3735 None => return Err(m),
3736 }
3737 }
3738 Ok(cf)
3739}
3740
3741pub(crate) const NET_ERR_CAPABILITY: c_int = -128;
3744
3745#[unsafe(no_mangle)]
3752pub unsafe extern "C" fn net_mesh_announce_capabilities(
3753 handle: *mut MeshNodeHandle,
3754 caps_json: *const c_char,
3755) -> c_int {
3756 if handle.is_null() || caps_json.is_null() {
3757 return NetError::NullPointer.into();
3758 }
3759 let h = unsafe { &*handle };
3760 let _op = match h.guard.try_enter() {
3761 Some(op) => op,
3762 None => return NetError::ShuttingDown.into(),
3763 };
3764 let Some(s) = (unsafe { c_str_to_string(caps_json) }) else {
3765 return NetError::InvalidUtf8.into();
3766 };
3767 let parsed: CapabilitySetJson = match serde_json::from_str(&s) {
3768 Ok(v) => v,
3769 Err(_) => return NetError::InvalidJson.into(),
3770 };
3771 let caps = match capability_set_from_json(parsed) {
3775 Ok(c) => c,
3776 Err(_) => return NetError::InvalidJson.into(),
3777 };
3778 let node = h.inner.clone();
3779 match block_on(async move { node.announce_capabilities(caps).await }) {
3780 Ok(()) => 0,
3781 Err(_) => NET_ERR_CAPABILITY,
3782 }
3783}
3784
3785#[unsafe(no_mangle)]
3788pub unsafe extern "C" fn net_mesh_find_nodes(
3789 handle: *mut MeshNodeHandle,
3790 filter_json: *const c_char,
3791 out_json: *mut *mut c_char,
3792 out_len: *mut usize,
3793) -> c_int {
3794 if handle.is_null() || filter_json.is_null() || out_json.is_null() || out_len.is_null() {
3795 return NetError::NullPointer.into();
3796 }
3797 let h = unsafe { &*handle };
3798 let _op = match h.guard.try_enter() {
3799 Some(op) => op,
3800 None => return NetError::ShuttingDown.into(),
3801 };
3802 let Some(s) = (unsafe { c_str_to_string(filter_json) }) else {
3803 return NetError::InvalidUtf8.into();
3804 };
3805 let parsed: CapabilityFilterJson = match serde_json::from_str(&s) {
3806 Ok(v) => v,
3807 Err(_) => return NetError::InvalidJson.into(),
3808 };
3809 let filter = match capability_filter_from_json(parsed) {
3813 Ok(f) => f,
3814 Err(_) => return NetError::InvalidJson.into(),
3815 };
3816 let ids = h.inner.find_nodes_by_filter(&filter);
3817 write_json_out(&ids, out_json, out_len)
3818}
3819
3820#[derive(serde::Deserialize)]
3839struct ScopeFilterJson {
3840 kind: String,
3841 #[serde(default)]
3842 tenant: Option<String>,
3843 #[serde(default)]
3844 tenants: Option<Vec<String>>,
3845 #[serde(default)]
3846 region: Option<String>,
3847 #[serde(default)]
3848 regions: Option<Vec<String>>,
3849}
3850
3851enum ScopeFilterOwned {
3857 Any,
3858 GlobalOnly,
3859 SameSubnet,
3860 Tenant(String),
3861 Tenants(Vec<String>),
3862 Region(String),
3863 Regions(Vec<String>),
3864}
3865
3866fn scope_filter_from_json(f: ScopeFilterJson) -> Result<ScopeFilterOwned, NetError> {
3885 fn clean(v: Vec<String>) -> Option<Vec<String>> {
3889 let cleaned: Vec<String> = v.into_iter().filter(|s| !s.is_empty()).collect();
3890 (!cleaned.is_empty()).then_some(cleaned)
3891 }
3892 let filter = match f.kind.as_str() {
3893 "any" => ScopeFilterOwned::Any,
3894 "global_only" | "globalOnly" => ScopeFilterOwned::GlobalOnly,
3895 "same_subnet" | "sameSubnet" => ScopeFilterOwned::SameSubnet,
3896 "tenant" => match f.tenant {
3897 Some(t) if !t.is_empty() => ScopeFilterOwned::Tenant(t),
3898 _ => return Err(NetError::InvalidArgument),
3899 },
3900 "tenants" => match f.tenants.and_then(clean) {
3901 Some(ts) => ScopeFilterOwned::Tenants(ts),
3902 None => return Err(NetError::InvalidArgument),
3903 },
3904 "region" => match f.region {
3905 Some(r) if !r.is_empty() => ScopeFilterOwned::Region(r),
3906 _ => return Err(NetError::InvalidArgument),
3907 },
3908 "regions" => match f.regions.and_then(clean) {
3909 Some(rs) => ScopeFilterOwned::Regions(rs),
3910 None => return Err(NetError::InvalidArgument),
3911 },
3912 _ => return Err(NetError::InvalidArgument),
3913 };
3914 Ok(filter)
3915}
3916
3917fn with_scope_filter<R>(
3922 owned: &ScopeFilterOwned,
3923 f: impl FnOnce(&crate::adapter::net::behavior::capability::ScopeFilter<'_>) -> R,
3924) -> R {
3925 use crate::adapter::net::behavior::capability::ScopeFilter as F;
3926 match owned {
3927 ScopeFilterOwned::Any => f(&F::Any),
3928 ScopeFilterOwned::GlobalOnly => f(&F::GlobalOnly),
3929 ScopeFilterOwned::SameSubnet => f(&F::SameSubnet),
3930 ScopeFilterOwned::Tenant(t) => f(&F::Tenant(t.as_str())),
3931 ScopeFilterOwned::Tenants(ts) => {
3932 let refs: Vec<&str> = ts.iter().map(|s| s.as_str()).collect();
3933 f(&F::Tenants(refs.as_slice()))
3934 }
3935 ScopeFilterOwned::Region(r) => f(&F::Region(r.as_str())),
3936 ScopeFilterOwned::Regions(rs) => {
3937 let refs: Vec<&str> = rs.iter().map(|s| s.as_str()).collect();
3938 f(&F::Regions(refs.as_slice()))
3939 }
3940 }
3941}
3942
3943#[unsafe(no_mangle)]
3966pub unsafe extern "C" fn net_mesh_find_nodes_scoped(
3967 handle: *mut MeshNodeHandle,
3968 filter_json: *const c_char,
3969 scope_json: *const c_char,
3970 out_json: *mut *mut c_char,
3971 out_len: *mut usize,
3972) -> c_int {
3973 if handle.is_null()
3974 || filter_json.is_null()
3975 || scope_json.is_null()
3976 || out_json.is_null()
3977 || out_len.is_null()
3978 {
3979 return NetError::NullPointer.into();
3980 }
3981 let h = unsafe { &*handle };
3982 let _op = match h.guard.try_enter() {
3983 Some(op) => op,
3984 None => return NetError::ShuttingDown.into(),
3985 };
3986 let Some(filter_s) = (unsafe { c_str_to_string(filter_json) }) else {
3987 return NetError::InvalidUtf8.into();
3988 };
3989 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
3990 return NetError::InvalidUtf8.into();
3991 };
3992 let parsed_filter: CapabilityFilterJson = match serde_json::from_str(&filter_s) {
3993 Ok(v) => v,
3994 Err(_) => return NetError::InvalidJson.into(),
3995 };
3996 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
3997 Ok(v) => v,
3998 Err(_) => return NetError::InvalidJson.into(),
3999 };
4000 let filter = match capability_filter_from_json(parsed_filter) {
4001 Ok(f) => f,
4002 Err(_) => return NetError::InvalidJson.into(),
4003 };
4004 let owned = match scope_filter_from_json(parsed_scope) {
4005 Ok(v) => v,
4006 Err(e) => return e.into(),
4007 };
4008 let ids = with_scope_filter(&owned, |sf| {
4009 h.inner.find_nodes_by_filter_scoped(&filter, sf)
4010 });
4011 write_json_out(&ids, out_json, out_len)
4012}
4013
4014#[derive(serde::Deserialize)]
4028struct CapabilityRequirementJson {
4029 #[serde(default)]
4030 filter: CapabilityFilterJson,
4031 #[serde(default)]
4032 prefer_more_memory: f32,
4033 #[serde(default)]
4034 prefer_more_vram: f32,
4035 #[serde(default)]
4036 prefer_faster_inference: f32,
4037 #[serde(default)]
4038 prefer_loaded_models: f32,
4039}
4040
4041fn capability_requirement_from_json(
4042 j: CapabilityRequirementJson,
4043) -> Result<crate::adapter::net::behavior::capability::CapabilityRequirement, String> {
4044 Ok(
4045 crate::adapter::net::behavior::capability::CapabilityRequirement::from_filter(
4046 capability_filter_from_json(j.filter)?,
4047 )
4048 .prefer_memory(j.prefer_more_memory)
4049 .prefer_vram(j.prefer_more_vram)
4050 .prefer_speed(j.prefer_faster_inference)
4051 .prefer_loaded(j.prefer_loaded_models),
4052 )
4053}
4054
4055#[unsafe(no_mangle)]
4065pub unsafe extern "C" fn net_mesh_find_best_node(
4066 handle: *mut MeshNodeHandle,
4067 requirement_json: *const c_char,
4068 out_node_id: *mut u64,
4069 out_has_match: *mut c_int,
4070) -> c_int {
4071 if handle.is_null()
4072 || requirement_json.is_null()
4073 || out_node_id.is_null()
4074 || out_has_match.is_null()
4075 {
4076 return NetError::NullPointer.into();
4077 }
4078 let h = unsafe { &*handle };
4079 let _op = match h.guard.try_enter() {
4080 Some(op) => op,
4081 None => return NetError::ShuttingDown.into(),
4082 };
4083 let Some(s) = (unsafe { c_str_to_string(requirement_json) }) else {
4084 return NetError::InvalidUtf8.into();
4085 };
4086 let parsed: CapabilityRequirementJson = match serde_json::from_str(&s) {
4087 Ok(v) => v,
4088 Err(_) => return NetError::InvalidJson.into(),
4089 };
4090 let req = match capability_requirement_from_json(parsed) {
4091 Ok(r) => r,
4092 Err(_) => return NetError::InvalidJson.into(),
4093 };
4094 match h.inner.find_best_node(&req) {
4095 Some(node_id) => unsafe {
4096 *out_node_id = node_id;
4097 *out_has_match = 1;
4098 },
4099 None => unsafe {
4100 *out_has_match = 0;
4101 },
4102 }
4103 0
4104}
4105
4106#[unsafe(no_mangle)]
4115pub unsafe extern "C" fn net_mesh_find_best_node_scoped(
4116 handle: *mut MeshNodeHandle,
4117 requirement_json: *const c_char,
4118 scope_json: *const c_char,
4119 out_node_id: *mut u64,
4120 out_has_match: *mut c_int,
4121) -> c_int {
4122 if handle.is_null()
4123 || requirement_json.is_null()
4124 || scope_json.is_null()
4125 || out_node_id.is_null()
4126 || out_has_match.is_null()
4127 {
4128 return NetError::NullPointer.into();
4129 }
4130 let h = unsafe { &*handle };
4131 let _op = match h.guard.try_enter() {
4132 Some(op) => op,
4133 None => return NetError::ShuttingDown.into(),
4134 };
4135 let Some(req_s) = (unsafe { c_str_to_string(requirement_json) }) else {
4136 return NetError::InvalidUtf8.into();
4137 };
4138 let Some(scope_s) = (unsafe { c_str_to_string(scope_json) }) else {
4139 return NetError::InvalidUtf8.into();
4140 };
4141 let parsed_req: CapabilityRequirementJson = match serde_json::from_str(&req_s) {
4142 Ok(v) => v,
4143 Err(_) => return NetError::InvalidJson.into(),
4144 };
4145 let parsed_scope: ScopeFilterJson = match serde_json::from_str(&scope_s) {
4146 Ok(v) => v,
4147 Err(_) => return NetError::InvalidJson.into(),
4148 };
4149 let req = match capability_requirement_from_json(parsed_req) {
4150 Ok(r) => r,
4151 Err(_) => return NetError::InvalidJson.into(),
4152 };
4153 let owned = match scope_filter_from_json(parsed_scope) {
4154 Ok(v) => v,
4155 Err(e) => return e.into(),
4156 };
4157 let result = with_scope_filter(&owned, |sf| h.inner.find_best_node_scoped(&req, sf));
4158 match result {
4159 Some(node_id) => unsafe {
4160 *out_node_id = node_id;
4161 *out_has_match = 1;
4162 },
4163 None => unsafe {
4164 *out_has_match = 0;
4165 },
4166 }
4167 0
4168}
4169
4170#[unsafe(no_mangle)]
4172pub unsafe extern "C" fn net_normalize_gpu_vendor(
4173 raw: *const c_char,
4174 out_json: *mut *mut c_char,
4175 out_len: *mut usize,
4176) -> c_int {
4177 if raw.is_null() || out_json.is_null() || out_len.is_null() {
4178 return NetError::NullPointer.into();
4179 }
4180 let Some(s) = (unsafe { c_str_to_string(raw) }) else {
4181 return NetError::InvalidUtf8.into();
4182 };
4183 let canonical = gpu_vendor_to_string_cap(parse_gpu_vendor_cap(&s));
4184 write_string_out(canonical.to_string(), out_json, out_len)
4185}
4186
4187pub(crate) const NET_ERR_GANG_INVALID: c_int = -140;
4198
4199#[derive(Deserialize)]
4203struct GangCriteriaJson {
4204 #[serde(default)]
4206 tags_all: Vec<String>,
4207 #[serde(default)]
4208 tags_any: Vec<String>,
4209 #[serde(default)]
4210 tag_groups_all: Vec<Vec<String>>,
4211 #[serde(default)]
4213 region: Option<String>,
4214 #[serde(default)]
4216 min_units: usize,
4217 #[serde(default)]
4218 max_load: Option<f32>,
4219 #[serde(default)]
4220 max_p50_latency_us: Option<u32>,
4221 #[serde(default)]
4222 require_all: Vec<String>,
4223 #[serde(default)]
4224 require_any: Vec<String>,
4225 #[serde(default)]
4226 selection: Option<String>,
4227 #[serde(default)]
4228 load_band_target: Option<f32>,
4229 #[serde(default)]
4230 prefer_capability: Option<String>,
4231}
4232
4233#[derive(Deserialize)]
4236struct IslandRecordJson {
4237 id: u64,
4238 #[serde(default)]
4239 units: Vec<u32>,
4240 #[serde(default)]
4241 capabilities: Vec<String>,
4242 #[serde(default)]
4243 load: f32,
4244 #[serde(default)]
4245 p50_latency_us: u32,
4246}
4247
4248fn build_gang_criteria(
4249 c: GangCriteriaJson,
4250) -> Option<crate::adapter::net::behavior::gang::MatchCriteria> {
4251 use crate::adapter::net::behavior::fold::{CapabilityFilter, CapabilityQuery};
4252 use crate::adapter::net::behavior::gang::{MatchCriteria, NumericFilter, SelectionPolicy};
4253 let selection = match c.selection.as_deref() {
4254 None | Some("least_loaded") => SelectionPolicy::LeastLoaded,
4255 Some("pack") => SelectionPolicy::Pack,
4256 Some("lowest_id") => SelectionPolicy::LowestId,
4257 Some("load_band") => SelectionPolicy::LoadBand(c.load_band_target.unwrap_or(0.5)),
4258 Some(_) => return None,
4259 };
4260 Some(MatchCriteria {
4261 capability: CapabilityQuery::Composite(CapabilityFilter {
4262 tags_all: c.tags_all,
4263 tags_any: c.tags_any,
4264 tag_groups_all: c.tag_groups_all,
4265 region: c.region,
4266 ..Default::default()
4267 }),
4268 numeric: NumericFilter {
4269 min_units: c.min_units,
4270 max_load: c.max_load,
4271 max_p50_latency_us: c.max_p50_latency_us,
4272 require_all: c.require_all,
4273 require_any: c.require_any,
4274 },
4275 selection,
4276 prefer_capability: c.prefer_capability,
4277 })
4278}
4279
4280#[unsafe(no_mangle)]
4285pub unsafe extern "C" fn net_mesh_publish_island_topology(
4286 handle: *mut MeshNodeHandle,
4287 record_json: *const c_char,
4288 out_count: *mut usize,
4289) -> c_int {
4290 if handle.is_null() || record_json.is_null() {
4291 return NetError::NullPointer.into();
4292 }
4293 let h = unsafe { &*handle };
4294 let _op = match h.guard.try_enter() {
4295 Some(op) => op,
4296 None => return NetError::ShuttingDown.into(),
4297 };
4298 let Some(js) = (unsafe { c_str_to_string(record_json) }) else {
4299 return NetError::InvalidUtf8.into();
4300 };
4301 let rec: IslandRecordJson = match serde_json::from_str(&js) {
4302 Ok(r) => r,
4303 Err(_) => return NET_ERR_GANG_INVALID,
4304 };
4305 use crate::adapter::net::behavior::fold::{IslandRecord, UnitSet};
4306 let record = IslandRecord {
4307 id: rec.id,
4308 units: UnitSet::new(rec.units),
4309 host: 0, capabilities: rec.capabilities,
4311 load: rec.load,
4312 p50_latency_us: rec.p50_latency_us,
4313 };
4314 let node = h.inner.clone();
4315 match block_on(async move { node.publish_island_topology(record).await }) {
4316 Ok(n) => {
4317 if !out_count.is_null() {
4318 unsafe {
4319 *out_count = n;
4320 }
4321 }
4322 0
4323 }
4324 Err(e) => adapter_err_to_code(&e),
4325 }
4326}
4327
4328#[unsafe(no_mangle)]
4332pub unsafe extern "C" fn net_mesh_match_islands(
4333 handle: *mut MeshNodeHandle,
4334 criteria_json: *const c_char,
4335 out_ids: *mut u64,
4336 cap: usize,
4337 out_count: *mut usize,
4338) -> c_int {
4339 if handle.is_null() || criteria_json.is_null() || out_count.is_null() {
4340 return NetError::NullPointer.into();
4341 }
4342 let h = unsafe { &*handle };
4343 let _op = match h.guard.try_enter() {
4344 Some(op) => op,
4345 None => return NetError::ShuttingDown.into(),
4346 };
4347 let Some(js) = (unsafe { c_str_to_string(criteria_json) }) else {
4348 return NetError::InvalidUtf8.into();
4349 };
4350 let parsed: GangCriteriaJson = match serde_json::from_str(&js) {
4351 Ok(c) => c,
4352 Err(_) => return NET_ERR_GANG_INVALID,
4353 };
4354 let Some(criteria) = build_gang_criteria(parsed) else {
4355 return NET_ERR_GANG_INVALID;
4356 };
4357 let ids = h.inner.match_islands(&criteria);
4358 unsafe {
4359 *out_count = ids.len();
4360 if !out_ids.is_null() {
4361 let n = ids.len().min(cap);
4362 std::ptr::copy_nonoverlapping(ids.as_ptr(), out_ids, n);
4363 }
4364 }
4365 0
4366}
4367
4368#[unsafe(no_mangle)]
4371pub unsafe extern "C" fn net_mesh_reserve_island(
4372 handle: *mut MeshNodeHandle,
4373 island: u64,
4374 until_unix_us: u64,
4375 out_outcome: *mut c_int,
4376) -> c_int {
4377 if handle.is_null() || out_outcome.is_null() {
4378 return NetError::NullPointer.into();
4379 }
4380 let h = unsafe { &*handle };
4381 let _op = match h.guard.try_enter() {
4382 Some(op) => op,
4383 None => return NetError::ShuttingDown.into(),
4384 };
4385 let node = h.inner.clone();
4386 match block_on(async move { node.reserve_island(island, until_unix_us).await }) {
4387 Ok(outcome) => {
4388 unsafe {
4389 *out_outcome = claim_outcome_code(outcome);
4390 }
4391 0
4392 }
4393 Err(e) => adapter_err_to_code(&e),
4394 }
4395}
4396
4397#[unsafe(no_mangle)]
4400pub unsafe extern "C" fn net_mesh_release_island(
4401 handle: *mut MeshNodeHandle,
4402 island: u64,
4403 out_outcome: *mut c_int,
4404) -> c_int {
4405 if handle.is_null() || out_outcome.is_null() {
4406 return NetError::NullPointer.into();
4407 }
4408 let h = unsafe { &*handle };
4409 let _op = match h.guard.try_enter() {
4410 Some(op) => op,
4411 None => return NetError::ShuttingDown.into(),
4412 };
4413 let node = h.inner.clone();
4414 match block_on(async move { node.release_island(island).await }) {
4415 Ok(outcome) => {
4416 unsafe {
4417 *out_outcome = claim_outcome_code(outcome);
4418 }
4419 0
4420 }
4421 Err(e) => adapter_err_to_code(&e),
4422 }
4423}
4424
4425#[unsafe(no_mangle)]
4429pub unsafe extern "C" fn net_mesh_claim_island(
4430 handle: *mut MeshNodeHandle,
4431 criteria_json: *const c_char,
4432 until_unix_us: u64,
4433 out_found: *mut c_int,
4434 out_island: *mut u64,
4435) -> c_int {
4436 if handle.is_null() || criteria_json.is_null() || out_found.is_null() || out_island.is_null() {
4437 return NetError::NullPointer.into();
4438 }
4439 unsafe {
4444 *out_found = 0;
4445 *out_island = 0;
4446 }
4447 let h = unsafe { &*handle };
4448 let _op = match h.guard.try_enter() {
4449 Some(op) => op,
4450 None => return NetError::ShuttingDown.into(),
4451 };
4452 let Some(js) = (unsafe { c_str_to_string(criteria_json) }) else {
4453 return NetError::InvalidUtf8.into();
4454 };
4455 let parsed: GangCriteriaJson = match serde_json::from_str(&js) {
4456 Ok(c) => c,
4457 Err(_) => return NET_ERR_GANG_INVALID,
4458 };
4459 let Some(criteria) = build_gang_criteria(parsed) else {
4460 return NET_ERR_GANG_INVALID;
4461 };
4462 let node = h.inner.clone();
4463 match block_on(async move { node.claim_island(&criteria, until_unix_us).await }) {
4464 Ok(Some(id)) => {
4465 unsafe {
4466 *out_found = 1;
4467 *out_island = id;
4468 }
4469 0
4470 }
4471 Ok(None) => 0,
4472 Err(e) => adapter_err_to_code(&e),
4473 }
4474}
4475
4476fn claim_outcome_code(o: crate::adapter::net::behavior::gang::ClaimOutcome) -> c_int {
4477 use crate::adapter::net::behavior::gang::ClaimOutcome;
4478 match o {
4479 ClaimOutcome::Won => 0,
4480 ClaimOutcome::Lost => 1,
4481 }
4482}
4483
4484#[cfg(test)]
4485mod tests {
4486 use super::*;
4487
4488 mod scope_filter_rejects_unusable {
4498 use super::super::{scope_filter_from_json, ScopeFilterJson, ScopeFilterOwned};
4499 use crate::ffi::NetError;
4500
4501 fn kind(kind: &str) -> ScopeFilterJson {
4502 ScopeFilterJson {
4503 kind: kind.into(),
4504 tenant: None,
4505 tenants: None,
4506 region: None,
4507 regions: None,
4508 }
4509 }
4510
4511 #[test]
4512 fn unknown_kind_is_invalid_argument() {
4513 assert!(matches!(
4514 scope_filter_from_json(kind("tenat")),
4515 Err(NetError::InvalidArgument)
4516 ));
4517 }
4518
4519 #[test]
4520 fn missing_or_empty_selectors_are_invalid_argument() {
4521 let cases = vec![
4522 kind("tenant"),
4523 ScopeFilterJson {
4524 tenant: Some(String::new()),
4525 ..kind("tenant")
4526 },
4527 kind("tenants"),
4528 ScopeFilterJson {
4529 tenants: Some(vec![String::new()]),
4530 ..kind("tenants")
4531 },
4532 kind("region"),
4533 ScopeFilterJson {
4534 region: Some(String::new()),
4535 ..kind("region")
4536 },
4537 kind("regions"),
4538 ScopeFilterJson {
4539 regions: Some(vec![String::new(), String::new()]),
4540 ..kind("regions")
4541 },
4542 ];
4543 for case in cases {
4544 let label = case.kind.clone();
4545 assert!(
4546 matches!(scope_filter_from_json(case), Err(NetError::InvalidArgument)),
4547 "kind {label:?} with an unusable selector must be \
4548 InvalidArgument, not a silent widen to Any"
4549 );
4550 }
4551 }
4552
4553 #[test]
4556 fn usable_filters_still_convert() {
4557 assert!(matches!(
4558 scope_filter_from_json(kind("any")),
4559 Ok(ScopeFilterOwned::Any)
4560 ));
4561 for k in ["global_only", "globalOnly"] {
4562 assert!(matches!(
4563 scope_filter_from_json(kind(k)),
4564 Ok(ScopeFilterOwned::GlobalOnly)
4565 ));
4566 }
4567 assert!(matches!(
4568 scope_filter_from_json(ScopeFilterJson {
4569 tenants: Some(vec![String::new(), "oem-123".into()]),
4570 ..kind("tenants")
4571 }),
4572 Ok(ScopeFilterOwned::Tenants(ts)) if ts == vec!["oem-123".to_string()]
4573 ));
4574 }
4575 }
4576
4577 #[cfg(feature = "nat-traversal")]
4585 mod traversal_stats_abi {
4586 use super::super::NetTraversalStatsV2;
4587 use std::mem::{align_of, offset_of, size_of};
4588
4589 fn rust_offset(name: &str) -> Option<usize> {
4594 Some(match name {
4595 "punches_attempted" => offset_of!(NetTraversalStatsV2, punches_attempted),
4596 "punches_succeeded" => offset_of!(NetTraversalStatsV2, punches_succeeded),
4597 "punches_failed" => offset_of!(NetTraversalStatsV2, punches_failed),
4598 "relay_fallbacks" => offset_of!(NetTraversalStatsV2, relay_fallbacks),
4599 "punch_timeouts" => offset_of!(NetTraversalStatsV2, punch_timeouts),
4600 "punch_rejections" => offset_of!(NetTraversalStatsV2, punch_rejections),
4601 "rendezvous_no_relay" => offset_of!(NetTraversalStatsV2, rendezvous_no_relay),
4602 "upgrades_attempted" => offset_of!(NetTraversalStatsV2, upgrades_attempted),
4603 "upgrades_succeeded" => offset_of!(NetTraversalStatsV2, upgrades_succeeded),
4604 "upgrades_deferred_busy" => offset_of!(NetTraversalStatsV2, upgrades_deferred_busy),
4605 "port_mapping_renewals" => offset_of!(NetTraversalStatsV2, port_mapping_renewals),
4606 "port_mapping_active" => offset_of!(NetTraversalStatsV2, port_mapping_active),
4607 "port_mapping_external" => offset_of!(NetTraversalStatsV2, port_mapping_external),
4608 _ => return None,
4609 })
4610 }
4611
4612 fn c_type_layout(ctype: &str) -> (usize, usize) {
4622 use std::mem::{align_of, size_of};
4623 use std::os::raw::c_char;
4624 match ctype {
4625 "uint64_t" => (size_of::<u64>(), align_of::<u64>()),
4626 "uint8_t" => (size_of::<u8>(), align_of::<u8>()),
4627 "char[64]" => (size_of::<c_char>() * 64, align_of::<c_char>()),
4628 other => panic!("unhandled C type in net_traversal_stats_v2_t: {other:?}"),
4629 }
4630 }
4631
4632 fn round_up(off: usize, align: usize) -> usize {
4633 off.div_ceil(align) * align
4634 }
4635
4636 fn parse_header_fields(header: &str) -> Vec<(String, String)> {
4640 let end = header
4641 .find("} net_traversal_stats_v2_t;")
4642 .expect("stats typedef present in header");
4643 let open = header[..end].rfind('{').expect("struct open brace");
4644 let mut fields = Vec::new();
4645 for line in header[open + 1..end].lines() {
4646 let line = line.trim();
4647 if line.is_empty()
4648 || line.starts_with("//")
4649 || line.starts_with('*')
4650 || line.starts_with("/*")
4651 {
4652 continue;
4653 }
4654 let decl = line.trim_end_matches(';').trim();
4655 let (ctype, name_arr) = decl
4656 .rsplit_once(char::is_whitespace)
4657 .expect("field decl shaped `type name`");
4658 let (ctype, name_arr) = (ctype.trim(), name_arr.trim());
4659 if let Some((name, arr)) = name_arr.split_once('[') {
4660 fields.push((format!("{ctype}[{arr}"), name.to_string()));
4661 } else {
4662 fields.push((ctype.to_string(), name_arr.to_string()));
4663 }
4664 }
4665 fields
4666 }
4667
4668 #[test]
4669 fn c_header_layout_matches_rust_repr_c() {
4670 let header =
4671 std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/include/net.go.h"))
4672 .expect("read include/net.go.h");
4673 let fields = parse_header_fields(&header);
4674 assert_eq!(
4675 fields.len(),
4676 13,
4677 "expected 13 fields in net_traversal_stats_v2_t, parsed {fields:?}",
4678 );
4679
4680 let mut off = 0usize;
4685 let mut align = 1usize;
4686 for (ctype, name) in &fields {
4687 let (sz, al) = c_type_layout(ctype);
4688 off = round_up(off, al);
4689 align = align.max(al);
4690 let rust = rust_offset(name)
4691 .unwrap_or_else(|| panic!("header field `{name}` has no Rust struct field"));
4692 assert_eq!(
4693 rust, off,
4694 "field `{name}`: Rust offset {rust} != C offset {off}"
4695 );
4696 off += sz;
4697 }
4698 assert_eq!(
4699 size_of::<NetTraversalStatsV2>(),
4700 round_up(off, align),
4701 "net_traversal_stats_v2_t total size drift (Rust vs C header)",
4702 );
4703 assert_eq!(
4704 align_of::<NetTraversalStatsV2>(),
4705 align,
4706 "net_traversal_stats_v2_t alignment drift (Rust vs C header)",
4707 );
4708 }
4709 }
4710
4711 #[test]
4723 fn saturating_u16_cap_clamps_at_u16_max() {
4724 assert_eq!(saturating_u16_cap(0), 0);
4725 assert_eq!(saturating_u16_cap(42), 42);
4726 assert_eq!(saturating_u16_cap(u16::MAX as u32), u16::MAX);
4727 assert_eq!(saturating_u16_cap(u16::MAX as u32 + 1), u16::MAX);
4728 assert_eq!(saturating_u16_cap(u32::MAX), u16::MAX);
4729 }
4730
4731 #[test]
4737 fn parse_peer_pubkey_hex_accepts_valid_and_rejects_malformed() {
4738 use std::ffi::CString;
4739
4740 let valid = CString::new("ab".repeat(32)).unwrap();
4741 let parsed = unsafe { parse_peer_pubkey_hex(valid.as_ptr()) };
4743 assert_eq!(parsed, Ok([0xABu8; 32]), "64-char hex round-trips");
4744
4745 let bad_hex = CString::new("zz".repeat(32)).unwrap();
4746 let err = unsafe { parse_peer_pubkey_hex(bad_hex.as_ptr()) };
4748 assert_eq!(err, Err(NET_ERR_MESH_HANDSHAKE), "non-hex rejects");
4749
4750 let short = CString::new("abcd").unwrap();
4751 let err = unsafe { parse_peer_pubkey_hex(short.as_ptr()) };
4753 assert_eq!(err, Err(NET_ERR_MESH_HANDSHAKE), "wrong length rejects");
4754
4755 let non_utf8 = CString::new(vec![0xFFu8, 0xFEu8]).unwrap();
4757 let err = unsafe { parse_peer_pubkey_hex(non_utf8.as_ptr()) };
4759 assert_eq!(
4760 err,
4761 Err(NetError::InvalidUtf8.into()),
4762 "non-UTF-8 C string rejects with the UTF-8 code",
4763 );
4764 }
4765
4766 #[cfg(feature = "nat-traversal")]
4773 #[test]
4774 fn traversal_stats_v2_fill_maps_all_fields() {
4775 use crate::adapter::net::traversal::TraversalStatsSnapshot;
4776
4777 let snap = TraversalStatsSnapshot {
4778 punches_attempted: 1,
4779 punches_succeeded: 2,
4780 relay_fallbacks: 3,
4781 port_mapping_active: true,
4782 port_mapping_external: Some("203.0.113.5:4321".parse().unwrap()),
4783 port_mapping_renewals: 4,
4784 upgrades_attempted: 5,
4785 upgrades_succeeded: 6,
4786 upgrades_deferred_busy: 7,
4787 punches_failed: 8,
4788 punch_timeouts: 9,
4789 punch_rejections: 10,
4790 rendezvous_no_relay: 11,
4791 };
4792 let mut out = NetTraversalStatsV2 {
4793 punches_attempted: 0,
4794 punches_succeeded: 0,
4795 punches_failed: 0,
4796 relay_fallbacks: 0,
4797 punch_timeouts: 0,
4798 punch_rejections: 0,
4799 rendezvous_no_relay: 0,
4800 upgrades_attempted: 0,
4801 upgrades_succeeded: 0,
4802 upgrades_deferred_busy: 0,
4803 port_mapping_renewals: 0,
4804 port_mapping_active: 0,
4805 port_mapping_external: [0x7F; 64], };
4807 fill_traversal_stats_v2(&snap, &mut out);
4808
4809 assert_eq!(out.punches_attempted, 1);
4810 assert_eq!(out.punches_succeeded, 2);
4811 assert_eq!(out.relay_fallbacks, 3);
4812 assert_eq!(out.port_mapping_renewals, 4);
4813 assert_eq!(out.upgrades_attempted, 5);
4814 assert_eq!(out.upgrades_succeeded, 6);
4815 assert_eq!(out.upgrades_deferred_busy, 7);
4816 assert_eq!(out.punches_failed, 8);
4817 assert_eq!(out.punch_timeouts, 9);
4818 assert_eq!(out.punch_rejections, 10);
4819 assert_eq!(out.rendezvous_no_relay, 11);
4820 assert_eq!(out.port_mapping_active, 1);
4821 let s: String = out
4822 .port_mapping_external
4823 .iter()
4824 .take_while(|&&c| c != 0)
4825 .map(|&c| c as u8 as char)
4826 .collect();
4827 assert_eq!(s, "203.0.113.5:4321");
4828 assert!(out.port_mapping_external.contains(&0));
4830
4831 let snap_off = TraversalStatsSnapshot {
4833 port_mapping_active: false,
4834 port_mapping_external: None,
4835 ..snap
4836 };
4837 fill_traversal_stats_v2(&snap_off, &mut out);
4838 assert_eq!(out.port_mapping_active, 0);
4839 assert_eq!(
4840 out.port_mapping_external[0], 0,
4841 "empty string when inactive"
4842 );
4843 }
4844
4845 #[test]
4857 fn parse_modality_cap_returns_none_on_unknown_strings() {
4858 for (s, expected) in [
4860 ("text", Modality::Text),
4861 ("Text", Modality::Text),
4862 ("TEXT", Modality::Text),
4863 ("image", Modality::Image),
4864 ("audio", Modality::Audio),
4865 ("video", Modality::Video),
4866 ("code", Modality::Code),
4867 ("embedding", Modality::Embedding),
4868 ("tool-use", Modality::ToolUse),
4869 ("tool_use", Modality::ToolUse),
4870 ("tooluse", Modality::ToolUse),
4871 ] {
4872 assert_eq!(
4873 parse_modality_cap(s),
4874 Some(expected),
4875 "known modality `{s}` must parse",
4876 );
4877 }
4878
4879 for s in ["audoi", "imageX", "vidoe", "embeding", "garbage", ""] {
4881 assert_eq!(
4882 parse_modality_cap(s),
4883 None,
4884 "unknown modality `{s}` must return None — pre-fix this \
4885 fell back to Modality::Text, advertising a capability \
4886 the node didn't actually have",
4887 );
4888 }
4889 }
4890
4891 fn abi_verify(entity_id: &[u8], msg: &[u8], sig: &[u8]) -> (c_int, c_int) {
4901 let mut valid: c_int = -1;
4902 let rc = unsafe {
4903 net_verify_signature(
4904 entity_id.as_ptr(),
4905 entity_id.len(),
4906 if msg.is_empty() {
4910 std::ptr::null()
4911 } else {
4912 msg.as_ptr()
4913 },
4914 msg.len(),
4915 sig.as_ptr(),
4916 sig.len(),
4917 &mut valid,
4918 )
4919 };
4920 (rc, valid)
4921 }
4922
4923 #[test]
4931 fn verify_signature_round_trips_and_rejects_tampering() {
4932 use crate::adapter::net::identity::EntityKeypair;
4933
4934 let keypair = EntityKeypair::generate();
4935 let entity = keypair.entity_id().as_bytes().to_vec();
4936 let message = b"the exact bytes that were signed";
4937 let sig = keypair.sign(message).to_bytes();
4938
4939 assert_eq!(
4940 abi_verify(&entity, message, &sig),
4941 (0, 1),
4942 "a freshly produced signature must verify",
4943 );
4944
4945 assert_eq!(
4948 abi_verify(&entity, b"different bytes", &sig),
4949 (0, 0),
4950 "a signature must not verify against another message",
4951 );
4952
4953 let other = EntityKeypair::generate();
4955 assert_eq!(
4956 abi_verify(other.entity_id().as_bytes(), message, &sig),
4957 (0, 0),
4958 "a signature must not verify under another entity",
4959 );
4960
4961 let mut bad = sig;
4964 bad[0] ^= 0xff;
4965 assert_eq!(abi_verify(&entity, message, &bad), (0, 0));
4966 assert_eq!(
4967 abi_verify(&entity, message, &[0u8; 64]),
4968 (0, 0),
4969 "64 zero bytes is the signature a length check accepts",
4970 );
4971 }
4972
4973 #[test]
4980 fn verify_signature_handles_an_empty_message() {
4981 use crate::adapter::net::identity::EntityKeypair;
4982
4983 let keypair = EntityKeypair::generate();
4984 let entity = keypair.entity_id().as_bytes().to_vec();
4985 let sig = keypair.sign(b"").to_bytes();
4986
4987 assert_eq!(
4988 abi_verify(&entity, b"", &sig),
4989 (0, 1),
4990 "a NULL message with length 0 is an empty message, not a \
4991 missing argument",
4992 );
4993 let other_sig = keypair.sign(b"not empty").to_bytes();
4995 assert_eq!(abi_verify(&entity, b"", &other_sig), (0, 0));
4996 }
4997
4998 #[test]
5006 fn verify_signature_rejects_malformed_arguments() {
5007 use crate::adapter::net::identity::EntityKeypair;
5008
5009 let keypair = EntityKeypair::generate();
5010 let entity = keypair.entity_id().as_bytes().to_vec();
5011 let msg = b"payload";
5012 let sig = keypair.sign(msg).to_bytes();
5013
5014 for bad_id_len in [0usize, 31, 33] {
5016 let bad_id = vec![0u8; bad_id_len];
5017 let (rc, _) = abi_verify(&bad_id, msg, &sig);
5018 assert_eq!(
5019 rc, NET_ERR_IDENTITY,
5020 "a {bad_id_len}-byte entity id must be refused",
5021 );
5022 }
5023
5024 for bad_sig_len in [0usize, 63, 65] {
5026 let bad_sig = vec![0u8; bad_sig_len];
5027 let (rc, _) = abi_verify(&entity, msg, &bad_sig);
5028 assert_eq!(
5029 rc, NET_ERR_IDENTITY,
5030 "a {bad_sig_len}-byte signature must be refused",
5031 );
5032 }
5033
5034 let rc = unsafe {
5037 net_verify_signature(
5038 entity.as_ptr(),
5039 entity.len(),
5040 msg.as_ptr(),
5041 msg.len(),
5042 sig.as_ptr(),
5043 sig.len(),
5044 std::ptr::null_mut(),
5045 )
5046 };
5047 assert_eq!(rc, c_int::from(NetError::NullPointer));
5048
5049 let mut valid: c_int = -1;
5052 let rc = unsafe {
5053 net_verify_signature(
5054 entity.as_ptr(),
5055 entity.len(),
5056 msg.as_ptr(),
5057 msg.len(),
5058 std::ptr::null(),
5059 64,
5060 &mut valid,
5061 )
5062 };
5063 assert_eq!(rc, c_int::from(NetError::NullPointer));
5064
5065 let rc = unsafe {
5066 net_verify_signature(
5067 entity.as_ptr(),
5068 entity.len(),
5069 std::ptr::null(),
5070 7,
5071 sig.as_ptr(),
5072 sig.len(),
5073 &mut valid,
5074 )
5075 };
5076 assert_eq!(
5077 rc,
5078 c_int::from(NetError::NullPointer),
5079 "a NULL message with a non-zero length must not be \
5080 dereferenced",
5081 );
5082 }
5083
5084 #[test]
5095 fn wildcard_scope_round_trips_through_the_c_converters() {
5096 let parsed = parse_scope_list(r#"["publish","wildcard"]"#).expect("wildcard must parse");
5097 assert!(parsed.contains(TokenScope::WILDCARD));
5098 assert!(parsed.contains(TokenScope::PUBLISH));
5099
5100 let rendered = scope_to_strings(parsed);
5101 assert!(
5102 rendered.contains(&"wildcard"),
5103 "wildcard must render, got {rendered:?}",
5104 );
5105 }
5106
5107 #[test]
5110 fn scope_vocabulary_is_exactly_the_five_names() {
5111 for name in ["publish", "subscribe", "admin", "delegate", "wildcard"] {
5112 let json = format!(r#"["{name}"]"#);
5113 let parsed = parse_scope_list(&json).expect("documented scope must parse");
5114 assert!(scope_to_strings(parsed).contains(&name));
5115 }
5116 for bad in [
5117 r#"["wild"]"#,
5118 r#"["WILDCARD"]"#,
5119 r#"["all"]"#,
5120 r#"["none"]"#,
5121 ] {
5122 assert!(
5123 parse_scope_list(bad).is_none(),
5124 "unknown scope must be refused: {bad}",
5125 );
5126 }
5127 }
5128
5129 #[test]
5130 fn unknown_modality_rejects_the_announcement() {
5131 let json = r#"{"models":[{"model_id":"m","modalities":["audoi"]}]}"#;
5132 let parsed: CapabilitySetJson = serde_json::from_str(json).unwrap();
5133 assert_eq!(
5134 capability_set_from_json(parsed).unwrap_err(),
5135 "audoi",
5136 "the error must name the offending value",
5137 );
5138 }
5139
5140 #[test]
5144 fn unknown_modality_rejects_the_filter() {
5145 let json = r#"{"require_modalities":["audoi"]}"#;
5146 let parsed: CapabilityFilterJson = serde_json::from_str(json).unwrap();
5147 assert_eq!(capability_filter_from_json(parsed).unwrap_err(), "audoi");
5148 }
5149
5150 #[test]
5153 fn every_documented_modality_still_converts() {
5154 for name in [
5155 "text",
5156 "image",
5157 "audio",
5158 "video",
5159 "code",
5160 "embedding",
5161 "tool-use",
5162 "tool_use",
5163 "tooluse",
5164 "TEXT",
5165 ] {
5166 let json = format!(r#"{{"require_modalities":["{name}"]}}"#);
5167 let parsed: CapabilityFilterJson = serde_json::from_str(&json).unwrap();
5168 assert!(
5169 capability_filter_from_json(parsed).is_ok(),
5170 "documented modality {name:?} must convert",
5171 );
5172 }
5173 }
5174
5175 #[test]
5191 fn gpu_info_from_json_preserves_full_u32_fp16_tflops() {
5192 for declared in [
5193 0u32,
5194 825, u16::MAX as u32, u16::MAX as u32 + 1, 16_777_217, 1_000_000_000, u32::MAX,
5200 ] {
5201 let g = GpuJson {
5202 vendor: None,
5203 model: "test".to_string(),
5204 vram_gb: 0,
5205 compute_units: None,
5206 tensor_cores: None,
5207 fp16_tflops_x10: Some(declared),
5208 };
5209 assert_eq!(
5210 gpu_info_from_json(g).fp16_tflops_x10,
5211 declared,
5212 "fp16_tflops_x10 must survive the C boundary unchanged",
5213 );
5214 }
5215 }
5216
5217 #[test]
5219 fn gpu_info_from_json_keeps_large_fp16_values_orderable() {
5220 let make = |tf: u32| GpuJson {
5221 vendor: None,
5222 model: "test".to_string(),
5223 vram_gb: 0,
5224 compute_units: None,
5225 tensor_cores: None,
5226 fp16_tflops_x10: Some(tf),
5227 };
5228 let smaller = gpu_info_from_json(make(1_000_000_000)).fp16_tflops_x10;
5229 let larger = gpu_info_from_json(make(2_000_000_000)).fp16_tflops_x10;
5230 assert!(
5231 smaller < larger,
5232 "both values used to saturate to 65_535 and compare equal, \
5233 so a placement scorer could not rank them",
5234 );
5235 }
5236
5237 #[test]
5250 fn alloc_bytes_round_trip_across_sizes() {
5251 for size in [0usize, 1, 15, 16, 17, 32, 64, 1024, 8192] {
5252 let src: Vec<u8> = (0..size).map(|i| (i as u8).wrapping_mul(37)).collect();
5253 let mut ptr: *mut u8 = std::ptr::null_mut();
5254 let mut len: usize = 0;
5255 let rc = alloc_bytes(&src, &mut ptr as *mut _, &mut len as *mut _);
5256 assert_eq!(rc, 0);
5257 assert_eq!(len, size);
5258 if size == 0 {
5259 assert!(ptr.is_null());
5260 } else {
5261 assert!(!ptr.is_null());
5262 let observed = unsafe { std::slice::from_raw_parts(ptr, len) };
5263 assert_eq!(observed, &src[..]);
5264 }
5265 unsafe { net_free_bytes(ptr, len) };
5268 }
5269 }
5270
5271 #[test]
5272 fn net_free_bytes_null_and_zero_len_are_noops() {
5273 unsafe { net_free_bytes(std::ptr::null_mut(), 0) };
5275 unsafe { net_free_bytes(std::ptr::null_mut(), 42) };
5276 let mut sentinel: u8 = 0;
5279 unsafe { net_free_bytes(&mut sentinel as *mut u8, 0) };
5280 }
5281
5282 #[test]
5294 fn net_free_bytes_does_not_panic_on_oversized_len() {
5295 let mut sentinel: u8 = 0;
5303 let ptr = &mut sentinel as *mut u8;
5304 unsafe { net_free_bytes(ptr, usize::MAX) };
5307 assert_eq!(sentinel, 0, "sentinel must not have been written through");
5310 }
5311
5312 #[test]
5321 fn net_mesh_shutdown_runs_even_with_outstanding_arc_refs() {
5322 let cfg = serde_json::json!({
5323 "bind_addr": "127.0.0.1:0",
5324 "psk_hex": "0".repeat(64),
5325 });
5326 let cfg_c = CString::new(cfg.to_string()).unwrap();
5327 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
5328 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
5329 assert_eq!(rc, 0, "net_mesh_new failed: {rc}");
5330 assert!(!out.is_null());
5331
5332 let inner_clone = {
5335 let h = unsafe { &*out };
5336 Arc::clone(&h.inner)
5337 };
5338 assert!(Arc::strong_count(&inner_clone) >= 2);
5339 assert!(!inner_clone.is_shutdown());
5340
5341 let rc = unsafe { net_mesh_shutdown(out) };
5342 assert_eq!(rc, 0, "net_mesh_shutdown returned {rc}");
5343 assert!(
5344 inner_clone.is_shutdown(),
5345 "shutdown flag must be set even when extra Arc refs are outstanding"
5346 );
5347
5348 drop(inner_clone);
5349 unsafe { net_mesh_free(out) };
5353 }
5354
5355 #[test]
5364 fn net_mesh_new_records_identity_provenance() {
5365 let cfg = serde_json::json!({
5367 "bind_addr": "127.0.0.1:0",
5368 "psk_hex": "0".repeat(64),
5369 "identity_seed_hex": "7a".repeat(32),
5370 });
5371 let cfg_c = CString::new(cfg.to_string()).unwrap();
5372 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
5373 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) }, 0);
5374 assert!(
5375 unsafe { &*out }.inner.has_configured_identity(),
5376 "a caller-supplied identity_seed_hex must set configured_identity"
5377 );
5378 unsafe { net_mesh_free(out) };
5379
5380 let cfg = serde_json::json!({
5382 "bind_addr": "127.0.0.1:0",
5383 "psk_hex": "0".repeat(64),
5384 });
5385 let cfg_c = CString::new(cfg.to_string()).unwrap();
5386 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
5387 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) }, 0);
5388 assert!(
5389 !unsafe { &*out }.inner.has_configured_identity(),
5390 "a generated ephemeral fallback must leave configured_identity false"
5391 );
5392 unsafe { net_mesh_free(out) };
5393 }
5394
5395 #[test]
5407 fn handles_match_rejects_stream_node_mismatch() {
5408 fn make_node_handle() -> *mut MeshNodeHandle {
5409 let cfg = serde_json::json!({
5410 "bind_addr": "127.0.0.1:0",
5411 "psk_hex": "0".repeat(64),
5412 });
5413 let cfg_c = CString::new(cfg.to_string()).unwrap();
5414 let mut out: *mut MeshNodeHandle = std::ptr::null_mut();
5415 let rc = unsafe { net_mesh_new(cfg_c.as_ptr(), &mut out) };
5416 assert_eq!(rc, 0);
5417 assert!(!out.is_null());
5418 out
5419 }
5420
5421 let nh_a = make_node_handle();
5422 let nh_b = make_node_handle();
5423
5424 let sh_a = {
5432 let h = unsafe { &*nh_a };
5433 let node_clone: Arc<MeshNode> = Arc::clone(&h.inner);
5434 MeshStreamHandle {
5435 stream: ManuallyDrop::new(CoreStream {
5436 peer_node_id: 0xDEAD,
5437 stream_id: 1,
5438 epoch: 0,
5439 config: StreamConfig::new(),
5440 }),
5441 _node: ManuallyDrop::new(node_clone),
5442 guard: HandleGuard::new(),
5443 }
5444 };
5445
5446 assert!(
5448 handles_match(&sh_a, unsafe { &*nh_a }),
5449 "stream from node_a + node_a handle must match"
5450 );
5451 assert!(
5453 !handles_match(&sh_a, unsafe { &*nh_b }),
5454 "stream from node_a + node_b handle must be rejected (#19)"
5455 );
5456
5457 unsafe {
5466 let mut sh_a = sh_a;
5467 let _ = ManuallyDrop::take(&mut sh_a.stream);
5468 let _ = ManuallyDrop::take(&mut sh_a._node);
5469 }
5470 unsafe { net_mesh_free(nh_a) };
5471 unsafe { net_mesh_free(nh_b) };
5472 }
5473
5474 #[test]
5492 fn close_stream_after_free_reports_shutting_down() {
5493 let cfg = serde_json::json!({
5494 "bind_addr": "127.0.0.1:0",
5495 "psk_hex": "0".repeat(64),
5496 });
5497 let cfg_c = CString::new(cfg.to_string()).unwrap();
5498 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
5499 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
5500
5501 let sh = Box::into_raw(Box::new(MeshStreamHandle {
5505 stream: ManuallyDrop::new(CoreStream {
5506 peer_node_id: 0xDEAD,
5507 stream_id: 7,
5508 epoch: 0,
5509 config: StreamConfig::new(),
5510 }),
5511 _node: ManuallyDrop::new(Arc::clone(&unsafe { &*nh }.inner)),
5512 guard: HandleGuard::new(),
5513 }));
5514
5515 assert_eq!(unsafe { net_mesh_close_stream(sh) }, 0);
5518
5519 assert_eq!(
5523 unsafe { net_mesh_close_stream(sh) },
5524 c_int::from(NetError::ShuttingDown),
5525 "a close after free must come from the guard, not from a \
5526 field read that happens to survive",
5527 );
5528
5529 unsafe { net_mesh_stream_free(sh) };
5531
5532 unsafe { net_mesh_free(nh) };
5533 }
5534
5535 #[test]
5542 fn net_mesh_free_is_idempotent() {
5543 let cfg = serde_json::json!({
5544 "bind_addr": "127.0.0.1:0",
5545 "psk_hex": "0".repeat(64),
5546 });
5547 let cfg_c = CString::new(cfg.to_string()).unwrap();
5548 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
5549 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
5550 assert!(!nh.is_null());
5551
5552 unsafe { net_mesh_free(nh) };
5553 unsafe { net_mesh_free(nh) };
5557 }
5558
5559 #[test]
5563 fn net_identity_free_is_idempotent() {
5564 let mut h: *mut IdentityHandle = std::ptr::null_mut();
5565 assert_eq!(unsafe { net_identity_generate(&mut h) }, 0);
5566 assert!(!h.is_null());
5567
5568 unsafe { net_identity_free(h) };
5569 unsafe { net_identity_free(h) };
5571 }
5572
5573 #[test]
5585 fn net_mesh_free_waits_for_inflight_op() {
5586 use std::sync::atomic::{AtomicBool, Ordering};
5587 use std::time::{Duration, Instant};
5588
5589 let cfg = serde_json::json!({
5590 "bind_addr": "127.0.0.1:0",
5591 "psk_hex": "0".repeat(64),
5592 });
5593 let cfg_c = CString::new(cfg.to_string()).unwrap();
5594 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
5595 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
5596 assert!(!nh.is_null());
5597
5598 let nh_addr = nh as usize;
5601 let started = Arc::new(AtomicBool::new(false));
5602 let release = Arc::new(AtomicBool::new(false));
5603 let started_w = started.clone();
5604 let release_w = release.clone();
5605
5606 let worker = std::thread::spawn(move || {
5607 let h = unsafe { &*(nh_addr as *mut MeshNodeHandle) };
5608 let op = h.guard.try_enter().expect("entry must succeed pre-free");
5612 started_w.store(true, Ordering::SeqCst);
5613 while !release_w.load(Ordering::SeqCst) {
5614 std::thread::sleep(Duration::from_millis(1));
5615 }
5616 drop(op);
5617 });
5618
5619 while !started.load(Ordering::SeqCst) {
5621 std::thread::yield_now();
5622 }
5623
5624 let release_clone = release.clone();
5627 std::thread::spawn(move || {
5628 std::thread::sleep(Duration::from_millis(50));
5629 release_clone.store(true, Ordering::SeqCst);
5630 });
5631
5632 let t0 = Instant::now();
5634 unsafe { net_mesh_free(nh) };
5635 let elapsed = t0.elapsed();
5636 assert!(
5637 elapsed >= Duration::from_millis(40),
5638 "net_mesh_free returned in {:?} — pre-fix it would have proceeded \
5639 immediately and the worker's subsequent op would UAF",
5640 elapsed,
5641 );
5642 worker.join().unwrap();
5643 }
5644
5645 #[test]
5652 fn net_mesh_stream_stats_returns_shutting_down_after_free() {
5653 let cfg = serde_json::json!({
5654 "bind_addr": "127.0.0.1:0",
5655 "psk_hex": "0".repeat(64),
5656 });
5657 let cfg_c = CString::new(cfg.to_string()).unwrap();
5658 let mut nh: *mut MeshNodeHandle = std::ptr::null_mut();
5659 assert_eq!(unsafe { net_mesh_new(cfg_c.as_ptr(), &mut nh) }, 0);
5660 assert!(!nh.is_null());
5661
5662 unsafe { net_mesh_free(nh) };
5665
5666 let mut out_json: *mut c_char = std::ptr::null_mut();
5667 let mut out_len: usize = 0;
5668 let rc = unsafe { net_mesh_stream_stats(nh, 0xDEAD, 1, &mut out_json, &mut out_len) };
5669 assert_eq!(
5670 rc,
5671 NetError::ShuttingDown as c_int,
5672 "post-free stream_stats must surface ShuttingDown (got {rc})",
5673 );
5674 assert!(
5675 out_json.is_null(),
5676 "no payload may be written after the guard fires",
5677 );
5678 }
5679
5680 #[test]
5685 fn net_identity_issue_token_returns_shutting_down_after_free() {
5686 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
5687 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
5688 assert!(!signer.is_null());
5689 unsafe { net_identity_free(signer) };
5690
5691 let subject = [0u8; 32];
5694 let scope = CString::new("[\"publish\"]").unwrap();
5695 let channel = CString::new("test-channel").unwrap();
5696 let mut out_token: *mut u8 = std::ptr::null_mut();
5697 let mut out_token_len: usize = 0;
5698 let rc = unsafe {
5699 net_identity_issue_token(
5700 signer,
5701 subject.as_ptr(),
5702 subject.len(),
5703 scope.as_ptr(),
5704 channel.as_ptr(),
5705 60,
5706 0,
5707 &mut out_token,
5708 &mut out_token_len,
5709 )
5710 };
5711 assert_eq!(
5712 rc,
5713 NetError::ShuttingDown as c_int,
5714 "post-free issue_token must surface ShuttingDown (got {rc})",
5715 );
5716 assert!(out_token.is_null(), "no token bytes may be allocated");
5717 }
5718
5719 #[test]
5725 fn net_delegate_token_returns_shutting_down_after_free() {
5726 let mut signer: *mut IdentityHandle = std::ptr::null_mut();
5727 assert_eq!(unsafe { net_identity_generate(&mut signer) }, 0);
5728 assert!(!signer.is_null());
5729
5730 let subject = [0u8; 32];
5732 let scope = CString::new("[\"publish\",\"delegate\"]").unwrap();
5733 let channel = CString::new("test-channel").unwrap();
5734 let mut parent_bytes: *mut u8 = std::ptr::null_mut();
5735 let mut parent_len: usize = 0;
5736 assert_eq!(
5737 unsafe {
5738 net_identity_issue_token(
5739 signer,
5740 subject.as_ptr(),
5741 subject.len(),
5742 scope.as_ptr(),
5743 channel.as_ptr(),
5744 60,
5745 1,
5746 &mut parent_bytes,
5747 &mut parent_len,
5748 )
5749 },
5750 0,
5751 );
5752 assert!(!parent_bytes.is_null());
5753
5754 unsafe { net_identity_free(signer) };
5756
5757 let new_subject = [1u8; 32];
5758 let restricted = CString::new("[\"publish\"]").unwrap();
5759 let mut child_bytes: *mut u8 = std::ptr::null_mut();
5760 let mut child_len: usize = 0;
5761 let rc = unsafe {
5762 net_delegate_token(
5763 signer,
5764 parent_bytes,
5765 parent_len,
5766 new_subject.as_ptr(),
5767 new_subject.len(),
5768 restricted.as_ptr(),
5769 &mut child_bytes,
5770 &mut child_len,
5771 )
5772 };
5773 assert_eq!(
5774 rc,
5775 NetError::ShuttingDown as c_int,
5776 "post-free delegate_token must surface ShuttingDown (got {rc})",
5777 );
5778 assert!(child_bytes.is_null(), "no child token may be allocated");
5779
5780 unsafe { net_free_bytes(parent_bytes, parent_len) };
5782 }
5783
5784 #[test]
5785 fn hardware_from_json_saturates_overflow_cpu_fields() {
5786 let h = HardwareJson {
5789 cpu_cores: Some(70_000),
5790 cpu_threads: Some(200_000),
5791 memory_gb: None,
5792 gpu: None,
5793 additional_gpus: Vec::new(),
5794 storage_gb: None,
5795 network_gbps: None,
5796 accelerators: Vec::new(),
5797 };
5798 let hw = hardware_from_json(h);
5799 assert_eq!(hw.cpu_cores, u16::MAX);
5800 assert_eq!(hw.cpu_threads, u16::MAX);
5801 }
5802
5803 #[test]
5810 fn token_entry_points_reject_oversize_len() {
5811 let invalid_json: c_int = NetError::InvalidJson.into();
5812 let mut sentinel: u8 = 0;
5813 let token = &mut sentinel as *mut u8 as *const u8;
5814
5815 let mut out_json: *mut c_char = std::ptr::null_mut();
5816 let mut out_len: usize = 0;
5817 assert_eq!(
5818 unsafe { net_parse_token(token, usize::MAX, &mut out_json, &mut out_len) },
5819 invalid_json,
5820 );
5821 assert!(out_json.is_null());
5822
5823 let mut out_ok: c_int = -42;
5824 assert_eq!(
5825 unsafe { net_verify_token(token, usize::MAX, &mut out_ok) },
5826 invalid_json,
5827 );
5828
5829 let mut out_expired: c_int = -42;
5830 assert_eq!(
5831 unsafe { net_token_is_expired(token, usize::MAX, &mut out_expired) },
5832 invalid_json,
5833 );
5834
5835 assert_eq!(
5836 sentinel, 0,
5837 "sentinel must not be touched: the length guard fires before any deref"
5838 );
5839 }
5840}
5841
5842#[cfg(all(test, not(feature = "nat-traversal")))]
5843mod nat_traversal_stub_tests {
5844 use super::*;
5861 use std::ptr;
5862
5863 #[test]
5864 fn nat_type_stub_returns_unsupported() {
5865 let mut out_str: *mut c_char = ptr::null_mut();
5866 let mut out_len: usize = 0;
5867 let code = unsafe { net_mesh_nat_type(ptr::null_mut(), &mut out_str, &mut out_len) };
5870 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5871 }
5872
5873 #[test]
5874 fn reflex_addr_stub_returns_unsupported() {
5875 let mut out_str: *mut c_char = ptr::null_mut();
5876 let mut out_len: usize = 0;
5877 let code = unsafe { net_mesh_reflex_addr(ptr::null_mut(), &mut out_str, &mut out_len) };
5879 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5880 }
5881
5882 #[test]
5883 fn peer_nat_type_stub_returns_unsupported() {
5884 let mut out_str: *mut c_char = ptr::null_mut();
5885 let mut out_len: usize = 0;
5886 let code =
5888 unsafe { net_mesh_peer_nat_type(ptr::null_mut(), 0, &mut out_str, &mut out_len) };
5889 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5890 }
5891
5892 #[test]
5893 fn probe_reflex_stub_returns_unsupported() {
5894 let mut out_str: *mut c_char = ptr::null_mut();
5895 let mut out_len: usize = 0;
5896 let code = unsafe { net_mesh_probe_reflex(ptr::null_mut(), 0, &mut out_str, &mut out_len) };
5898 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5899 }
5900
5901 #[test]
5902 fn reclassify_nat_stub_returns_unsupported() {
5903 let code = unsafe { net_mesh_reclassify_nat(ptr::null_mut()) };
5905 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5906 }
5907
5908 #[test]
5909 fn traversal_stats_stub_returns_unsupported() {
5910 let mut a: u64 = 0;
5911 let mut b: u64 = 0;
5912 let mut c: u64 = 0;
5913 let code = unsafe { net_mesh_traversal_stats(ptr::null_mut(), &mut a, &mut b, &mut c) };
5915 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5916 }
5917
5918 #[test]
5919 fn connect_direct_stub_returns_unsupported() {
5920 let code = unsafe { net_mesh_connect_direct(ptr::null_mut(), 0, ptr::null(), 0) };
5922 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5923 }
5924
5925 #[test]
5926 fn connect_direct_auto_stub_returns_unsupported() {
5927 let code = unsafe { net_mesh_connect_direct_auto(ptr::null_mut(), 0, ptr::null()) };
5929 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5930 }
5931
5932 #[test]
5933 fn traversal_stats_v2_stub_returns_unsupported() {
5934 let code = unsafe { net_mesh_traversal_stats_v2(ptr::null_mut(), ptr::null_mut()) };
5936 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5937 }
5938
5939 #[test]
5940 fn set_reflex_override_stub_returns_unsupported() {
5941 let code = unsafe { net_mesh_set_reflex_override(ptr::null_mut(), ptr::null()) };
5943 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5944 }
5945
5946 #[test]
5947 fn clear_reflex_override_stub_returns_unsupported() {
5948 let code = unsafe { net_mesh_clear_reflex_override(ptr::null_mut()) };
5950 assert_eq!(code, NET_ERR_TRAVERSAL_UNSUPPORTED);
5951 }
5952
5953 #[test]
5959 fn unsupported_code_is_stable() {
5960 assert_eq!(NET_ERR_TRAVERSAL_UNSUPPORTED, -137);
5961 }
5962
5963 #[test]
5967 fn capability_set_from_go_marshal_preserves_gpu_vendor() {
5968 let json = r#"{"hardware":{"cpu_cores":16,"memory_gb":64,"gpu":{"vendor":"nvidia","model":"h100","vram_gb":80}},"tags":["gpu"]}"#;
5969 let parsed: CapabilitySetJson = serde_json::from_str(json).expect("JSON should parse");
5970 let caps = capability_set_from_json(parsed).expect("valid capability set");
5971 let views = caps.views();
5975 assert_eq!(
5976 views.hardware().gpu_vendor(),
5977 Some(super::GpuVendor::Nvidia),
5978 "vendor lost in conversion"
5979 );
5980 assert_eq!(views.hardware().memory_gb, 64);
5981 assert_eq!(views.hardware().total_vram_gb(), 80);
5982 assert!(caps.has_tag("gpu"));
5983 }
5984
5985 #[test]
5994 fn collect_payloads_rejects_null_entry_with_nonzero_length() {
5995 let buf_a = b"hello".as_slice();
5996 let buf_b = b"world".as_slice();
5997 let ptrs: [*const u8; 3] = [buf_a.as_ptr(), std::ptr::null(), buf_b.as_ptr()];
5998 let lens: [usize; 3] = [buf_a.len(), 4, buf_b.len()];
5999
6000 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 3) };
6001 assert!(
6002 result.is_none(),
6003 "null entry with non-zero length must reject the whole batch"
6004 );
6005 }
6006
6007 #[test]
6008 fn collect_payloads_allows_null_entry_with_zero_length() {
6009 let buf_a = b"hello".as_slice();
6010 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), std::ptr::null()];
6011 let lens: [usize; 2] = [buf_a.len(), 0];
6012
6013 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
6014 .expect("zero-length null is treated as empty payload");
6015 assert_eq!(result.len(), 2);
6016 assert_eq!(&result[0][..], b"hello");
6017 assert!(result[1].is_empty());
6018 }
6019
6020 #[test]
6021 fn collect_payloads_happy_path() {
6022 let buf_a = b"abc".as_slice();
6023 let buf_b = b"defg".as_slice();
6024 let ptrs: [*const u8; 2] = [buf_a.as_ptr(), buf_b.as_ptr()];
6025 let lens: [usize; 2] = [buf_a.len(), buf_b.len()];
6026
6027 let result = unsafe { collect_payloads(ptrs.as_ptr(), lens.as_ptr(), 2) }
6028 .expect("non-null entries should succeed");
6029 assert_eq!(result.len(), 2);
6030 assert_eq!(&result[0][..], b"abc");
6031 assert_eq!(&result[1][..], b"defg");
6032 }
6033}
6034
6035#[cfg(all(test, feature = "net"))]
6036mod subnet_authority_config_tests {
6037 use super::*;
6050
6051 const AUTHORITY: &str = "d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7d7";
6052 const ROOT: &str = "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1";
6053
6054 fn parse(json: &str) -> Result<MeshNewConfig, serde_json::Error> {
6055 serde_json::from_str(json)
6056 }
6057
6058 #[test]
6061 fn trust_anchor_fields_parse_and_convert() {
6062 let cfg = parse(&format!(
6063 r#"{{"bind_addr":"127.0.0.1:0","psk_hex":"{psk}",
6064 "subnet_authorities":[{{"authority_hex":"{AUTHORITY}",
6065 "root_hexes":["{ROOT}"],"maximum_grant_lifetime_secs":604800}}],
6066 "subnet_attachment":[3,9],
6067 "subnet_control_channel":"subnet.control"}}"#,
6068 psk = "42".repeat(32),
6069 ))
6070 .expect("config parses");
6071
6072 let authorities = cfg.subnet_authorities.expect("authorities present");
6073 assert_eq!(authorities.len(), 1);
6074 let core = authorities[0].to_core().expect("converts");
6075 assert_eq!(core.maximum_grant_lifetime_secs, 604_800);
6076 assert_eq!(core.roots.len(), 1);
6077 assert!(
6078 crate::adapter::net::subnet::provision::validate_subnet_authorities(&[core]).is_ok(),
6079 "a well-formed anchor must validate",
6080 );
6081
6082 assert_eq!(cfg.subnet_attachment.as_deref(), Some(&[3u8, 9][..]));
6083 assert_eq!(
6084 cfg.subnet_control_channel.as_deref(),
6085 Some("subnet.control")
6086 );
6087 }
6088
6089 #[test]
6093 fn trust_anchor_fields_are_optional() {
6094 let cfg = parse(&format!(
6095 r#"{{"bind_addr":"127.0.0.1:0","psk_hex":"{}"}}"#,
6096 "42".repeat(32)
6097 ))
6098 .expect("config parses without any subnet authority field");
6099 assert!(cfg.subnet_authorities.is_none());
6100 assert!(cfg.subnet_attachment.is_none());
6101 assert!(cfg.subnet_control_channel.is_none());
6102 }
6103
6104 #[test]
6109 fn configuration_mistakes_are_refused() {
6110 use crate::adapter::net::subnet::provision::{
6111 dto::SubnetAuthorityConfigDto, validate_subnet_authorities,
6112 };
6113
6114 let good = SubnetAuthorityConfigDto {
6115 authority_hex: AUTHORITY.to_string(),
6116 root_hexes: vec![ROOT.to_string()],
6117 maximum_grant_lifetime_secs: 604_800,
6118 };
6119
6120 let bad_hex = SubnetAuthorityConfigDto {
6122 authority_hex: "not-hex".to_string(),
6123 ..good.clone()
6124 };
6125 assert!(bad_hex.to_core().is_err(), "a malformed id must be refused");
6126
6127 let empty_roots = SubnetAuthorityConfigDto {
6129 root_hexes: Vec::new(),
6130 ..good.clone()
6131 };
6132 assert!(validate_subnet_authorities(&[empty_roots.to_core().expect("converts")]).is_err());
6133
6134 let zero_life = SubnetAuthorityConfigDto {
6136 maximum_grant_lifetime_secs: 0,
6137 ..good.clone()
6138 };
6139 assert!(validate_subnet_authorities(&[zero_life.to_core().expect("converts")]).is_err());
6140
6141 let one = good.to_core().expect("converts");
6143 let two = good.to_core().expect("converts");
6144 assert!(validate_subnet_authorities(&[one, two]).is_err());
6145 }
6146
6147 #[test]
6161 fn the_go_bindings_emitted_config_deserializes() {
6162 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]}"#;
6163
6164 let cfg = parse(GO_EMITTED).expect("the Go binding's own JSON must deserialize");
6165 assert_eq!(cfg.subnet_attachment.as_deref(), Some(&[3u8][..]));
6166 let exports = cfg.subnet_exports.expect("exports present");
6167 assert_eq!(exports.len(), 1);
6168 let export = exports[0].to_core().expect("export converts");
6169 assert_eq!(export.name, "factory-export");
6170 let authorities = cfg.subnet_authorities.expect("authorities present");
6171 assert!(authorities[0].to_core().is_ok());
6172 }
6173
6174 #[test]
6177 fn a_base64_level_array_is_refused() {
6178 let base64_attachment =
6179 r#"{"bind_addr":"127.0.0.1:0","psk_hex":"42","subnet_attachment":"Awk="}"#;
6180 assert!(
6181 parse(base64_attachment).is_err(),
6182 "a base64 attachment must be refused, not silently accepted",
6183 );
6184 }
6185
6186 #[test]
6189 fn an_over_deep_attachment_is_refused() {
6190 use crate::adapter::net::subnet::provision::dto::SubnetPathDto;
6191 assert!(SubnetPathDto {
6192 levels: vec![1, 2, 3, 4, 5]
6193 }
6194 .to_core()
6195 .is_err());
6196 assert!(SubnetPathDto {
6197 levels: vec![1, 2, 3, 4]
6198 }
6199 .to_core()
6200 .is_ok());
6201 assert!(SubnetPathDto { levels: vec![] }.to_core().is_ok());
6202 }
6203}
6204
6205#[cfg(all(test, feature = "net"))]
6206mod named_export_construction_tests {
6207 use super::*;
6216 use crate::adapter::net::identity::EntityKeypair;
6217 use crate::adapter::net::subnet::provision::{NamedSubnetExport, SubnetExportAccess};
6218 use crate::adapter::net::subnet::{SubnetRef, TopologySubnetId};
6219
6220 fn export(name: &str) -> NamedSubnetExport {
6221 NamedSubnetExport {
6222 name: name.to_string(),
6223 access: SubnetExportAccess::Granted,
6224 subnet: SubnetRef {
6225 authority: EntityKeypair::from_bytes([0x11; 32]).entity_id().clone(),
6226 path: TopologySubnetId::new(&[3, 9]),
6227 },
6228 topology_epoch: 0,
6229 }
6230 }
6231
6232 async fn build(exports: Vec<NamedSubnetExport>) -> Result<MeshNode, AdapterError> {
6233 let mut cfg = MeshNodeConfig::new("127.0.0.1:0".parse().expect("addr"), [0u8; 32]);
6234 for e in exports {
6235 cfg = cfg.with_subnet_export(e);
6236 }
6237 MeshNode::new(EntityKeypair::generate(), cfg).await
6238 }
6239
6240 #[tokio::test]
6242 async fn configured_exports_are_resolvable_from_the_node() {
6243 let node = build(vec![export("factory-export"), export("lab-export")])
6244 .await
6245 .expect("distinct names construct");
6246 let map = node.subnet_exports();
6247 assert!(map.resolve("factory-export").is_some());
6248 assert!(map.resolve("lab-export").is_some());
6249 assert!(
6250 map.resolve("no-such-export").is_none(),
6251 "an unconfigured name must not resolve",
6252 );
6253 }
6254
6255 #[tokio::test]
6258 async fn a_duplicate_export_name_refuses_construction() {
6259 let Err(err) = build(vec![export("dup"), export("dup")]).await else {
6260 panic!("a duplicate label must refuse construction");
6261 };
6262 assert!(
6263 err.to_string().contains("duplicate_export_name"),
6264 "expected the stable kind in the refusal, got {err}",
6265 );
6266 }
6267
6268 #[tokio::test]
6270 async fn an_empty_export_name_refuses_construction() {
6271 let Err(err) = build(vec![export("")]).await else {
6272 panic!("an empty label must refuse construction");
6273 };
6274 assert!(
6275 err.to_string().contains("empty_export_name"),
6276 "expected the stable kind in the refusal, got {err}",
6277 );
6278 }
6279
6280 #[tokio::test]
6283 async fn no_exports_is_valid_and_resolves_nothing() {
6284 let node = build(Vec::new()).await.expect("no exports constructs");
6285 assert!(node.subnet_exports().is_empty());
6286 assert!(node.subnet_exports().resolve("anything").is_none());
6287 }
6288}