deltachat/net/
proxy.rs

1//! # Proxy support.
2//!
3//! Delta Chat supports HTTP(S) CONNECT, SOCKS5 and Shadowsocks protocols.
4
5use std::fmt;
6use std::pin::Pin;
7
8use anyhow::{Context as _, Result, bail, format_err};
9use base64::Engine;
10use bytes::{BufMut, BytesMut};
11use fast_socks5::AuthenticationMethod;
12use fast_socks5::Socks5Command;
13use fast_socks5::client::Socks5Stream;
14use fast_socks5::util::target_addr::ToTargetAddr;
15use percent_encoding::{NON_ALPHANUMERIC, percent_encode, utf8_percent_encode};
16use tokio::io::{AsyncReadExt, AsyncWriteExt};
17use tokio::net::TcpStream;
18use tokio_io_timeout::TimeoutStream;
19use url::Url;
20
21use crate::config::Config;
22use crate::context::Context;
23use crate::net::connect_tcp;
24use crate::net::session::SessionStream;
25use crate::net::tls::wrap_rustls;
26use crate::sql::Sql;
27
28/// Default SOCKS5 port according to [RFC 1928](https://tools.ietf.org/html/rfc1928).
29pub const DEFAULT_SOCKS_PORT: u16 = 1080;
30
31#[derive(Debug, Clone)]
32pub struct ShadowsocksConfig {
33    pub server_config: shadowsocks::config::ServerConfig,
34}
35
36impl PartialEq for ShadowsocksConfig {
37    fn eq(&self, other: &Self) -> bool {
38        self.server_config.to_url() == other.server_config.to_url()
39    }
40}
41
42impl Eq for ShadowsocksConfig {}
43
44impl ShadowsocksConfig {
45    fn to_url(&self) -> String {
46        self.server_config.to_url()
47    }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct HttpConfig {
52    /// HTTP proxy host.
53    pub host: String,
54
55    /// HTTP proxy port.
56    pub port: u16,
57
58    /// Username and password for basic authentication.
59    ///
60    /// If set, `Proxy-Authorization` header is sent.
61    pub user_password: Option<(String, String)>,
62}
63
64impl HttpConfig {
65    fn from_url(url: Url) -> Result<Self> {
66        let host = url
67            .host_str()
68            .context("HTTP proxy URL has no host")?
69            .to_string();
70        let port = url
71            .port_or_known_default()
72            .context("HTTP(S) URLs are guaranteed to return Some port")?;
73        let user_password = if let Some(password) = url.password() {
74            let username = percent_encoding::percent_decode_str(url.username())
75                .decode_utf8()
76                .context("HTTP(S) proxy username is not a valid UTF-8")?
77                .to_string();
78            let password = percent_encoding::percent_decode_str(password)
79                .decode_utf8()
80                .context("HTTP(S) proxy password is not a valid UTF-8")?
81                .to_string();
82            Some((username, password))
83        } else {
84            None
85        };
86        let http_config = HttpConfig {
87            host,
88            port,
89            user_password,
90        };
91        Ok(http_config)
92    }
93
94    fn to_url(&self, scheme: &str) -> String {
95        if let Some((user, password)) = &self.user_password {
96            let user = utf8_percent_encode(user, NON_ALPHANUMERIC);
97            let password = utf8_percent_encode(password, NON_ALPHANUMERIC);
98            format!("{scheme}://{user}:{password}@{}:{}", self.host, self.port)
99        } else {
100            format!("{scheme}://{}:{}", self.host, self.port)
101        }
102    }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct Socks5Config {
107    pub host: String,
108    pub port: u16,
109    pub user_password: Option<(String, String)>,
110}
111
112impl Socks5Config {
113    async fn connect(
114        &self,
115        context: &Context,
116        target_host: &str,
117        target_port: u16,
118        load_dns_cache: bool,
119    ) -> Result<Socks5Stream<Pin<Box<TimeoutStream<TcpStream>>>>> {
120        let tcp_stream = connect_tcp(context, &self.host, self.port, load_dns_cache)
121            .await
122            .context("Failed to connect to SOCKS5 proxy")?;
123
124        let authentication_method = if let Some((username, password)) = self.user_password.as_ref()
125        {
126            Some(AuthenticationMethod::Password {
127                username: username.into(),
128                password: password.into(),
129            })
130        } else {
131            None
132        };
133        let mut socks_stream =
134            Socks5Stream::use_stream(tcp_stream, authentication_method, Default::default()).await?;
135        let target_addr = (target_host, target_port).to_target_addr()?;
136        socks_stream
137            .request(Socks5Command::TCPConnect, target_addr)
138            .await?;
139
140        Ok(socks_stream)
141    }
142
143    fn to_url(&self) -> String {
144        if let Some((user, password)) = &self.user_password {
145            let user = utf8_percent_encode(user, NON_ALPHANUMERIC);
146            let password = utf8_percent_encode(password, NON_ALPHANUMERIC);
147            format!("socks5://{user}:{password}@{}:{}", self.host, self.port)
148        } else {
149            format!("socks5://{}:{}", self.host, self.port)
150        }
151    }
152}
153
154/// Configuration for the proxy through which all traffic
155/// (except for iroh p2p connections)
156/// will be sent.
157#[derive(Debug, Clone, PartialEq, Eq)]
158#[expect(clippy::large_enum_variant)]
159pub enum ProxyConfig {
160    /// HTTP proxy.
161    Http(HttpConfig),
162
163    /// HTTPS proxy.
164    Https(HttpConfig),
165
166    /// SOCKS5 proxy.
167    Socks5(Socks5Config),
168
169    /// Shadowsocks proxy.
170    Shadowsocks(ShadowsocksConfig),
171}
172
173/// Constructs HTTP/1.1 `CONNECT` request for HTTP(S) proxy.
174fn http_connect_request(host: &str, port: u16, auth: Option<(&str, &str)>) -> String {
175    // According to <https://datatracker.ietf.org/doc/html/rfc7230#section-5.4>
176    // clients MUST send `Host:` header in HTTP/1.1 requests,
177    // so repeat the host there.
178    let mut res = format!("CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n");
179    if let Some((username, password)) = auth {
180        res += "Proxy-Authorization: Basic ";
181        res += &base64::engine::general_purpose::STANDARD.encode(format!("{username}:{password}"));
182        res += "\r\n";
183    }
184    res += "\r\n";
185    res
186}
187
188/// Sends HTTP/1.1 `CONNECT` request over given connection
189/// to establish an HTTP tunnel.
190///
191/// Returns the same connection back so actual data can be tunneled over it.
192async fn http_tunnel<T>(mut conn: T, host: &str, port: u16, auth: Option<(&str, &str)>) -> Result<T>
193where
194    T: AsyncReadExt + AsyncWriteExt + Unpin,
195{
196    // Send HTTP/1.1 CONNECT request.
197    let request = http_connect_request(host, port, auth);
198    conn.write_all(request.as_bytes()).await?;
199
200    let mut buffer = BytesMut::with_capacity(4096);
201
202    let res = loop {
203        if !buffer.has_remaining_mut() {
204            bail!("CONNECT response exceeded buffer size");
205        }
206        let n = conn.read_buf(&mut buffer).await?;
207        if n == 0 {
208            bail!("Unexpected end of CONNECT response");
209        }
210
211        let res = &buffer[..];
212        if res.ends_with(b"\r\n\r\n") {
213            // End of response is not reached, read more.
214            break res;
215        }
216    };
217
218    // Normally response looks like
219    // `HTTP/1.1 200 Connection established\r\n\r\n`.
220    if !res.starts_with(b"HTTP/") {
221        bail!("Unexpected HTTP CONNECT response: {res:?}");
222    }
223
224    // HTTP-version followed by space has fixed length
225    // according to RFC 7230:
226    // <https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2>
227    //
228    // Normally status line starts with `HTTP/1.1 `.
229    // We only care about 3-digit status code.
230    let status_code = res
231        .get(9..12)
232        .context("HTTP status line does not contain a status code")?;
233
234    // Interpret status code according to
235    // <https://datatracker.ietf.org/doc/html/rfc7231#section-6>.
236    if status_code == b"407" {
237        Err(format_err!("Proxy Authentication Required"))
238    } else if status_code.starts_with(b"2") {
239        // Success.
240        Ok(conn)
241    } else {
242        Err(format_err!(
243            "Failed to establish HTTP CONNECT tunnel: {res:?}"
244        ))
245    }
246}
247
248impl ProxyConfig {
249    /// Creates a new proxy configuration by parsing given proxy URL.
250    pub fn from_url(url: &str) -> Result<Self> {
251        let url = Url::parse(url).context("Cannot parse proxy URL")?;
252        match url.scheme() {
253            "http" => {
254                let http_config = HttpConfig::from_url(url)?;
255                Ok(Self::Http(http_config))
256            }
257            "https" => {
258                let https_config = HttpConfig::from_url(url)?;
259                Ok(Self::Https(https_config))
260            }
261            "ss" => {
262                let server_config = shadowsocks::config::ServerConfig::from_url(url.as_str())?;
263                let shadowsocks_config = ShadowsocksConfig { server_config };
264                Ok(Self::Shadowsocks(shadowsocks_config))
265            }
266
267            // Because of `curl` convention,
268            // `socks5` URL scheme may be expected to resolve domain names locally
269            // with `socks5h` URL scheme meaning that hostnames are passed to the proxy.
270            // Resolving hostnames locally is not supported
271            // in Delta Chat when using a proxy
272            // to prevent DNS leaks.
273            // Because of this we do not distinguish
274            // between `socks5` and `socks5h`.
275            "socks5" => {
276                let host = url
277                    .host_str()
278                    .context("socks5 URL has no host")?
279                    .to_string();
280                let port = url.port().unwrap_or(DEFAULT_SOCKS_PORT);
281                let user_password = if let Some(password) = url.password() {
282                    let username = percent_encoding::percent_decode_str(url.username())
283                        .decode_utf8()
284                        .context("SOCKS5 username is not a valid UTF-8")?
285                        .to_string();
286                    let password = percent_encoding::percent_decode_str(password)
287                        .decode_utf8()
288                        .context("SOCKS5 password is not a valid UTF-8")?
289                        .to_string();
290                    Some((username, password))
291                } else {
292                    None
293                };
294                let socks5_config = Socks5Config {
295                    host,
296                    port,
297                    user_password,
298                };
299                Ok(Self::Socks5(socks5_config))
300            }
301            scheme => Err(format_err!("Unknown URL scheme {scheme:?}")),
302        }
303    }
304
305    /// Serializes proxy config into an URL.
306    ///
307    /// This function can be used to normalize proxy URL
308    /// by parsing it and serializing back.
309    pub fn to_url(&self) -> String {
310        match self {
311            Self::Http(http_config) => http_config.to_url("http"),
312            Self::Https(http_config) => http_config.to_url("https"),
313            Self::Socks5(socks5_config) => socks5_config.to_url(),
314            Self::Shadowsocks(shadowsocks_config) => shadowsocks_config.to_url(),
315        }
316    }
317
318    /// Migrates legacy `socks5_host`, `socks5_port`, `socks5_user` and `socks5_password`
319    /// config into `proxy_url` if `proxy_url` is unset or empty.
320    ///
321    /// Unsets `socks5_host`, `socks5_port`, `socks5_user` and `socks5_password` in any case.
322    async fn migrate_socks_config(sql: &Sql) -> Result<()> {
323        if sql.get_raw_config("proxy_url").await?.is_none() {
324            // Load legacy SOCKS5 settings.
325            if let Some(host) = sql
326                .get_raw_config("socks5_host")
327                .await?
328                .filter(|s| !s.is_empty())
329            {
330                let port: u16 = sql
331                    .get_raw_config_int("socks5_port")
332                    .await?
333                    .unwrap_or(DEFAULT_SOCKS_PORT.into()) as u16;
334                let user = sql.get_raw_config("socks5_user").await?.unwrap_or_default();
335                let pass = sql
336                    .get_raw_config("socks5_password")
337                    .await?
338                    .unwrap_or_default();
339
340                let mut proxy_url = "socks5://".to_string();
341                if !pass.is_empty() {
342                    proxy_url += &percent_encode(user.as_bytes(), NON_ALPHANUMERIC).to_string();
343                    proxy_url += ":";
344                    proxy_url += &percent_encode(pass.as_bytes(), NON_ALPHANUMERIC).to_string();
345                    proxy_url += "@";
346                };
347                proxy_url += &host;
348                proxy_url += ":";
349                proxy_url += &port.to_string();
350
351                sql.set_raw_config("proxy_url", Some(&proxy_url)).await?;
352            } else {
353                sql.set_raw_config("proxy_url", Some("")).await?;
354            }
355
356            let socks5_enabled = sql.get_raw_config("socks5_enabled").await?;
357            sql.set_raw_config("proxy_enabled", socks5_enabled.as_deref())
358                .await?;
359        }
360
361        sql.set_raw_config("socks5_enabled", None).await?;
362        sql.set_raw_config("socks5_host", None).await?;
363        sql.set_raw_config("socks5_port", None).await?;
364        sql.set_raw_config("socks5_user", None).await?;
365        sql.set_raw_config("socks5_password", None).await?;
366        Ok(())
367    }
368
369    /// Reads proxy configuration from the database.
370    pub async fn load(context: &Context) -> Result<Option<Self>> {
371        Self::migrate_socks_config(&context.sql)
372            .await
373            .context("Failed to migrate legacy SOCKS config")?;
374
375        let enabled = context.get_config_bool(Config::ProxyEnabled).await?;
376        if !enabled {
377            return Ok(None);
378        }
379
380        let proxy_url = context
381            .get_config(Config::ProxyUrl)
382            .await?
383            .unwrap_or_default();
384        let proxy_url = proxy_url
385            .split_once('\n')
386            .map_or(proxy_url.clone(), |(first_url, _rest)| {
387                first_url.to_string()
388            });
389        let proxy_config = Self::from_url(&proxy_url).context("Failed to parse proxy URL")?;
390        Ok(Some(proxy_config))
391    }
392
393    /// If `load_dns_cache` is true, loads cached DNS resolution results.
394    /// Use this only if the connection is going to be protected with TLS checks.
395    pub(crate) async fn connect(
396        &self,
397        context: &Context,
398        target_host: &str,
399        target_port: u16,
400        load_dns_cache: bool,
401    ) -> Result<Box<dyn SessionStream>> {
402        match self {
403            ProxyConfig::Http(http_config) => {
404                let load_cache = false;
405                let tcp_stream = crate::net::connect_tcp(
406                    context,
407                    &http_config.host,
408                    http_config.port,
409                    load_cache,
410                )
411                .await?;
412                let auth = if let Some((username, password)) = &http_config.user_password {
413                    Some((username.as_str(), password.as_str()))
414                } else {
415                    None
416                };
417                let tunnel_stream = http_tunnel(tcp_stream, target_host, target_port, auth).await?;
418                Ok(Box::new(tunnel_stream))
419            }
420            ProxyConfig::Https(https_config) => {
421                let load_cache = true;
422                let tcp_stream = crate::net::connect_tcp(
423                    context,
424                    &https_config.host,
425                    https_config.port,
426                    load_cache,
427                )
428                .await?;
429                let use_sni = true;
430                let tls_stream = wrap_rustls(
431                    &https_config.host,
432                    https_config.port,
433                    use_sni,
434                    "",
435                    tcp_stream,
436                    &context.tls_session_store,
437                    &context.spki_hash_store,
438                    &context.sql,
439                )
440                .await?;
441                let auth = if let Some((username, password)) = &https_config.user_password {
442                    Some((username.as_str(), password.as_str()))
443                } else {
444                    None
445                };
446                let tunnel_stream = http_tunnel(tls_stream, target_host, target_port, auth).await?;
447                Ok(Box::new(tunnel_stream))
448            }
449            ProxyConfig::Socks5(socks5_config) => {
450                let socks5_stream = socks5_config
451                    .connect(context, target_host, target_port, load_dns_cache)
452                    .await?;
453                Ok(Box::new(socks5_stream))
454            }
455            ProxyConfig::Shadowsocks(ShadowsocksConfig { server_config }) => {
456                let shadowsocks_context = shadowsocks::context::Context::new_shared(
457                    shadowsocks::config::ServerType::Local,
458                );
459
460                let tcp_stream = {
461                    let server_addr = server_config.addr();
462                    let host = server_addr.host();
463                    let port = server_addr.port();
464                    connect_tcp(context, &host, port, load_dns_cache)
465                        .await
466                        .context("Failed to connect to Shadowsocks proxy")?
467                };
468
469                let shadowsocks_stream = shadowsocks::ProxyClientStream::from_stream(
470                    shadowsocks_context,
471                    tcp_stream,
472                    server_config,
473                    (target_host.to_string(), target_port),
474                );
475
476                Ok(Box::new(shadowsocks_stream))
477            }
478        }
479    }
480}
481
482impl fmt::Display for Socks5Config {
483    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
484        write!(
485            f,
486            "host:{},port:{},user_password:{}",
487            self.host,
488            self.port,
489            if let Some(user_password) = self.user_password.clone() {
490                format!("user: {}, password: ***", user_password.0)
491            } else {
492                "user: None".to_string()
493            }
494        )
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501    use crate::config::Config;
502    use crate::test_utils::TestContext;
503
504    #[test]
505    fn test_socks5_url() {
506        let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1:9050").unwrap();
507        assert_eq!(
508            proxy_config,
509            ProxyConfig::Socks5(Socks5Config {
510                host: "127.0.0.1".to_string(),
511                port: 9050,
512                user_password: None
513            })
514        );
515
516        let proxy_config = ProxyConfig::from_url("socks5://foo:bar@127.0.0.1:9150").unwrap();
517        assert_eq!(
518            proxy_config,
519            ProxyConfig::Socks5(Socks5Config {
520                host: "127.0.0.1".to_string(),
521                port: 9150,
522                user_password: Some(("foo".to_string(), "bar".to_string()))
523            })
524        );
525
526        let proxy_config = ProxyConfig::from_url("socks5://%66oo:b%61r@127.0.0.1:9150").unwrap();
527        assert_eq!(
528            proxy_config,
529            ProxyConfig::Socks5(Socks5Config {
530                host: "127.0.0.1".to_string(),
531                port: 9150,
532                user_password: Some(("foo".to_string(), "bar".to_string()))
533            })
534        );
535
536        let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1:80").unwrap();
537        assert_eq!(
538            proxy_config,
539            ProxyConfig::Socks5(Socks5Config {
540                host: "127.0.0.1".to_string(),
541                port: 80,
542                user_password: None
543            })
544        );
545
546        let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1").unwrap();
547        assert_eq!(
548            proxy_config,
549            ProxyConfig::Socks5(Socks5Config {
550                host: "127.0.0.1".to_string(),
551                port: 1080,
552                user_password: None
553            })
554        );
555
556        let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1:1080").unwrap();
557        assert_eq!(
558            proxy_config,
559            ProxyConfig::Socks5(Socks5Config {
560                host: "127.0.0.1".to_string(),
561                port: 1080,
562                user_password: None
563            })
564        );
565
566        let proxy_config = ProxyConfig::from_url("socks5://my-proxy.example.org").unwrap();
567        assert_eq!(
568            proxy_config,
569            ProxyConfig::Socks5(Socks5Config {
570                host: "my-proxy.example.org".to_string(),
571                port: 1080,
572                user_password: None
573            })
574        );
575        assert_eq!(
576            proxy_config.to_url(),
577            "socks5://my-proxy.example.org:1080".to_string()
578        );
579    }
580
581    #[test]
582    fn test_http_url() {
583        let proxy_config = ProxyConfig::from_url("http://127.0.0.1").unwrap();
584        assert_eq!(
585            proxy_config,
586            ProxyConfig::Http(HttpConfig {
587                host: "127.0.0.1".to_string(),
588                port: 80,
589                user_password: None
590            })
591        );
592
593        let proxy_config = ProxyConfig::from_url("http://127.0.0.1:80").unwrap();
594        assert_eq!(
595            proxy_config,
596            ProxyConfig::Http(HttpConfig {
597                host: "127.0.0.1".to_string(),
598                port: 80,
599                user_password: None
600            })
601        );
602
603        let proxy_config = ProxyConfig::from_url("http://127.0.0.1:443").unwrap();
604        assert_eq!(
605            proxy_config,
606            ProxyConfig::Http(HttpConfig {
607                host: "127.0.0.1".to_string(),
608                port: 443,
609                user_password: None
610            })
611        );
612
613        let proxy_config = ProxyConfig::from_url("http://my-proxy.example.org").unwrap();
614        assert_eq!(
615            proxy_config,
616            ProxyConfig::Http(HttpConfig {
617                host: "my-proxy.example.org".to_string(),
618                port: 80,
619                user_password: None
620            })
621        );
622        assert_eq!(
623            proxy_config.to_url(),
624            "http://my-proxy.example.org:80".to_string()
625        );
626    }
627
628    #[test]
629    fn test_https_url() {
630        let proxy_config = ProxyConfig::from_url("https://127.0.0.1").unwrap();
631        assert_eq!(
632            proxy_config,
633            ProxyConfig::Https(HttpConfig {
634                host: "127.0.0.1".to_string(),
635                port: 443,
636                user_password: None
637            })
638        );
639
640        let proxy_config = ProxyConfig::from_url("https://127.0.0.1:80").unwrap();
641        assert_eq!(
642            proxy_config,
643            ProxyConfig::Https(HttpConfig {
644                host: "127.0.0.1".to_string(),
645                port: 80,
646                user_password: None
647            })
648        );
649
650        let proxy_config = ProxyConfig::from_url("https://127.0.0.1:443").unwrap();
651        assert_eq!(
652            proxy_config,
653            ProxyConfig::Https(HttpConfig {
654                host: "127.0.0.1".to_string(),
655                port: 443,
656                user_password: None
657            })
658        );
659
660        let proxy_config = ProxyConfig::from_url("https://my-proxy.example.org").unwrap();
661        assert_eq!(
662            proxy_config,
663            ProxyConfig::Https(HttpConfig {
664                host: "my-proxy.example.org".to_string(),
665                port: 443,
666                user_password: None
667            })
668        );
669        assert_eq!(
670            proxy_config.to_url(),
671            "https://my-proxy.example.org:443".to_string()
672        );
673    }
674
675    #[test]
676    fn test_http_connect_request() {
677        assert_eq!(
678            http_connect_request("example.org", 143, Some(("aladdin", "opensesame"))),
679            "CONNECT example.org:143 HTTP/1.1\r\nHost: example.org:143\r\nProxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l\r\n\r\n"
680        );
681        assert_eq!(
682            http_connect_request("example.net", 587, None),
683            "CONNECT example.net:587 HTTP/1.1\r\nHost: example.net:587\r\n\r\n"
684        );
685    }
686
687    #[test]
688    fn test_shadowsocks_url() {
689        // Example URL from <https://shadowsocks.org/doc/sip002.html>.
690        let proxy_config =
691            ProxyConfig::from_url("ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1")
692                .unwrap();
693        assert!(matches!(proxy_config, ProxyConfig::Shadowsocks(_)));
694    }
695
696    #[test]
697    fn test_invalid_proxy_url() {
698        assert!(ProxyConfig::from_url("foobar://127.0.0.1:9050").is_err());
699        assert!(ProxyConfig::from_url("abc").is_err());
700
701        // This caused panic before shadowsocks 1.22.0.
702        assert!(ProxyConfig::from_url("ss://foo:bar@127.0.0.1:9999").is_err());
703    }
704
705    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
706    async fn test_socks5_migration() -> Result<()> {
707        let t = TestContext::new().await;
708
709        // Test that config is migrated on attempt to load even if disabled.
710        t.set_config(Config::Socks5Host, Some("127.0.0.1")).await?;
711        t.set_config(Config::Socks5Port, Some("9050")).await?;
712
713        let proxy_config = ProxyConfig::load(&t).await?;
714        // Even though proxy is not enabled, config should be migrated.
715        assert_eq!(proxy_config, None);
716
717        assert_eq!(
718            t.get_config(Config::ProxyUrl).await?.unwrap(),
719            "socks5://127.0.0.1:9050"
720        );
721        Ok(())
722    }
723
724    // Test SOCKS5 setting migration if proxy was never configured.
725    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
726    async fn test_socks5_migration_unconfigured() -> Result<()> {
727        let t = TestContext::new().await;
728
729        // Try to load config to trigger migration.
730        assert_eq!(ProxyConfig::load(&t).await?, None);
731
732        assert_eq!(t.get_config(Config::ProxyEnabled).await?, None);
733        assert_eq!(
734            t.get_config(Config::ProxyUrl).await?.unwrap(),
735            String::new()
736        );
737        Ok(())
738    }
739
740    // Test SOCKS5 setting migration if SOCKS5 host is empty.
741    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
742    async fn test_socks5_migration_empty() -> Result<()> {
743        let t = TestContext::new().await;
744
745        t.set_config(Config::Socks5Host, Some("")).await?;
746
747        // Try to load config to trigger migration.
748        assert_eq!(ProxyConfig::load(&t).await?, None);
749
750        assert_eq!(t.get_config(Config::ProxyEnabled).await?, None);
751        assert_eq!(
752            t.get_config(Config::ProxyUrl).await?.unwrap(),
753            String::new()
754        );
755        Ok(())
756    }
757}