1use std::collections::btree_map::Entry;
11use std::collections::BTreeMap;
12use std::time::Duration;
13
14use async_trait::async_trait;
15use rings_core::dht::Did;
16use rings_core::ecc::PublicKey;
17use rings_core::ecc::VerificationPublicKey;
18use rings_core::error::Error as CoreError;
19use rings_core::error::Result as CoreResult;
20use rings_core::message::Decoder;
21use rings_core::message::Encoded;
22use rings_core::message::Encoder;
23use rings_core::message::MessageVerification;
24use rings_core::session::SessionSk;
25use rings_core::utils::get_epoch_ms;
26use serde::Deserialize;
27use serde::Serialize;
28
29use crate::descriptor::decode_descriptor;
30use crate::descriptor::encode_descriptor;
31use crate::descriptor::sign_descriptor_body;
32use crate::descriptor::SignedDescriptor;
33use crate::descriptor::SignedDescriptorBody;
34use crate::error::Error;
35use crate::error::Result;
36use crate::online::OnlineNodeType;
37use crate::registration::DhtRegistrationPublisher;
38use crate::registration::RegistrationContext;
39use crate::registration::RegistrationTask;
40
41pub mod circuit;
42pub(crate) mod directory;
43pub(crate) mod exit_accounting;
44mod failure;
45#[cfg(rings_native)]
46mod gateway;
47#[cfg(any(rings_native, rings_browser))]
48pub mod https;
49pub mod proxy;
50pub(crate) mod replay;
51pub mod route;
52pub mod target;
53#[cfg(rings_native)]
54pub mod tcp;
55
56pub use failure::OnionExitFailure;
57pub use failure::OnionRouteError;
58#[cfg(rings_native)]
59pub use gateway::NativeOnionGatewayConnector;
60pub use route::select_onion_route;
61pub(crate) use route::select_onion_route_from_candidates_with_first_hop;
62pub use route::OnionRoute;
63pub(crate) use route::OnionRouteCandidates;
64pub use route::OnionRouteHop;
65pub use route::OnionRouteRequest;
66pub(crate) use route::SystemRouteEntropy;
67pub use route::DEFAULT_ONION_ROUTE_HOPS;
68pub use target::OnionProxyTarget;
69pub use target::OnionProxyTargetError;
70
71pub const ONION_EXITS_TOPIC: &str = "onion_exits";
73
74pub(crate) const ONION_EXIT_DESCRIPTOR_SCHEMA_VERSION: u16 = 2;
75
76pub const ONION_RELAY_CAPABILITY: &str = "onion-relay";
78
79const DEFAULT_ONION_EXIT_HEARTBEAT_INTERVAL_SECS: u64 = 30;
80const DEFAULT_ONION_EXIT_TTL_SECS: u64 = 90;
81
82pub(crate) const fn default_onion_exit_heartbeat_interval_secs() -> u64 {
84 DEFAULT_ONION_EXIT_HEARTBEAT_INTERVAL_SECS
85}
86
87pub(crate) const fn default_onion_exit_ttl_secs() -> u64 {
89 DEFAULT_ONION_EXIT_TTL_SECS
90}
91
92pub(crate) const fn default_advertise_onion_relay() -> bool {
94 false
95}
96
97pub(crate) const fn default_advertise_onion_exit() -> bool {
99 false
100}
101
102pub fn default_onion_exit_services() -> Vec<OnionExitService> {
105 vec![OnionExitService::tcp(), OnionExitService::https()]
106}
107
108pub fn https_onion_exit_services() -> Vec<OnionExitService> {
110 vec![OnionExitService::https()]
111}
112
113pub fn default_onion_exit_policy() -> OnionExitPolicy {
115 OnionExitPolicy::default()
116}
117
118pub(crate) fn validate_onion_exit_registration_timing(
120 advertise_exit: bool,
121 heartbeat_interval: Duration,
122 ttl: Duration,
123) -> Result<()> {
124 if advertise_exit && heartbeat_interval >= ttl {
125 return Err(Error::InvalidConfig(format!(
126 "onion_exit_heartbeat_interval ({heartbeat_interval:?}) must be less than onion_exit_ttl ({ttl:?}) when advertise_onion_exit is enabled"
127 )));
128 }
129 Ok(())
130}
131
132#[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
134pub enum OnionExitTransport {
135 Tcp,
137 Udp,
139 WebTransport,
141 RequestResponse,
143 Https,
148}
149
150#[derive(Clone, Debug, Deserialize, Serialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
152pub struct OnionExitService {
153 pub name: OnionServiceName,
155 pub transport: OnionExitTransport,
157}
158
159#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
161#[serde(try_from = "String", into = "String")]
162pub struct OnionServiceName(String);
163
164impl OnionServiceName {
165 pub fn parse(name: impl AsRef<str>) -> Result<Self> {
167 let name = name.as_ref();
168 let trimmed = name.trim();
169 if trimmed.is_empty() || trimmed != name {
170 return Err(Error::InvalidConfig(
171 "onion exit service name must be non-empty and trimmed".to_string(),
172 ));
173 }
174 if trimmed.len() > 64 || trimmed.chars().any(|ch| !is_service_name_char(ch)) {
175 return Err(Error::InvalidConfig(format!(
176 "invalid onion exit service name {name:?}; expected [A-Za-z0-9._-] up to 64 bytes"
177 )));
178 }
179 Ok(Self(trimmed.to_ascii_lowercase()))
180 }
181
182 pub fn https() -> Self {
184 Self::static_name("https")
185 }
186
187 pub fn tcp() -> Self {
189 Self::static_name("tcp")
190 }
191
192 fn static_name(name: &'static str) -> Self {
194 Self(name.to_string())
195 }
196
197 pub fn as_str(&self) -> &str {
199 self.0.as_str()
200 }
201
202 pub fn matches(&self, service: &str) -> bool {
204 Self::parse(service).is_ok_and(|candidate| candidate == *self)
205 }
206}
207
208impl TryFrom<String> for OnionServiceName {
209 type Error = String;
210
211 fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
212 Self::parse(&value).map_err(|error| error.to_string())
213 }
214}
215
216impl From<OnionServiceName> for String {
217 fn from(name: OnionServiceName) -> Self {
218 name.0
219 }
220}
221
222impl OnionExitService {
223 pub fn new(name: impl AsRef<str>, transport: OnionExitTransport) -> Result<Self> {
225 Ok(Self::from_name(OnionServiceName::parse(name)?, transport))
226 }
227
228 pub fn from_name(name: OnionServiceName, transport: OnionExitTransport) -> Self {
230 Self { name, transport }
231 }
232
233 pub fn https() -> Self {
235 Self::from_name(OnionServiceName::https(), OnionExitTransport::Tcp)
236 }
237
238 pub fn tcp() -> Self {
240 Self::from_name(OnionServiceName::tcp(), OnionExitTransport::Tcp)
241 }
242
243 pub fn has_name(&self, service: &str) -> bool {
245 self.name.matches(service)
246 }
247
248 pub fn matches(&self, service: &str, transport: OnionExitTransport) -> bool {
250 self.has_name(service) && self.transport == transport
251 }
252
253 pub fn matches_route_service(&self, service: &str) -> bool {
259 match Self::reserved_transport(service) {
260 Some(transport) => {
261 self.matches(service, transport) || self.matches_legacy_reserved_transport(service)
262 }
263 None => self.has_name(service),
264 }
265 }
266
267 fn matches_legacy_reserved_transport(&self, service: &str) -> bool {
268 OnionServiceName::parse(service).is_ok_and(|name| {
269 name == OnionServiceName::https()
270 && self.name == name
271 && self.transport == OnionExitTransport::Https
272 })
273 }
274
275 pub fn reserved_transport(service: &str) -> Option<OnionExitTransport> {
277 let service = OnionServiceName::parse(service).ok()?;
278 match service.as_str() {
279 "tcp" => Some(OnionExitTransport::Tcp),
280 "https" => Some(OnionExitTransport::Tcp),
281 _ => None,
282 }
283 }
284}
285
286fn is_service_name_char(ch: char) -> bool {
287 ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-')
288}
289
290#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
292pub struct OnionExitPolicy {
293 pub allowed_targets: Vec<OnionExitTarget>,
295 pub denied_targets: Vec<OnionExitTarget>,
297 pub max_circuits: u32,
299 pub max_streams_per_circuit: u32,
301 pub max_bytes_per_minute: u64,
303}
304
305#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
307#[serde(try_from = "String", into = "String")]
308pub struct OnionExitTarget(String);
309
310impl OnionExitTarget {
311 const WILDCARD_AUTHORITY: &'static str = "*:*";
312
313 pub fn parse(target: impl AsRef<str>) -> Result<Self> {
315 let raw = target.as_ref().trim();
316 if raw == "*" || raw == Self::WILDCARD_AUTHORITY {
317 return Ok(Self(Self::WILDCARD_AUTHORITY.to_string()));
318 }
319 OnionProxyTarget::parse_authority(raw)
320 .map(|target| Self(target.authority()))
321 .map_err(|error| {
322 Error::InvalidConfig(format!(
323 "invalid onion exit target {:?}; expected host:port or *:*: {error}",
324 target.as_ref()
325 ))
326 })
327 }
328
329 pub fn authority(&self) -> &str {
331 self.0.as_str()
332 }
333
334 pub fn from_proxy_target(target: &OnionProxyTarget) -> Self {
336 Self(target.authority())
337 }
338
339 fn matches_target(&self, target: &Self) -> bool {
340 self.0 == Self::WILDCARD_AUTHORITY || self == target
341 }
342}
343
344impl TryFrom<String> for OnionExitTarget {
345 type Error = String;
346
347 fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
348 Self::parse(&value).map_err(|error| error.to_string())
349 }
350}
351
352impl From<OnionExitTarget> for String {
353 fn from(target: OnionExitTarget) -> Self {
354 target.0
355 }
356}
357
358impl OnionExitPolicy {
359 pub fn from_target_strings(
361 allowed_targets: Vec<String>,
362 denied_targets: Vec<String>,
363 ) -> Result<Self> {
364 Ok(Self {
365 allowed_targets: parse_exit_targets(allowed_targets)?,
366 denied_targets: parse_exit_targets(denied_targets)?,
367 ..Self::default()
368 })
369 }
370
371 pub fn is_closed(&self) -> bool {
373 self.allowed_targets.is_empty()
374 }
375
376 pub fn validate_targets(&self) -> Result<()> {
378 if self.is_closed() {
379 return Err(Error::InvalidConfig(
380 "advertise_onion_exit requires at least one valid onion_exit_policy allowed target"
381 .to_string(),
382 ));
383 }
384 Ok(())
385 }
386
387 pub fn allows_target(&self, target: &OnionExitTarget) -> bool {
389 if self.is_closed() {
390 return false;
391 }
392 if self.denies(target) {
393 return false;
394 }
395 self.allows(target)
396 }
397
398 fn allows(&self, target: &OnionExitTarget) -> bool {
399 self.allowed_targets
400 .iter()
401 .any(|allowed| allowed.matches_target(target))
402 }
403
404 fn denies(&self, target: &OnionExitTarget) -> bool {
405 self.denied_targets
406 .iter()
407 .any(|denied| denied.matches_target(target))
408 }
409}
410
411fn parse_exit_targets(targets: Vec<String>) -> Result<Vec<OnionExitTarget>> {
412 targets
413 .into_iter()
414 .map(OnionExitTarget::parse)
415 .collect::<Result<Vec<_>>>()
416}
417
418#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
420pub struct OnionExitDescriptorBody {
421 pub did: Did,
423 pub public_key: VerificationPublicKey,
425 pub session_public_key: PublicKey<33>,
427 pub node_type: OnlineNodeType,
429 pub network_id: u32,
431 pub service: OnionExitService,
433 pub policy: OnionExitPolicy,
435 pub started_at_ms: u128,
437 pub heartbeat_at_ms: u128,
439 pub expires_at_ms: u128,
441 pub version: String,
443}
444
445impl OnionExitDescriptorBody {
446 fn body_ref(&self) -> OnionExitDescriptorBodyRef<'_> {
447 OnionExitDescriptorBodyRef {
448 schema_version: ONION_EXIT_DESCRIPTOR_SCHEMA_VERSION,
449 did: self.did,
450 public_key: &self.public_key,
451 session_public_key: &self.session_public_key,
452 node_type: &self.node_type,
453 network_id: self.network_id,
454 service: &self.service,
455 policy: &self.policy,
456 started_at_ms: self.started_at_ms,
457 heartbeat_at_ms: self.heartbeat_at_ms,
458 expires_at_ms: self.expires_at_ms,
459 version: self.version.as_str(),
460 }
461 }
462
463 fn signing_data(&self) -> CoreResult<Vec<u8>> {
464 self.body_ref().signing_data()
465 }
466}
467
468impl SignedDescriptorBody for OnionExitDescriptorBody {
469 type Descriptor = OnionExitDescriptor;
470
471 fn body_did(&self) -> Did {
472 self.did
473 }
474
475 fn body_public_key(&self) -> &VerificationPublicKey {
476 &self.public_key
477 }
478
479 fn body_signing_data(&self) -> CoreResult<Vec<u8>> {
480 self.signing_data()
481 }
482
483 fn into_signed_descriptor(self, signature: MessageVerification) -> Self::Descriptor {
484 OnionExitDescriptor {
485 schema_version: ONION_EXIT_DESCRIPTOR_SCHEMA_VERSION,
486 did: self.did,
487 public_key: self.public_key,
488 session_public_key: self.session_public_key,
489 node_type: self.node_type,
490 network_id: self.network_id,
491 service: self.service,
492 policy: self.policy,
493 started_at_ms: self.started_at_ms,
494 heartbeat_at_ms: self.heartbeat_at_ms,
495 expires_at_ms: self.expires_at_ms,
496 version: self.version,
497 signature,
498 }
499 }
500}
501
502#[derive(Serialize)]
503struct OnionExitDescriptorBodyRef<'a> {
504 schema_version: u16,
505 did: Did,
506 public_key: &'a VerificationPublicKey,
507 session_public_key: &'a PublicKey<33>,
508 node_type: &'a OnlineNodeType,
509 network_id: u32,
510 service: &'a OnionExitService,
511 policy: &'a OnionExitPolicy,
512 started_at_ms: u128,
513 heartbeat_at_ms: u128,
514 expires_at_ms: u128,
515 version: &'a str,
516}
517
518impl OnionExitDescriptorBodyRef<'_> {
519 fn signing_data(&self) -> CoreResult<Vec<u8>> {
520 rings_codec::serialize(self).map_err(CoreError::CodecSerialize)
521 }
522}
523
524#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
526pub struct OnionExitDescriptor {
527 pub schema_version: u16,
529 pub did: Did,
531 pub public_key: VerificationPublicKey,
533 pub session_public_key: PublicKey<33>,
535 pub node_type: OnlineNodeType,
537 pub network_id: u32,
539 pub service: OnionExitService,
541 pub policy: OnionExitPolicy,
543 pub started_at_ms: u128,
545 pub heartbeat_at_ms: u128,
547 pub expires_at_ms: u128,
549 pub version: String,
551 pub signature: MessageVerification,
553}
554
555impl OnionExitDescriptor {
556 pub fn new_signed(body: OnionExitDescriptorBody, session_sk: &SessionSk) -> CoreResult<Self> {
558 sign_descriptor_body(
559 body,
560 session_sk,
561 "onion exit descriptor DID/public key/session mismatch",
562 )
563 }
564
565 fn body_ref(&self) -> OnionExitDescriptorBodyRef<'_> {
566 let Self {
567 schema_version,
568 did,
569 public_key,
570 session_public_key,
571 node_type,
572 network_id,
573 service,
574 policy,
575 started_at_ms,
576 heartbeat_at_ms,
577 expires_at_ms,
578 version,
579 signature: _,
580 } = self;
581
582 OnionExitDescriptorBodyRef {
583 schema_version: *schema_version,
584 did: *did,
585 public_key,
586 session_public_key,
587 node_type,
588 network_id: *network_id,
589 service,
590 policy,
591 started_at_ms: *started_at_ms,
592 heartbeat_at_ms: *heartbeat_at_ms,
593 expires_at_ms: *expires_at_ms,
594 version: version.as_str(),
595 }
596 }
597
598 fn signing_data(&self) -> CoreResult<Vec<u8>> {
599 self.body_ref().signing_data()
600 }
601
602 pub const fn has_supported_schema(&self) -> bool {
604 self.schema_version == ONION_EXIT_DESCRIPTOR_SCHEMA_VERSION
605 }
606
607 pub const fn matches_network(&self, network_id: u32) -> bool {
609 self.network_id == network_id
610 }
611
612 pub fn advertises_service_name(&self, service: &str) -> bool {
614 self.service.has_name(service)
615 }
616
617 pub fn offers_service(&self, service: &str) -> bool {
619 self.service.matches_route_service(service)
620 }
621
622 pub fn offers_service_transport(&self, service: &str, transport: OnionExitTransport) -> bool {
624 self.service.matches(service, transport)
625 || (transport == OnionExitTransport::Tcp
626 && self.service.matches_legacy_reserved_transport(service))
627 }
628
629 pub fn verify_signature(&self) -> bool {
631 self.has_supported_schema() && self.descriptor_verify_signature()
632 }
633
634 pub fn is_expired_at(&self, now_ms: u128) -> bool {
636 self.descriptor_is_expired_at(now_ms)
637 }
638
639 pub fn is_live_at(&self, now_ms: u128) -> bool {
641 self.verify_signature() && !self.is_expired_at(now_ms)
642 }
643
644 pub fn latest_valid_by_service_did(
650 descriptors: impl IntoIterator<Item = Self>,
651 now_ms: u128,
652 include_expired: bool,
653 ) -> Vec<Self> {
654 let mut latest = BTreeMap::<(Did, OnionExitService), Self>::new();
655 for descriptor in descriptors {
656 if include_expired {
657 if !descriptor.verify_signature() {
658 continue;
659 }
660 } else if !descriptor.is_live_at(now_ms) {
661 continue;
662 }
663 let key = (descriptor.did, descriptor.service.clone());
664 match latest.entry(key) {
665 Entry::Occupied(mut entry) => {
666 if descriptor.heartbeat_at_ms > entry.get().heartbeat_at_ms {
667 entry.insert(descriptor);
668 }
669 }
670 Entry::Vacant(entry) => {
671 entry.insert(descriptor);
672 }
673 }
674 }
675 latest.into_values().collect()
676 }
677}
678
679impl SignedDescriptor for OnionExitDescriptor {
680 fn descriptor_did(&self) -> Did {
681 self.did
682 }
683
684 fn descriptor_public_key(&self) -> &VerificationPublicKey {
685 &self.public_key
686 }
687
688 fn descriptor_signature(&self) -> &MessageVerification {
689 &self.signature
690 }
691
692 fn descriptor_heartbeat_at_ms(&self) -> u128 {
693 self.heartbeat_at_ms
694 }
695
696 fn descriptor_expires_at_ms(&self) -> u128 {
697 self.expires_at_ms
698 }
699
700 fn descriptor_signing_data(&self) -> CoreResult<Vec<u8>> {
701 self.signing_data()
702 }
703}
704
705impl Encoder for OnionExitDescriptor {
706 fn encode(&self) -> CoreResult<Encoded> {
707 encode_descriptor(self)
708 }
709}
710
711impl Decoder for OnionExitDescriptor {
712 fn from_encoded(encoded: &Encoded) -> CoreResult<Self> {
713 let descriptor: Self = decode_descriptor(encoded)?;
714 if descriptor.has_supported_schema() {
715 Ok(descriptor)
716 } else {
717 Err(CoreError::Decode)
718 }
719 }
720}
721
722#[derive(Clone, Debug, Default, Eq, PartialEq)]
724pub struct OnionExitDescriptorDecodeReport {
725 pub descriptors: Vec<OnionExitDescriptor>,
727 pub rejected_values: usize,
729}
730
731impl OnionExitDescriptorDecodeReport {
732 pub const fn has_rejections(&self) -> bool {
734 self.rejected_values > 0
735 }
736}
737
738#[derive(Clone, Debug)]
740pub struct OnionExitRegistration {
741 heartbeat_interval: Duration,
742 ttl: Duration,
743 node_type: OnlineNodeType,
744 started_at_ms: u128,
745 services: Vec<OnionExitService>,
746 policy: OnionExitPolicy,
747 publisher: DhtRegistrationPublisher,
748}
749
750impl OnionExitRegistration {
751 pub fn new(
753 heartbeat_interval: Duration,
754 ttl: Duration,
755 node_type: OnlineNodeType,
756 services: Vec<OnionExitService>,
757 policy: OnionExitPolicy,
758 ) -> Self {
759 Self {
760 heartbeat_interval,
761 ttl,
762 node_type,
763 started_at_ms: get_epoch_ms(),
764 services,
765 policy,
766 publisher: DhtRegistrationPublisher::new(ONION_EXITS_TOPIC),
767 }
768 }
769
770 pub fn validate_enabled_schedule(&self) -> Result<()> {
772 if self.heartbeat_interval >= self.ttl {
773 return Err(Error::InvalidConfig(format!(
774 "onion_exit_heartbeat_interval ({:?}) must be less than onion_exit_ttl ({:?})",
775 self.heartbeat_interval, self.ttl
776 )));
777 }
778 Ok(())
779 }
780
781 pub fn descriptors_at(
783 &self,
784 context: &RegistrationContext<'_>,
785 now_ms: u128,
786 ) -> Result<Vec<OnionExitDescriptor>> {
787 self.services
788 .iter()
789 .cloned()
790 .map(|service| self.descriptor_for_service(context, now_ms, service))
791 .collect()
792 }
793
794 fn descriptor_for_service(
795 &self,
796 context: &RegistrationContext<'_>,
797 now_ms: u128,
798 service: OnionExitService,
799 ) -> Result<OnionExitDescriptor> {
800 OnionExitDescriptor::new_signed(
801 OnionExitDescriptorBody {
802 did: context.did(),
803 public_key: context.account_verification_pubkey()?,
804 session_public_key: context.session_sk().session_public_key(),
805 node_type: self.node_type.clone(),
806 network_id: context.network_id(),
807 service,
808 policy: self.policy.clone(),
809 started_at_ms: self.started_at_ms,
810 heartbeat_at_ms: now_ms,
811 expires_at_ms: now_ms + self.ttl.as_millis(),
812 version: crate::util::build_version(),
813 },
814 context.session_sk(),
815 )
816 .map_err(Error::CoreError)
817 }
818
819 pub async fn publish_descriptors(
821 &self,
822 context: &RegistrationContext<'_>,
823 ) -> Result<Vec<OnionExitDescriptor>> {
824 let now_ms = get_epoch_ms();
825 let descriptors = self.descriptors_at(context, now_ms)?;
826 let encoded = descriptors
827 .iter()
828 .map(|descriptor| descriptor.encode().map_err(Error::CoreError))
829 .collect::<Result<Vec<_>>>()?;
830 self.publisher
831 .publish_many_replacing(context, encoded, |observed| {
832 observed
833 .decode::<OnionExitDescriptor>()
834 .is_ok_and(|descriptor| {
835 descriptor.did == context.did()
836 || (descriptor.verify_signature() && descriptor.is_expired_at(now_ms))
837 })
838 })
839 .await?;
840 Ok(descriptors)
841 }
842
843 pub fn decode_descriptors_from_entry(
845 entry: &rings_core::dht::entry::Entry,
846 ) -> OnionExitDescriptorDecodeReport {
847 let mut report = OnionExitDescriptorDecodeReport::default();
848 for value in &entry.data {
849 match value.decode::<OnionExitDescriptor>() {
850 Ok(descriptor) => report.descriptors.push(descriptor),
851 Err(error) => {
852 report.rejected_values = report.rejected_values.saturating_add(1);
853 tracing::debug!(
854 "rejected onion-exit descriptor registry value at schema boundary: {error}"
855 );
856 }
857 }
858 }
859 report
860 }
861
862 pub fn descriptors_from_entry(
864 entry: &rings_core::dht::entry::Entry,
865 ) -> Vec<OnionExitDescriptor> {
866 let report = Self::decode_descriptors_from_entry(entry);
867 if report.has_rejections() {
868 tracing::warn!(
869 rejected_values = report.rejected_values,
870 "ignored unsupported onion-exit descriptor registry values"
871 );
872 }
873 report.descriptors
874 }
875}
876
877#[cfg_attr(rings_browser, async_trait(?Send))]
878#[cfg_attr(rings_native, async_trait)]
879impl RegistrationTask for OnionExitRegistration {
880 fn name(&self) -> &'static str {
881 "onion-exit"
882 }
883
884 fn interval(&self) -> Duration {
885 self.heartbeat_interval
886 }
887
888 async fn register_once(&self, context: &RegistrationContext<'_>) -> Result<()> {
889 self.publish_descriptors(context).await.map(|_| ())
890 }
891}
892
893#[cfg(test)]
894mod tests;