1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
38use std::time::Duration;
39
40use url::Url;
41
42pub mod env_keys {
44 pub const ENABLED: &str = "SOF_RESOLVE_REMOTE";
45 pub const ALLOWED_BASE_URLS: &str = "SOF_RESOLVE_ALLOWED_BASE_URLS";
46 pub const TIMEOUT_MS: &str = "SOF_RESOLVE_TIMEOUT_MS";
47 pub const MAX_FETCHES: &str = "SOF_RESOLVE_MAX_FETCHES";
48 pub const MAX_DEPTH: &str = "SOF_RESOLVE_MAX_DEPTH";
49 pub const MAX_RESPONSE_BYTES: &str = "SOF_RESOLVE_MAX_RESPONSE_BYTES";
50 pub const CONCURRENCY: &str = "SOF_RESOLVE_CONCURRENCY";
51 pub const AUTH: &str = "SOF_RESOLVE_AUTH";
53 pub const ALLOW_PRIVATE_ADDRESSES: &str = "SOF_RESOLVE_ALLOW_PRIVATE_ADDRESSES";
55 pub const CACHE_MAX_ENTRIES: &str = "SOF_RESOLVE_CACHE_MAX_ENTRIES";
57}
58
59const DEFAULT_TIMEOUT_MS: u64 = 5_000;
60const DEFAULT_MAX_FETCHES: usize = 256;
61const DEFAULT_MAX_DEPTH: usize = 1;
62const DEFAULT_MAX_RESPONSE_BYTES: usize = 5_000_000;
63const DEFAULT_CONCURRENCY: usize = 8;
64const DEFAULT_CACHE_MAX_ENTRIES: usize = 10_000;
65
66#[derive(Debug, Clone)]
71pub struct RemoteResolveConfig {
72 pub enabled: bool,
74 pub allowed_base_urls: Vec<AllowedBaseUrl>,
76 pub timeout: Duration,
78 pub max_fetches: usize,
80 pub max_depth: usize,
82 pub max_response_bytes: usize,
84 pub concurrency: usize,
86 pub bearer_tokens: std::collections::HashMap<String, String>,
89 pub allow_private_addresses: bool,
99 pub cache_max_entries: usize,
106}
107
108impl Default for RemoteResolveConfig {
109 fn default() -> Self {
110 Self {
111 enabled: false,
112 allowed_base_urls: Vec::new(),
113 timeout: Duration::from_millis(DEFAULT_TIMEOUT_MS),
114 max_fetches: DEFAULT_MAX_FETCHES,
115 max_depth: DEFAULT_MAX_DEPTH,
116 max_response_bytes: DEFAULT_MAX_RESPONSE_BYTES,
117 concurrency: DEFAULT_CONCURRENCY,
118 bearer_tokens: std::collections::HashMap::new(),
119 allow_private_addresses: false,
120 cache_max_entries: DEFAULT_CACHE_MAX_ENTRIES,
121 }
122 }
123}
124
125impl RemoteResolveConfig {
126 pub fn from_env() -> Self {
131 Self::from_env_with(|key| std::env::var(key).ok())
132 }
133
134 pub fn from_env_with(get: impl Fn(&str) -> Option<String>) -> Self {
139 let enabled = get(env_keys::ENABLED)
140 .map(|v| parse_bool(&v))
141 .unwrap_or(false);
142
143 let allowed_base_urls = get(env_keys::ALLOWED_BASE_URLS)
144 .map(|v| parse_allowlist(&v))
145 .unwrap_or_default();
146
147 let timeout = Duration::from_millis(parse_or_default(
148 get(env_keys::TIMEOUT_MS).as_deref(),
149 env_keys::TIMEOUT_MS,
150 DEFAULT_TIMEOUT_MS,
151 ));
152 let max_fetches = parse_or_default(
153 get(env_keys::MAX_FETCHES).as_deref(),
154 env_keys::MAX_FETCHES,
155 DEFAULT_MAX_FETCHES,
156 );
157 let max_depth = parse_or_default(
158 get(env_keys::MAX_DEPTH).as_deref(),
159 env_keys::MAX_DEPTH,
160 DEFAULT_MAX_DEPTH,
161 );
162 let max_response_bytes = parse_or_default(
163 get(env_keys::MAX_RESPONSE_BYTES).as_deref(),
164 env_keys::MAX_RESPONSE_BYTES,
165 DEFAULT_MAX_RESPONSE_BYTES,
166 );
167 let concurrency = parse_or_default(
168 get(env_keys::CONCURRENCY).as_deref(),
169 env_keys::CONCURRENCY,
170 DEFAULT_CONCURRENCY,
171 )
172 .max(1);
173
174 let bearer_tokens = get(env_keys::AUTH)
175 .map(|v| parse_bearer_tokens(&v))
176 .unwrap_or_default();
177
178 let allow_private_addresses = get(env_keys::ALLOW_PRIVATE_ADDRESSES)
179 .map(|v| parse_bool(&v))
180 .unwrap_or(false);
181
182 let cache_max_entries = parse_or_default(
183 get(env_keys::CACHE_MAX_ENTRIES).as_deref(),
184 env_keys::CACHE_MAX_ENTRIES,
185 DEFAULT_CACHE_MAX_ENTRIES,
186 )
187 .max(1);
188
189 Self {
190 enabled,
191 allowed_base_urls,
192 timeout,
193 max_fetches,
194 max_depth,
195 max_response_bytes,
196 concurrency,
197 bearer_tokens,
198 allow_private_addresses,
199 cache_max_entries,
200 }
201 }
202
203 pub fn bearer_for_host(&self, host: &str) -> Option<&str> {
205 self.bearer_tokens
206 .get(&host.to_ascii_lowercase())
207 .map(String::as_str)
208 }
209
210 pub fn is_active(&self) -> bool {
213 self.enabled && !self.allowed_base_urls.is_empty()
214 }
215
216 pub fn fetch_decision(&self, reference: &str) -> FetchDecision {
221 if !self.enabled {
222 return FetchDecision::Deny(DenyReason::Disabled);
223 }
224
225 let url = match Url::parse(reference) {
226 Ok(u) => u,
227 Err(_) => return FetchDecision::Deny(DenyReason::NotAbsoluteUrl),
228 };
229
230 match url.scheme() {
233 "http" | "https" => {}
234 _ => return FetchDecision::Deny(DenyReason::UnsupportedScheme),
235 }
236 if url.host_str().is_none() {
237 return FetchDecision::Deny(DenyReason::NotAbsoluteUrl);
238 }
239
240 if self.allowed_base_urls.iter().any(|base| base.matches(&url)) {
241 FetchDecision::Allow
242 } else {
243 FetchDecision::Deny(DenyReason::NotAllowlisted)
244 }
245 }
246}
247
248#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct AllowedBaseUrl {
254 scheme: String,
255 host: String,
256 port: u16,
257 path_prefix: String,
259}
260
261impl AllowedBaseUrl {
262 pub fn parse(raw: &str) -> Result<Self, AllowlistParseError> {
264 let url = Url::parse(raw.trim()).map_err(|_| AllowlistParseError::InvalidUrl)?;
265
266 let scheme = url.scheme().to_string();
267 if scheme != "http" && scheme != "https" {
268 return Err(AllowlistParseError::UnsupportedScheme);
269 }
270
271 let host = url
272 .host_str()
273 .ok_or(AllowlistParseError::MissingHost)?
274 .to_ascii_lowercase();
275
276 let port = url
277 .port_or_known_default()
278 .ok_or(AllowlistParseError::MissingPort)?;
279
280 let path_prefix = url.path().trim_end_matches('/').to_string();
281
282 Ok(Self {
283 scheme,
284 host,
285 port,
286 path_prefix,
287 })
288 }
289
290 fn matches(&self, url: &Url) -> bool {
292 url.scheme() == self.scheme
293 && url
294 .host_str()
295 .map(|h| h.eq_ignore_ascii_case(&self.host))
296 .unwrap_or(false)
297 && url.port_or_known_default() == Some(self.port)
298 && path_prefix_matches(&self.path_prefix, url.path())
299 }
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
304pub enum AllowlistParseError {
305 #[error("not a valid absolute URL")]
306 InvalidUrl,
307 #[error("unsupported scheme (only http/https are allowed)")]
308 UnsupportedScheme,
309 #[error("URL has no host")]
310 MissingHost,
311 #[error("URL has no resolvable port")]
312 MissingPort,
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub enum FetchDecision {
318 Allow,
320 Deny(DenyReason),
322}
323
324impl FetchDecision {
325 pub fn is_allowed(&self) -> bool {
327 matches!(self, FetchDecision::Allow)
328 }
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq)]
333pub enum DenyReason {
334 Disabled,
336 NotAbsoluteUrl,
338 UnsupportedScheme,
340 NotAllowlisted,
342}
343
344pub fn is_disallowed_ip(ip: IpAddr) -> bool {
351 is_blocked_address(ip, false)
352}
353
354pub fn is_blocked_address(ip: IpAddr, allow_private: bool) -> bool {
369 let (always_blocked, private) = match ip {
370 IpAddr::V4(v4) => (is_always_blocked_ipv4(v4), v4.is_private()),
371 IpAddr::V6(v6) => {
372 if let Some(mapped) = v6.to_ipv4_mapped() {
374 (is_always_blocked_ipv4(mapped), mapped.is_private())
375 } else {
376 (is_always_blocked_ipv6(v6), is_unique_local_ipv6(v6))
377 }
378 }
379 };
380 always_blocked || (!allow_private && private)
381}
382
383fn is_always_blocked_ipv4(ip: Ipv4Addr) -> bool {
384 let [a, b, _, _] = ip.octets();
385 ip.is_unspecified()
386 || ip.is_loopback()
387 || ip.is_link_local()
388 || ip.is_broadcast()
389 || ip.is_documentation()
390 || ip.is_multicast()
391 || a == 0
393 || (a == 100 && (64..=127).contains(&b))
395 || a >= 240
397}
398
399fn is_always_blocked_ipv6(ip: Ipv6Addr) -> bool {
400 let first = ip.segments()[0];
401 ip.is_unspecified()
402 || ip.is_loopback()
403 || ip.is_multicast()
404 || (first & 0xffc0) == 0xfe80
406}
407
408fn is_unique_local_ipv6(ip: Ipv6Addr) -> bool {
410 (ip.segments()[0] & 0xfe00) == 0xfc00
411}
412
413fn path_prefix_matches(prefix: &str, path: &str) -> bool {
416 if prefix.is_empty() {
417 return true;
418 }
419 let path = path.trim_end_matches('/');
420 path == prefix || path.starts_with(&format!("{prefix}/"))
421}
422
423pub fn parse_allowed_base_urls(csv: &str) -> Vec<AllowedBaseUrl> {
427 parse_allowlist(csv)
428}
429
430fn parse_bearer_tokens(csv: &str) -> std::collections::HashMap<String, String> {
432 csv.split(',')
433 .map(str::trim)
434 .filter(|s| !s.is_empty())
435 .filter_map(|pair| {
436 let (host, token) = pair.split_once('=')?;
437 let host = host.trim();
438 let token = token.trim();
439 if host.is_empty() || token.is_empty() {
440 tracing::warn!(pair, "ignoring malformed {} entry", env_keys::AUTH);
441 return None;
442 }
443 Some((host.to_ascii_lowercase(), token.to_string()))
444 })
445 .collect()
446}
447
448fn parse_allowlist(csv: &str) -> Vec<AllowedBaseUrl> {
450 csv.split(',')
451 .map(str::trim)
452 .filter(|s| !s.is_empty())
453 .filter_map(|entry| match AllowedBaseUrl::parse(entry) {
454 Ok(base) => Some(base),
455 Err(err) => {
456 tracing::warn!(
457 entry,
458 error = %err,
459 "ignoring invalid {} entry",
460 env_keys::ALLOWED_BASE_URLS
461 );
462 None
463 }
464 })
465 .collect()
466}
467
468fn parse_bool(value: &str) -> bool {
469 matches!(
470 value.trim().to_ascii_lowercase().as_str(),
471 "1" | "true" | "yes" | "on"
472 )
473}
474
475fn parse_or_default<T: std::str::FromStr>(value: Option<&str>, key: &str, default: T) -> T {
476 match value {
477 None => default,
478 Some(raw) => match raw.trim().parse() {
479 Ok(parsed) => parsed,
480 Err(_) => {
481 tracing::warn!(key, value = raw, "invalid value; using default");
482 default
483 }
484 },
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491 use std::collections::HashMap;
492
493 fn cfg(enabled: bool, allow: &[&str]) -> RemoteResolveConfig {
494 RemoteResolveConfig {
495 enabled,
496 allowed_base_urls: allow
497 .iter()
498 .map(|s| AllowedBaseUrl::parse(s).expect("valid test base"))
499 .collect(),
500 ..Default::default()
501 }
502 }
503
504 #[test]
507 fn default_config_is_off() {
508 let c = RemoteResolveConfig::default();
509 assert!(!c.enabled);
510 assert!(c.allowed_base_urls.is_empty());
511 assert!(!c.is_active());
512 assert_eq!(c.timeout, Duration::from_millis(DEFAULT_TIMEOUT_MS));
513 assert_eq!(c.max_depth, DEFAULT_MAX_DEPTH);
514 }
515
516 #[test]
517 fn from_env_parses_values() {
518 let env: HashMap<&str, &str> = HashMap::from([
519 (env_keys::ENABLED, "true"),
520 (
521 env_keys::ALLOWED_BASE_URLS,
522 "https://fhir.example.org/r4, https://hapi.example.com/baseR4",
523 ),
524 (env_keys::TIMEOUT_MS, "1500"),
525 (env_keys::MAX_FETCHES, "10"),
526 (env_keys::MAX_DEPTH, "3"),
527 (env_keys::CONCURRENCY, "4"),
528 ]);
529 let c = RemoteResolveConfig::from_env_with(|k| env.get(k).map(|s| s.to_string()));
530
531 assert!(c.enabled);
532 assert!(c.is_active());
533 assert_eq!(c.allowed_base_urls.len(), 2);
534 assert_eq!(c.timeout, Duration::from_millis(1500));
535 assert_eq!(c.max_fetches, 10);
536 assert_eq!(c.max_depth, 3);
537 assert_eq!(c.concurrency, 4);
538 }
539
540 #[test]
541 fn from_env_defaults_off_when_unset() {
542 let c = RemoteResolveConfig::from_env_with(|_| None);
543 assert!(!c.enabled);
544 assert!(c.allowed_base_urls.is_empty());
545 }
546
547 #[test]
548 fn from_env_skips_invalid_allowlist_entries() {
549 let env: HashMap<&str, &str> = HashMap::from([
550 (env_keys::ENABLED, "1"),
551 (
552 env_keys::ALLOWED_BASE_URLS,
553 "https://ok.example.org/fhir, not-a-url, ftp://nope.example.org, ",
554 ),
555 ]);
556 let c = RemoteResolveConfig::from_env_with(|k| env.get(k).map(|s| s.to_string()));
557 assert_eq!(c.allowed_base_urls.len(), 1);
558 }
559
560 #[test]
561 fn from_env_bad_numbers_fall_back_to_default() {
562 let env: HashMap<&str, &str> =
563 HashMap::from([(env_keys::TIMEOUT_MS, "abc"), (env_keys::MAX_FETCHES, "")]);
564 let c = RemoteResolveConfig::from_env_with(|k| env.get(k).map(|s| s.to_string()));
565 assert_eq!(c.timeout, Duration::from_millis(DEFAULT_TIMEOUT_MS));
566 assert_eq!(c.max_fetches, DEFAULT_MAX_FETCHES);
567 }
568
569 #[test]
570 fn bool_parsing_is_lenient() {
571 for v in ["1", "true", "TRUE", "Yes", "on"] {
572 assert!(parse_bool(v), "{v} should be true");
573 }
574 for v in ["0", "false", "no", "", "off", "maybe"] {
575 assert!(!parse_bool(v), "{v} should be false");
576 }
577 }
578
579 #[test]
582 fn allows_reference_under_trusted_base() {
583 let c = cfg(true, &["https://fhir.example.org/r4"]);
584 assert!(
585 c.fetch_decision("https://fhir.example.org/r4/Patient/123")
586 .is_allowed()
587 );
588 assert!(c.fetch_decision("https://fhir.example.org/r4").is_allowed());
590 assert!(
592 c.fetch_decision("https://fhir.example.org/r4/Patient/1?_format=json")
593 .is_allowed()
594 );
595 }
596
597 #[test]
598 fn path_prefix_is_anchored_on_segment_boundary() {
599 let c = cfg(true, &["https://fhir.example.org/r4"]);
600 assert_eq!(
602 c.fetch_decision("https://fhir.example.org/r4extra/Patient/1"),
603 FetchDecision::Deny(DenyReason::NotAllowlisted)
604 );
605 assert_eq!(
606 c.fetch_decision("https://fhir.example.org/other"),
607 FetchDecision::Deny(DenyReason::NotAllowlisted)
608 );
609 }
610
611 #[test]
612 fn host_scheme_and_port_must_match() {
613 let c = cfg(true, &["https://fhir.example.org/r4"]);
614 assert_eq!(
616 c.fetch_decision("https://evil.example.org/r4/Patient/1"),
617 FetchDecision::Deny(DenyReason::NotAllowlisted)
618 );
619 assert_eq!(
621 c.fetch_decision("http://fhir.example.org/r4/Patient/1"),
622 FetchDecision::Deny(DenyReason::NotAllowlisted)
623 );
624 assert_eq!(
626 c.fetch_decision("https://fhir.example.org:8443/r4/Patient/1"),
627 FetchDecision::Deny(DenyReason::NotAllowlisted)
628 );
629 }
630
631 #[test]
632 fn substring_smuggling_does_not_match() {
633 let c = cfg(true, &["https://fhir.example.org/r4"]);
634 assert_eq!(
636 c.fetch_decision("https://evil.com/?u=https://fhir.example.org/r4/Patient/1"),
637 FetchDecision::Deny(DenyReason::NotAllowlisted)
638 );
639 assert_eq!(
641 c.fetch_decision("https://fhir.example.org@evil.com/r4/Patient/1"),
642 FetchDecision::Deny(DenyReason::NotAllowlisted)
643 );
644 }
645
646 #[test]
647 fn explicit_http_base_allows_plaintext() {
648 let c = cfg(true, &["http://localhost:8080/baseR4"]);
650 assert!(
651 c.fetch_decision("http://localhost:8080/baseR4/Patient/1")
652 .is_allowed()
653 );
654 assert_eq!(
656 c.fetch_decision("https://localhost:8080/baseR4/Patient/1"),
657 FetchDecision::Deny(DenyReason::NotAllowlisted)
658 );
659 }
660
661 #[test]
662 fn literal_private_ip_requires_explicit_allowlisting() {
663 let c = cfg(true, &["https://fhir.example.org/r4"]);
665 assert_eq!(
666 c.fetch_decision("https://10.0.0.5/r4/Patient/1"),
667 FetchDecision::Deny(DenyReason::NotAllowlisted)
668 );
669 let c2 = cfg(true, &["https://10.0.0.5/r4"]);
671 assert!(
672 c2.fetch_decision("https://10.0.0.5/r4/Patient/1")
673 .is_allowed()
674 );
675 }
676
677 #[test]
678 fn non_absolute_and_unsupported_schemes_are_denied() {
679 let c = cfg(true, &["https://fhir.example.org/r4"]);
680 assert_eq!(
681 c.fetch_decision("Patient/123"),
682 FetchDecision::Deny(DenyReason::NotAbsoluteUrl)
683 );
684 assert_eq!(
685 c.fetch_decision("#contained-1"),
686 FetchDecision::Deny(DenyReason::NotAbsoluteUrl)
687 );
688 assert_eq!(
689 c.fetch_decision("ftp://fhir.example.org/r4/Patient/1"),
690 FetchDecision::Deny(DenyReason::UnsupportedScheme)
691 );
692 assert_eq!(
693 c.fetch_decision("file:///etc/passwd"),
694 FetchDecision::Deny(DenyReason::UnsupportedScheme)
695 );
696 }
697
698 #[test]
699 fn disabled_or_empty_allowlist_denies_everything() {
700 let disabled = cfg(false, &["https://fhir.example.org/r4"]);
701 assert_eq!(
702 disabled.fetch_decision("https://fhir.example.org/r4/Patient/1"),
703 FetchDecision::Deny(DenyReason::Disabled)
704 );
705
706 let empty = cfg(true, &[]);
707 assert!(!empty.is_active());
708 assert_eq!(
709 empty.fetch_decision("https://fhir.example.org/r4/Patient/1"),
710 FetchDecision::Deny(DenyReason::NotAllowlisted)
711 );
712 }
713
714 #[test]
715 fn root_base_matches_any_path_on_host() {
716 let c = cfg(true, &["https://fhir.example.org"]);
717 assert!(
718 c.fetch_decision("https://fhir.example.org/anything/here")
719 .is_allowed()
720 );
721 assert!(c.fetch_decision("https://fhir.example.org/").is_allowed());
722 }
723
724 #[test]
727 fn disallowed_ipv4_ranges() {
728 for ip in [
729 "0.0.0.0",
730 "127.0.0.1",
731 "10.0.0.1",
732 "172.16.0.1",
733 "172.31.255.255",
734 "192.168.1.1",
735 "169.254.169.254", "100.64.0.1", "192.0.2.1", "255.255.255.255",
739 "224.0.0.1", "240.0.0.1", ] {
742 assert!(
743 is_disallowed_ip(ip.parse().unwrap()),
744 "{ip} should be disallowed"
745 );
746 }
747 }
748
749 #[test]
750 fn allowed_public_ipv4() {
751 for ip in ["8.8.8.8", "1.1.1.1", "93.184.216.34"] {
752 assert!(
753 !is_disallowed_ip(ip.parse().unwrap()),
754 "{ip} should be allowed"
755 );
756 }
757 }
758
759 #[test]
760 fn disallowed_ipv6_ranges() {
761 for ip in [
762 "::1", "fe80::1", "fc00::1", "fd12:3456::1", "ff02::1", "::", "::ffff:127.0.0.1", "::ffff:169.254.169.254", ] {
771 assert!(
772 is_disallowed_ip(ip.parse().unwrap()),
773 "{ip} should be disallowed"
774 );
775 }
776 }
777
778 #[test]
779 fn allowed_public_ipv6() {
780 for ip in [
781 "2606:4700:4700::1111",
782 "2001:4860:4860::8888",
783 "::ffff:8.8.8.8",
784 ] {
785 assert!(
786 !is_disallowed_ip(ip.parse().unwrap()),
787 "{ip} should be allowed"
788 );
789 }
790 }
791
792 #[test]
795 fn allow_private_permits_rfc1918_and_ula() {
796 for ip in [
798 "10.0.0.5",
799 "172.16.4.4",
800 "192.168.1.10",
801 "fc00::1",
802 "fd12:3456::1",
803 ] {
804 let addr: IpAddr = ip.parse().unwrap();
805 assert!(
806 is_blocked_address(addr, false),
807 "{ip} must be blocked by default"
808 );
809 assert!(
810 !is_blocked_address(addr, true),
811 "{ip} must be permitted when allow_private is set"
812 );
813 }
814 }
815
816 #[test]
817 fn always_blocked_ranges_ignore_allow_private() {
818 for ip in [
820 "127.0.0.1",
821 "169.254.169.254", "0.0.0.0",
823 "100.64.0.1", "224.0.0.1", "240.0.0.1", "::1",
827 "fe80::1", "::ffff:127.0.0.1", ] {
830 let addr: IpAddr = ip.parse().unwrap();
831 assert!(
832 is_blocked_address(addr, true),
833 "{ip} must stay blocked even with allow_private"
834 );
835 }
836 }
837
838 #[test]
839 fn public_addresses_allowed_regardless_of_flag() {
840 for ip in ["8.8.8.8", "2606:4700:4700::1111"] {
841 let addr: IpAddr = ip.parse().unwrap();
842 assert!(!is_blocked_address(addr, false));
843 assert!(!is_blocked_address(addr, true));
844 }
845 }
846
847 #[test]
848 fn from_env_parses_allow_private() {
849 let env: HashMap<&str, &str> = HashMap::from([(env_keys::ALLOW_PRIVATE_ADDRESSES, "true")]);
850 let c = RemoteResolveConfig::from_env_with(|k| env.get(k).map(|s| s.to_string()));
851 assert!(c.allow_private_addresses);
852 let d = RemoteResolveConfig::from_env_with(|_| None);
854 assert!(!d.allow_private_addresses);
855 }
856}