tor_hsservice/caps.rs
1//! Helpers for capability declaration and negotiation.
2
3// TODO #2598: Remove duplicate code between tor_hsclient::caps,
4// tor_hsservice::caps, and tor_proto::circuit (to the extent possible).
5
6use cfg_if::cfg_if;
7use tor_cell::relaycell::hs::intro_payload::IntroduceHandshakePayload;
8use tor_protover::Protocols;
9
10use crate::IntroRequestError;
11
12cfg_if! {
13 if #[cfg(feature = "negotiate-extensions")] {
14 use std::sync::LazyLock;
15 use tor_protover::{NamedSubver, named::*};
16
17 /// A set of protocols that we can advertise in our HS descriptor,
18 /// if we support them.
19 static ADVERTIZABLE_PROTOCOLS: LazyLock<Protocols> = LazyLock::new(
20 || [RELAY_CRYPT_CGO, RELAY_NEGOTIATE_SUBPROTO].into_iter().collect()
21 );
22
23 /// An array of protocols that can be requested via a subprotocol
24 /// request extension.
25 ///
26 /// Note that we don't necessarily support all of these ourselves!
27 static REQUESTABLE_LIST: &[NamedSubver] = &[
28 RELAY_CRYPT_CGO
29 ];
30
31 /// A set of protocols that can be requested via a subprotocol request extension.
32 static REQUESTABLE: LazyLock<Protocols> = LazyLock::new(
33 || REQUESTABLE_LIST.iter().copied().collect()
34 );
35
36 /// A set of protocols that we should advertise, if supported,
37 /// in our `flow-control` line.
38 static ADVERTISE_FLOWCTRL: LazyLock<Protocols> = LazyLock::new(
39 || [FLOWCTRL_AUTH_SENDME, FLOWCTRL_CC].into_iter().collect()
40 );
41 }
42}
43
44/// Return the list of protocols that we should advertise in our hsdesc.
45pub(crate) fn declared_protocols() -> Protocols {
46 cfg_if! {
47 if #[cfg(feature = "negotiate-extensions")] {
48 tor_proto::supported_client_protocols().intersection(&ADVERTIZABLE_PROTOCOLS)
49 } else {
50 Protocols::new()
51 }
52 }
53}
54
55/// Return the flow control information that we should include in our hsdesc.
56pub(crate) fn declared_flowctrl(
57 params: &tor_netdir::params::NetParameters,
58) -> Option<(Protocols, u8)> {
59 cfg_if! {
60 if #[cfg(feature = "negotiate-extensions")] {
61 let supported = tor_proto::supported_client_protocols();
62 supported.supports_named_subver(FLOWCTRL_CC).then(|| {
63 (
64 supported.intersection(&ADVERTISE_FLOWCTRL),
65 params.cc_sendme_inc.into()
66 )
67 })
68 } else {
69 None
70 }
71 }
72}
73
74/// Return a set of capabilities requested by the client in an INTRODUCE2 message payload.
75///
76/// The returned set will include capabilities that are...
77/// - Requested explicitly by using `SUBPROTOCOL_REQUEST`
78/// - Requested implicitly by using `FLOWCTRL_CC`
79///
80/// Returns an error if the client requested any capability that we do not support,
81/// or which may not be included in a list of requested protocols.
82#[cfg(feature = "negotiate-extensions")]
83pub(crate) fn negotiated_capabilities(
84 intro: &IntroduceHandshakePayload,
85) -> Result<Protocols, IntroRequestError> {
86 let mut requested = Vec::new();
87
88 // A list of capabilities (at the tor_proto level!) that we actually support.
89 //
90 // (We could possibly stop checking this once the flowctrl-cc and counter-galois-onion features
91 // are always-on.)
92 //
93 // We use this instead of annotating NEGOTIABLE_REQUEST_LIST members with `cfg`,
94 // because we don't want to add our own set of flowctrl-cc/counter-galois-onion features
95 // to match tor-proto.
96 let supported_protocols = tor_proto::supported_client_protocols();
97
98 if intro.cc_request_extension().is_some() {
99 // The client asked for FLOWCTRL_CC. If we support it, add it to `requested`.
100 // Otherwise, reject the request.
101 if supported_protocols.supports_named_subver(FLOWCTRL_CC) {
102 requested.push(FLOWCTRL_CC);
103 } else {
104 return Err(IntroRequestError::UnsupportedCapability);
105 }
106 }
107
108 if let Some(sp) = intro.subprotocol_request_extension() {
109 if !sp.contains_only(&REQUESTABLE) {
110 // Some capability was listed that is not supported at all with this extension.
111 // Reject.
112 return Err(IntroRequestError::UnsupportedCapability);
113 }
114 for cap in REQUESTABLE_LIST {
115 if sp.contains(*cap) {
116 if !supported_protocols.supports_named_subver(*cap) {
117 // They requested something that we don't support. Reject the request.
118 return Err(IntroRequestError::UnsupportedCapability);
119 }
120 requested.push(*cap);
121 }
122 }
123 }
124
125 let requested: Protocols = requested.into_iter().collect();
126
127 // We cannot provide CGO unless we also have negotiated CC .
128 if requested.supports_named_subver(RELAY_CRYPT_CGO)
129 && !requested.supports_named_subver(FLOWCTRL_CC)
130 {
131 return Err(IntroRequestError::UnsupportedCapability);
132 }
133
134 Ok(requested)
135}
136
137/// Legacy implementation of negotiated_capabilities.
138///
139// TODO: Remove this, and make the entire negotiate-extensions feature always-on.
140#[cfg(not(feature = "negotiate-extensions"))]
141pub(crate) fn negotiated_capabilities(
142 intro: &IntroduceHandshakePayload,
143) -> Result<Protocols, IntroRequestError> {
144 // In this case, we "do not recognize" SUBPROTO_REQUEST,
145 // and so we ignore it.
146
147 Ok(Protocols::new())
148}