1use std::net::SocketAddr;
14
15use iroh::{EndpointAddr, PublicKey, RelayMode, RelayUrl};
16
17use crate::from_hex;
18
19pub const ENV: &str = "BORG_RELAY";
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum Relay {
23 Default,
25 Disabled,
27 Custom(Vec<RelayUrl>),
29}
30
31impl Relay {
32 pub fn choose(configured: Option<&str>) -> Result<Relay, String> {
34 let env = std::env::var(ENV).ok().filter(|v| !v.trim().is_empty());
35 Relay::parse(env.as_deref().or(configured).unwrap_or("default"))
36 }
37
38 pub fn parse(s: &str) -> Result<Relay, String> {
39 match s.trim() {
40 "" | "default" | "n0" => Ok(Relay::Default),
41 "disabled" | "off" | "none" => Ok(Relay::Disabled),
42 urls => urls.split(',').map(|u| relay_url(u.trim())).collect::<Result<Vec<_>, _>>().map(Relay::Custom),
43 }
44 }
45
46 pub fn setting(&self) -> String {
48 match self {
49 Relay::Default => "default".into(),
50 Relay::Disabled => "disabled".into(),
51 Relay::Custom(urls) => urls.iter().map(|u| u.to_string()).collect::<Vec<_>>().join(","),
52 }
53 }
54
55 pub fn mode(&self) -> RelayMode {
56 match self {
57 Relay::Default => iroh::endpoint::default_relay_mode(),
58 Relay::Disabled => RelayMode::Disabled,
59 Relay::Custom(urls) => RelayMode::custom(urls.iter().cloned()),
60 }
61 }
62}
63
64fn relay_url(u: &str) -> Result<RelayUrl, String> {
65 if !(u.starts_with("https://") || u.starts_with("http://")) {
66 return Err(format!("relay {u:?}: expected \"default\", \"disabled\" or an http(s) URL"));
67 }
68 u.parse().map_err(|e| format!("relay {u:?}: {e}"))
69}
70
71fn hint(h: &str) -> Option<Result<Hint, String>> {
73 let h = h.trim();
74 if h.is_empty() {
75 return None;
76 }
77 if h.starts_with("https://") || h.starts_with("http://") {
78 return Some(relay_url(h).map(Hint::Relay));
79 }
80 Some(match h.parse::<SocketAddr>() {
81 Ok(a) if a.ip().is_unspecified() || a.port() == 0 => Err(format!("{h}: not an address a peer can dial")),
82 Ok(a) => Ok(Hint::Direct(a)),
83 Err(e) => Err(format!("bad address {h}: {e}")),
84 })
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum Hint {
89 Direct(SocketAddr),
90 Relay(RelayUrl),
91}
92
93impl std::fmt::Display for Hint {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 match self {
96 Hint::Direct(a) => write!(f, "{a}"),
97 Hint::Relay(u) => write!(f, "{u}"),
98 }
99 }
100}
101
102pub fn hints(s: &str) -> Result<Vec<Hint>, String> {
104 s.split(',').filter_map(hint).collect()
105}
106
107pub fn join_hints(h: &[Hint]) -> String {
108 h.iter().map(|h| h.to_string()).collect::<Vec<_>>().join(",")
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct Target {
114 pub key: String,
115 pub hints: Vec<Hint>,
116}
117
118impl Target {
119 pub fn parse(s: &str) -> Result<Target, String> {
121 let (key, rest) = s.split_once('@').unwrap_or((s, ""));
122 crate::identity::check_key(key)?;
123 Ok(Target { key: key.into(), hints: hints(rest)? })
124 }
125
126 pub fn id(&self) -> Result<PublicKey, String> {
127 PublicKey::from_bytes(&from_hex::<32>(&self.key)?).map_err(|e| e.to_string())
128 }
129
130 pub fn endpoint_addr(&self, relay: &Relay) -> Result<EndpointAddr, String> {
133 let mut a = EndpointAddr::new(self.id()?);
134 for h in &self.hints {
135 a = match h {
136 Hint::Direct(d) => a.with_ip_addr(*d),
137 Hint::Relay(u) if *relay != Relay::Disabled => a.with_relay_url(u.clone()),
138 Hint::Relay(_) => a,
139 };
140 }
141 if let Relay::Custom(urls) = relay {
142 for u in urls {
143 a = a.with_relay_url(u.clone());
144 }
145 }
146 if *relay == Relay::Disabled && a.ip_addrs().next().is_none() {
147 return Err(format!("no direct address for {}: relays are disabled, give <key>@<ip:port>", self.key));
148 }
149 Ok(a)
150 }
151}
152
153impl std::fmt::Display for Target {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 if self.hints.is_empty() { write!(f, "{}", self.key) } else { write!(f, "{}@{}", self.key, join_hints(&self.hints)) }
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 const KEY: &str = "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29";
164
165 #[test]
166 fn relay_settings_parse() {
167 assert_eq!(Relay::parse("").unwrap(), Relay::Default);
168 assert_eq!(Relay::parse("disabled").unwrap(), Relay::Disabled);
169 let Relay::Custom(u) = Relay::parse("https://relay.corp.example").unwrap() else { panic!() };
170 assert_eq!(u.len(), 1);
171 assert!(Relay::parse("relay.corp.example").is_err(), "a bare host is a typo, not a relay");
172 assert_eq!(
173 Relay::parse(&Relay::parse("https://a.example,https://b.example").unwrap().setting()).unwrap().setting(),
174 "https://a.example/,https://b.example/"
175 );
176 }
177
178 #[test]
179 fn targets_by_key_with_old_and_new_hints() {
180 let t = Target::parse(KEY).unwrap();
181 assert!(t.hints.is_empty());
182 assert!(t.endpoint_addr(&Relay::Default).is_ok(), "the key alone: address lookup finds it");
183 assert!(t.endpoint_addr(&Relay::Disabled).unwrap_err().contains("relays are disabled"));
184 let old = Target::parse(&format!("{KEY}@10.0.0.5:7878")).unwrap();
185 assert_eq!(old.hints, vec![Hint::Direct("10.0.0.5:7878".parse().unwrap())]);
186 assert_eq!(old.to_string(), format!("{KEY}@10.0.0.5:7878"));
187 let both = Target::parse(&format!("{KEY}@10.0.0.5:7878,https://relay.corp.example")).unwrap();
188 let a = both.endpoint_addr(&Relay::Default).unwrap();
189 assert_eq!((a.ip_addrs().count(), a.relay_urls().count()), (1, 1));
190 assert_eq!(Target::parse(&both.to_string()).unwrap(), both);
191 assert!(Target::parse(&format!("{KEY}@0.0.0.0:7878")).is_err());
192 assert!(Target::parse("nokey@10.0.0.5:1").is_err());
193 }
194}