1use std::collections::HashMap;
13use std::net::SocketAddr;
14use std::time::Duration;
15
16use serde::Deserialize;
17
18#[derive(Clone, Debug)]
22pub struct QuicBridge {
23 pub addresses: Vec<SocketAddr>,
25 pub sni_host: Option<String>,
27 pub id_pubkey_base64: String,
29}
30
31#[derive(Clone, Debug, Default)]
33pub(crate) struct DirEntry {
34 pub name: Option<String>,
35 pub country: Option<String>,
36 pub quic: Option<QuicBridge>,
37}
38
39#[derive(Clone, Debug, Default)]
41pub(crate) struct DvpnDirectory {
42 entries: HashMap<String, DirEntry>,
43}
44
45impl DvpnDirectory {
46 pub(crate) async fn fetch(url: &str) -> Result<Self, String> {
49 let client = reqwest::Client::builder()
50 .timeout(Duration::from_secs(30))
51 .build()
52 .map_err(|e| e.to_string())?;
53 let raw: Vec<RawGateway> = client
54 .get(url)
55 .send()
56 .await
57 .map_err(|e| e.to_string())?
58 .error_for_status()
59 .map_err(|e| e.to_string())?
60 .json()
61 .await
62 .map_err(|e| e.to_string())?;
63
64 let mut entries = HashMap::with_capacity(raw.len());
65 for gw in raw {
66 entries.insert(
67 gw.identity_key.clone(),
68 DirEntry {
69 name: gw.name.filter(|n| !n.is_empty() && n != "N/A"),
70 country: gw
71 .location
72 .and_then(|l| l.two_letter_iso_country_code)
73 .filter(|c| !c.is_empty()),
74 quic: gw.bridges.and_then(|b| b.into_quic()),
75 },
76 );
77 }
78 Ok(Self { entries })
79 }
80
81 pub(crate) fn entry(&self, identity_base58: &str) -> Option<&DirEntry> {
83 self.entries.get(identity_base58)
84 }
85
86 pub(crate) fn has_quic(&self, identity_base58: &str) -> bool {
88 self.entries
89 .get(identity_base58)
90 .is_some_and(|e| e.quic.is_some())
91 }
92}
93
94#[derive(Deserialize)]
97struct RawGateway {
98 identity_key: String,
99 #[serde(default)]
100 name: Option<String>,
101 #[serde(default)]
102 location: Option<RawLocation>,
103 #[serde(default)]
104 bridges: Option<RawBridges>,
105}
106
107#[derive(Deserialize)]
108struct RawLocation {
109 #[serde(default)]
110 two_letter_iso_country_code: Option<String>,
111}
112
113#[derive(Deserialize)]
114struct RawBridges {
115 #[serde(default)]
116 transports: Vec<RawTransport>,
117}
118
119impl RawBridges {
120 fn into_quic(self) -> Option<QuicBridge> {
122 self.transports
126 .into_iter()
127 .filter(|t| t.transport_type == "quic_plain")
128 .find_map(|t| {
129 let args = t.args?;
130 let addresses = args
131 .addresses
132 .iter()
133 .filter_map(|a| a.parse::<SocketAddr>().ok())
134 .collect::<Vec<_>>();
135 if addresses.is_empty() {
136 return None;
137 }
138 let id_pubkey_base64 = args.id_pubkey.trim().to_string();
142 if id_pubkey_base64.is_empty() {
143 return None;
144 }
145 let sni_host = args
146 .host
147 .map(|h| h.trim().to_string())
148 .filter(|h| !h.is_empty());
149 Some(QuicBridge {
150 addresses,
151 sni_host,
152 id_pubkey_base64,
153 })
154 })
155 }
156}
157
158#[derive(Deserialize)]
159struct RawTransport {
160 transport_type: String,
161 #[serde(default)]
162 args: Option<RawQuicArgs>,
163}
164
165#[derive(Deserialize)]
166struct RawQuicArgs {
167 #[serde(default)]
168 addresses: Vec<String>,
169 #[serde(default)]
170 host: Option<String>,
171 id_pubkey: String,
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 fn bridges(json: &str) -> RawBridges {
179 serde_json::from_str(json).expect("valid RawBridges json")
180 }
181
182 #[test]
183 fn into_quic_skips_broken_first_transport() {
184 let quic = bridges(
187 r#"{"transports":[
188 {"transport_type":"quic_plain","args":{"addresses":["1.2.3.4:443"],"host":"a","id_pubkey":""}},
189 {"transport_type":"quic_plain","args":{"addresses":["5.6.7.8:443"],"host":"b","id_pubkey":"PINKEY"}}
190 ]}"#,
191 )
192 .into_quic()
193 .expect("a usable quic bridge exists");
194 assert_eq!(quic.id_pubkey_base64, "PINKEY");
195 assert_eq!(quic.addresses, vec!["5.6.7.8:443".parse().unwrap()]);
196 }
197
198 #[test]
199 fn into_quic_trims_id_pubkey_and_host() {
200 let quic = bridges(
201 r#"{"transports":[
202 {"transport_type":"quic_plain","args":{"addresses":["1.2.3.4:443"],"host":" sni.example ","id_pubkey":" PADDED "}}
203 ]}"#,
204 )
205 .into_quic()
206 .expect("a usable quic bridge exists");
207 assert_eq!(quic.id_pubkey_base64, "PADDED");
208 assert_eq!(quic.sni_host.as_deref(), Some("sni.example"));
209 }
210
211 #[test]
212 fn into_quic_none_when_all_unusable() {
213 assert!(bridges(
215 r#"{"transports":[
216 {"transport_type":"quic_plain","args":{"addresses":[],"id_pubkey":"X"}},
217 {"transport_type":"quic_plain","args":{"addresses":["1.2.3.4:443"],"id_pubkey":" "}}
218 ]}"#,
219 )
220 .into_quic()
221 .is_none());
222 }
223
224 #[test]
225 fn into_quic_ignores_non_quic_transports() {
226 assert!(bridges(
227 r#"{"transports":[{"transport_type":"other","args":{"addresses":["1.2.3.4:443"],"id_pubkey":"K"}}]}"#,
228 )
229 .into_quic()
230 .is_none());
231 }
232}