sipx_ua/gruu.rs
1//! GRUUs: obtaining them from a registrar, and choosing which one to use (RFC 5627).
2//!
3//! A registration says "this user is reachable here". A GRUU says "*this device* is reachable
4//! here", which is a different claim and the one a transfer, a conference invitation or a
5//! callback needs: an address of record resolves to every phone the user has registered, and a
6//! request routed to all of them is not a request routed back to the one that was talking.
7//!
8//! sipx **obtains and uses** GRUUs. Minting them is §5's registrar behaviour and is not here —
9//! sipx is not a registrar, and the two halves share nothing but the wire format.
10//!
11//! Three things are worth knowing before reading on:
12//!
13//! - **The instance ID is the same one Outbound registers with.** §4.1 identifies the instance
14//! with the `+sip.instance` media feature tag that RFC 5626 §4.1 also defines. Two mechanisms,
15//! one identity — [`Registration`](crate::registrar::Registration) holds it in a single field
16//! so that they cannot come to hold two, which is a fault that only appears under a registrar
17//! that correlates them.
18//! - **The two GRUUs are not interchangeable.** See [`Kind`]. Substituting one for the other
19//! silently is the failure this module goes out of its way not to have.
20//! - **Recognising one needs more than URI equality.** §5.4: "A public GRUU will always be
21//! equivalent to the AOR based on URI equality rules." The comparison that does not make that
22//! mistake is [`sipx_sip::gruu::addressed_to`].
23
24use bytes::Bytes;
25use sipx_sip::headers::ContactValue;
26use sipx_sip::{Address, Response, Uri};
27
28use crate::outbound::InstanceId;
29
30/// The option tag a UA offers to ask for a GRUU.
31///
32/// §4.1: a compliant UA "MUST include the Supported header field" in every REGISTER and "the
33/// value of that header field MUST include 'gruu' as one of the option tags". A registrar that
34/// does not see it has been told nothing was asked for, and §5.2 has it attach nothing.
35pub const OPTION_TAG: &str = "gruu";
36
37/// The `Contact` header field parameter carrying the public GRUU (§7).
38const PUB_GRUU_PARAM: &str = "pub-gruu";
39
40/// The `Contact` header field parameter carrying the temporary GRUU (§7).
41const TEMP_GRUU_PARAM: &str = "temp-gruu";
42
43/// The `Contact` header field parameter naming the instance a binding belongs to.
44///
45/// RFC 5626 §4.1's media feature tag, which §4.1 of this RFC reuses rather than inventing a
46/// second name for the same fact.
47const INSTANCE_PARAM: &str = "+sip.instance";
48
49/// Which of the two GRUUs a UA puts in a `Contact` (§4.4).
50///
51/// # Why the default is [`Kind::Public`]
52///
53/// The two differ in exactly one property each way, and the trade is not sipx's to make.
54///
55/// A **public** GRUU is a stable identifier for the instance. It survives re-registration, and
56/// §5.2 has a registrar keep treating it as valid even after the binding lapses — answering 480
57/// until the device comes back — so an address handed out under one still names this device
58/// tomorrow. What it does not offer is privacy: two public GRUUs for one instance are the same
59/// URI, so anyone holding two of them can see they are the same device.
60///
61/// A **temporary** GRUU buys precisely that privacy. §5.4: "Given a pair of GRUUs, it MUST be
62/// computationally infeasible to determine whether they were issued for the same AOR or instance
63/// ID or for different AORs and instance IDs." It pays for it in lifetime — §4.2 requires a UA
64/// to discard every temporary GRUU it learned whenever its `Call-ID` changes, so an address
65/// handed out under one stops resolving as soon as the UA registers afresh.
66///
67/// So the default is the public one, because it is the choice that keeps working, and because
68/// only the application knows whether the address it is about to put in a `Contact` has to
69/// outlive this registration. Unlinkability is a property that must be *asked* for — and, having
70/// been asked for, is never quietly downgraded: see [`Gruus::preferred`].
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
72pub enum Kind {
73 /// One stable URI for this instance, usable for as long as the instance exists.
74 #[default]
75 Public,
76 /// An address that cannot be correlated with any other GRUU, and that lapses with the
77 /// registration that produced it.
78 Temporary,
79}
80
81/// The GRUUs a registrar issued for one instance's binding (§4.2, §5.2).
82///
83/// Held with the registration rather than beside it, and replaced wholesale on every 2xx. That
84/// is not tidiness: §4.2 requires a UA to "discard all temporary GRUUs learned through prior
85/// REGISTER responses" whenever the `Call-ID` changes, and a set that is replaced rather than
86/// merged cannot carry a stale one across.
87#[derive(Debug, Clone, Default)]
88pub struct Gruus {
89 public: Option<Uri>,
90 temporary: Option<Uri>,
91}
92
93impl Gruus {
94 /// Read the GRUUs a REGISTER 2xx issued for `instance` (§4.2).
95 ///
96 /// §4.2 pairs the two parameters with the `Contact` carrying the `+sip.instance` they were
97 /// minted for — and a 2xx lists *every* current binding for the address of record, other
98 /// devices' included (RFC 3261 §10.3). Selecting the row by instance rather than by position
99 /// is what stops this adopting another phone's GRUU and then answering to it.
100 ///
101 /// A value that is not a GRUU is dropped rather than kept. §7 gives a GRUU the `gr`
102 /// parameter, and a URI without one is the address of record: using it as though it named
103 /// this instance would route every one of the user's devices at a request meant for one.
104 #[must_use]
105 pub fn from_response(response: &Response, instance: &InstanceId) -> Self {
106 for value in response.headers.typed_all::<ContactValue>() {
107 let Ok(ContactValue::Address(address)) = value else {
108 continue;
109 };
110 if !names_instance(&address, instance) {
111 continue;
112 }
113 return Self {
114 public: gruu_param(&address, PUB_GRUU_PARAM),
115 temporary: gruu_param(&address, TEMP_GRUU_PARAM),
116 };
117 }
118 Self::default()
119 }
120
121 /// The public GRUU, if the registrar issued one.
122 #[must_use]
123 pub fn public(&self) -> Option<&Uri> {
124 self.public.as_ref()
125 }
126
127 /// The temporary GRUU, if the registrar issued one.
128 #[must_use]
129 pub fn temporary(&self) -> Option<&Uri> {
130 self.temporary.as_ref()
131 }
132
133 /// Whether the registrar issued neither.
134 ///
135 /// §4.2: "A UA must be prepared for a Contact to contain just one, both, or neither" — a
136 /// registrar that does not implement RFC 5627 answers a REGISTER perfectly well and attaches
137 /// nothing, and that is not an error.
138 #[must_use]
139 pub fn is_empty(&self) -> bool {
140 self.public.is_none() && self.temporary.is_none()
141 }
142
143 /// The GRUU to use for a caller that asked for `kind` (§4.4).
144 ///
145 /// **One never stands in for the other.** Asking for a temporary GRUU and being handed the
146 /// public one would tell the caller the opposite of the truth about what it just put in a
147 /// `Contact`: it believes it published an address nobody can correlate, and it published the
148 /// device's permanent name. `None` — falling back to the ordinary contact — is the honest
149 /// answer, and it is the one that leaks least.
150 #[must_use]
151 pub fn preferred(&self, kind: Kind) -> Option<&Uri> {
152 match kind {
153 Kind::Public => self.public(),
154 Kind::Temporary => self.temporary(),
155 }
156 }
157
158 /// Whether a request whose Request-URI is `request_uri` was sent to one of these (§4.5).
159 #[must_use]
160 pub fn sent_to(&self, request_uri: &Uri) -> bool {
161 [self.public.as_ref(), self.temporary.as_ref()]
162 .into_iter()
163 .flatten()
164 .any(|ours| sipx_sip::gruu::addressed_to(request_uri, ours))
165 }
166
167 /// Each GRUU rendered back to text, for logging and for comparison.
168 #[must_use]
169 fn rendered(&self) -> Vec<Option<String>> {
170 vec![
171 self.public.as_ref().map(Uri::to_string),
172 self.temporary.as_ref().map(Uri::to_string),
173 ]
174 }
175}
176
177/// Compared by what the two URIs say, because [`Uri`] deliberately has no `PartialEq`: RFC 3261
178/// §19.1.4 equivalence is not transitive, and it is the wrong relation here anyway — two
179/// registrations that returned *different spellings* of one GRUU did return different values.
180impl PartialEq for Gruus {
181 fn eq(&self, other: &Self) -> bool {
182 self.rendered() == other.rendered()
183 }
184}
185
186impl Eq for Gruus {}
187
188/// Whether this `Contact` is the binding for `instance` (§4.2).
189///
190/// The angle brackets are part of the wire form — RFC 5626 §4.1 quotes the URN inside them — and
191/// are stripped from both sides so that a registrar echoing the parameter in either spelling is
192/// still recognised.
193#[must_use]
194fn names_instance(address: &Address, instance: &InstanceId) -> bool {
195 address
196 .param(INSTANCE_PARAM)
197 .is_some_and(|value| trim_brackets(value).eq_ignore_ascii_case(instance.urn().as_bytes()))
198}
199
200/// One of the two GRUU `Contact` parameters, parsed (§7).
201///
202/// §7 makes both a `quoted-string`; the parser has already removed the quotes and resolved the
203/// escapes. Angle brackets are stripped as well, because they are not in the grammar and are
204/// exactly the sort of thing an implementation adds anyway — accepting them costs nothing and
205/// rejecting the GRUU costs reachability.
206#[must_use]
207fn gruu_param(address: &Address, name: &str) -> Option<Uri> {
208 let raw = trim_brackets(address.param(name)?);
209 let uri = Uri::parse(Bytes::copy_from_slice(raw)).ok()?;
210 sipx_sip::gruu::is_gruu(&uri).then_some(uri)
211}
212
213/// Strip one layer of `<`/`>`, and the whitespace around it.
214#[must_use]
215fn trim_brackets(value: &[u8]) -> &[u8] {
216 let value = value.trim_ascii();
217 match (value.first(), value.last()) {
218 (Some(b'<'), Some(b'>')) if value.len() >= 2 => {
219 value.get(1..value.len() - 1).unwrap_or_default()
220 }
221 _ => value,
222 }
223}
224
225#[cfg(test)]
226#[allow(
227 clippy::unwrap_used,
228 clippy::expect_used,
229 clippy::panic,
230 clippy::indexing_slicing
231)]
232mod tests {
233 use super::*;
234 use sipx_sip::{Limits, Message, parse_datagram};
235
236 const INSTANCE: &str = "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6";
237 const OTHER_INSTANCE: &str = "urn:uuid:00000000-0000-4000-8000-000000000000";
238
239 fn instance() -> InstanceId {
240 InstanceId::parse(INSTANCE).expect("a urn")
241 }
242
243 fn ok_with(contacts: &str) -> Response {
244 let text = format!(
245 "SIP/2.0 200 OK\r\n\
246 Via: SIP/2.0/UDP 192.0.2.5:5060;branch=z9hG4bKx\r\n\
247 To: <sip:alice@example.com>;tag=r\r\n\
248 From: <sip:alice@example.com>;tag=1\r\n\
249 Call-ID: reg-1@192.0.2.5\r\n\
250 CSeq: 1 REGISTER\r\n\
251 {contacts}\
252 Content-Length: 0\r\n\r\n"
253 );
254 match parse_datagram(Bytes::from(text), &Limits::datagram()).expect("parses") {
255 Message::Response(r) => r,
256 Message::Request(_) => panic!("a response"),
257 }
258 }
259
260 fn issued(contacts: &str) -> Gruus {
261 Gruus::from_response(&ok_with(contacts), &instance())
262 }
263
264 #[test]
265 fn both_gruus_are_read_from_the_binding_that_names_this_instance() {
266 let gruus = issued(&format!(
267 "Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\
268 ;pub-gruu=\"sip:alice@example.com;gr={INSTANCE}\"\
269 ;temp-gruu=\"sip:t7k2xq9f4m@example.com;gr\";expires=3600\r\n"
270 ));
271 assert_eq!(
272 gruus.public().map(Uri::to_string),
273 Some(format!("sip:alice@example.com;gr={INSTANCE}"))
274 );
275 assert_eq!(
276 gruus.temporary().map(Uri::to_string),
277 Some("sip:t7k2xq9f4m@example.com;gr".to_owned())
278 );
279 }
280
281 /// §4.2: "A UA must be prepared for a Contact to contain just one, both, or neither."
282 #[test]
283 fn one_both_or_neither_are_all_ordinary_answers() {
284 assert!(
285 issued(&format!(
286 "Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\r\n"
287 ))
288 .is_empty()
289 );
290 let public_only = issued(&format!(
291 "Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\
292 ;pub-gruu=\"sip:alice@example.com;gr={INSTANCE}\"\r\n"
293 ));
294 assert!(public_only.public().is_some() && public_only.temporary().is_none());
295 let temp_only = issued(&format!(
296 "Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\
297 ;temp-gruu=\"sip:t7k2xq9f4m@example.com;gr\"\r\n"
298 ));
299 assert!(temp_only.public().is_none() && temp_only.temporary().is_some());
300 }
301
302 /// RFC 3261 §10.3 has a 2xx list *every* binding for the address of record. Reading the GRUUs
303 /// off the first row would adopt whichever device happened to register first and answer to a
304 /// URI that routes somewhere else entirely.
305 #[test]
306 fn another_devices_gruu_is_not_ours() {
307 let gruus = issued(&format!(
308 "Contact: <sip:alice@198.51.100.9:5060>;+sip.instance=\"<{OTHER_INSTANCE}>\"\
309 ;pub-gruu=\"sip:alice@example.com;gr={OTHER_INSTANCE}\"\r\n\
310 Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\
311 ;pub-gruu=\"sip:alice@example.com;gr={INSTANCE}\"\r\n"
312 ));
313 assert_eq!(
314 gruus.public().map(Uri::to_string),
315 Some(format!("sip:alice@example.com;gr={INSTANCE}")),
316 "the row for another instance was taken for ours"
317 );
318 }
319
320 /// A binding with no `+sip.instance` is not this instance's, whatever it carries.
321 #[test]
322 fn a_binding_that_names_no_instance_yields_nothing() {
323 assert!(
324 issued(
325 "Contact: <sip:alice@192.0.2.5:5060>;pub-gruu=\"sip:alice@example.com;gr=x\"\r\n"
326 )
327 .is_empty()
328 );
329 assert!(issued("").is_empty());
330 }
331
332 /// §7 gives a GRUU the `gr` parameter. Without it the value is the address of record, and
333 /// putting *that* in a `Contact` as though it named this instance would fan a mid-dialog
334 /// request out to every device the user has.
335 #[test]
336 fn a_pub_gruu_without_the_gr_parameter_is_not_a_gruu() {
337 assert!(
338 issued(&format!(
339 "Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\
340 ;pub-gruu=\"sip:alice@example.com\"\r\n"
341 ))
342 .is_empty()
343 );
344 }
345
346 /// §7 spells both parameters as quoted strings and neither in angle brackets, but a value
347 /// that arrives bracketed is still the URI it names.
348 #[test]
349 fn a_bracketed_value_is_accepted_the_same_as_a_bare_one() {
350 let gruus = issued(&format!(
351 "Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\
352 ;pub-gruu=\"<sip:alice@example.com;gr={INSTANCE}>\"\r\n"
353 ));
354 assert_eq!(
355 gruus.public().map(Uri::to_string),
356 Some(format!("sip:alice@example.com;gr={INSTANCE}"))
357 );
358 }
359
360 /// The point of the type: a caller that asked for privacy is told it did not get it, rather
361 /// than being handed the stable identifier and left believing otherwise.
362 #[test]
363 fn a_missing_temporary_gruu_is_never_answered_with_the_public_one() {
364 let gruus = issued(&format!(
365 "Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\
366 ;pub-gruu=\"sip:alice@example.com;gr={INSTANCE}\"\r\n"
367 ));
368 assert!(gruus.preferred(Kind::Public).is_some());
369 assert!(
370 gruus.preferred(Kind::Temporary).is_none(),
371 "§5.4's unlinkability is not something a public GRUU can stand in for"
372 );
373 }
374
375 #[test]
376 fn the_default_choice_is_the_public_gruu() {
377 assert_eq!(Kind::default(), Kind::Public);
378 }
379
380 #[test]
381 fn a_request_is_recognised_only_when_it_names_one_of_our_gruus() {
382 let gruus = issued(&format!(
383 "Contact: <sip:alice@192.0.2.5:5060>;+sip.instance=\"<{INSTANCE}>\"\
384 ;pub-gruu=\"sip:alice@example.com;gr={INSTANCE}\"\
385 ;temp-gruu=\"sip:t7k2xq9f4m@example.com;gr\"\r\n"
386 ));
387 let uri = |text: &str| Uri::parse(Bytes::from(text.to_owned())).expect("a URI");
388 assert!(gruus.sent_to(&uri(&format!("sip:alice@example.com;gr={INSTANCE}"))));
389 assert!(gruus.sent_to(&uri("sip:t7k2xq9f4m@example.com;gr")));
390 // §5.4: the address of record is URI-equivalent to the public GRUU and must still not
391 // count, because it names every device rather than this one.
392 assert!(!gruus.sent_to(&uri("sip:alice@example.com")));
393 assert!(!gruus.sent_to(&uri(&format!("sip:alice@example.com;gr={OTHER_INSTANCE}"))));
394 assert!(!Gruus::default().sent_to(&uri(&format!("sip:alice@example.com;gr={INSTANCE}"))));
395 }
396}