feat: shadowsocks support

This change introduces new config options
`proxy_enabled` and `proxy_url`
that replace `socks5_*`.

Tested with deltachat-repl
by starting it with
`cargo run --locked -p deltachat-repl -- deltachat-db` and running
```
> set proxy_enabled 1
> set proxy_url ss://...
> setqr dcaccount:https://chatmail.example.org/new
> configure
```
This commit is contained in:
link2xt
2024-09-12 00:22:09 +00:00
committed by GitHub
parent 2c136f6355
commit 37ca9d7319
26 changed files with 818 additions and 291 deletions

220
Cargo.lock generated
View File

@@ -551,6 +551,19 @@ dependencies = [
"digest",
]
[[package]]
name = "blake3"
version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -631,6 +644,12 @@ version = "3.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b"
[[package]]
name = "byte_string"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11aade7a05aa8c3a351cedc44c3fc45806430543382fcc4743a9b757a2a0b4ed"
[[package]]
name = "bytemuck"
version = "1.16.3"
@@ -716,9 +735,12 @@ dependencies = [
[[package]]
name = "cc"
version = "1.1.7"
version = "1.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc"
checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b"
dependencies = [
"shlex",
]
[[package]]
name = "cfb-mode"
@@ -752,6 +774,19 @@ dependencies = [
"cpufeatures",
]
[[package]]
name = "chacha20poly1305"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20",
"cipher",
"poly1305",
"zeroize",
]
[[package]]
name = "charset"
version = "0.1.3"
@@ -774,7 +809,7 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.52.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -1288,6 +1323,7 @@ dependencies = [
"parking_lot",
"percent-encoding",
"pgp",
"pin-project",
"pretty_assertions",
"proptest",
"qrcodegen",
@@ -1303,6 +1339,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"sha-1",
"shadowsocks",
"smallvec",
"strum",
"strum_macros",
@@ -3294,6 +3331,12 @@ dependencies = [
"linked-hash-map",
]
[[package]]
name = "lru_time_cache"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd"
[[package]]
name = "mailparse"
version = "0.15.0"
@@ -4811,6 +4854,25 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "ring-compat"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccce7bae150b815f0811db41b8312fcb74bffa4cab9cee5429ee00f356dd5bd4"
dependencies = [
"aead",
"digest",
"ecdsa",
"ed25519",
"generic-array",
"p256",
"p384",
"pkcs8",
"rand_core 0.6.4",
"ring",
"signature",
]
[[package]]
name = "ripemd"
version = "0.1.3"
@@ -5176,6 +5238,16 @@ dependencies = [
"serde",
]
[[package]]
name = "sendfd"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "604b71b8fc267e13bb3023a2c901126c8f349393666a6d98ac1ae5729b701798"
dependencies = [
"libc",
"tokio",
]
[[package]]
name = "serde"
version = "1.0.209"
@@ -5322,6 +5394,60 @@ dependencies = [
"keccak",
]
[[package]]
name = "shadowsocks"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06b6af20f0f009894644c9fb149ce6244c69b0a264ffcf7a53cbb3dd4883e4a3"
dependencies = [
"aes",
"async-trait",
"base64 0.22.1",
"blake3",
"byte_string",
"bytes",
"cfg-if",
"futures",
"libc",
"log",
"lru_time_cache",
"once_cell",
"percent-encoding",
"pin-project",
"rand 0.8.5",
"sendfd",
"serde",
"serde_json",
"serde_urlencoded",
"shadowsocks-crypto",
"socket2",
"spin 0.9.8",
"thiserror",
"tokio",
"tokio-tfo",
"url",
"windows-sys 0.59.0",
]
[[package]]
name = "shadowsocks-crypto"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e49ecfad8b27f3df28848af11f08aa10df0c6b74b45748131753913be23373"
dependencies = [
"aes",
"aes-gcm",
"blake3",
"bytes",
"cfg-if",
"chacha20poly1305",
"hkdf",
"md-5",
"rand 0.8.5",
"ring-compat",
"sha1",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@@ -5341,6 +5467,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
@@ -5917,6 +6049,23 @@ dependencies = [
"xattr",
]
[[package]]
name = "tokio-tfo"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb4382c6371e29365853d2b71e915d5398df46312a2158097d8bb3f54d0f1b4"
dependencies = [
"cfg-if",
"futures",
"libc",
"log",
"once_cell",
"pin-project",
"socket2",
"tokio",
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-tungstenite"
version = "0.21.0"
@@ -6531,7 +6680,7 @@ dependencies = [
"windows-core 0.52.0",
"windows-implement",
"windows-interface",
"windows-targets 0.52.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -6549,7 +6698,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -6589,7 +6738,16 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.5",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
@@ -6609,18 +6767,18 @@ dependencies = [
[[package]]
name = "windows-targets"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.5",
"windows_aarch64_msvc 0.52.5",
"windows_i686_gnu 0.52.5",
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.5",
"windows_x86_64_gnu 0.52.5",
"windows_x86_64_gnullvm 0.52.5",
"windows_x86_64_msvc 0.52.5",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
@@ -6631,9 +6789,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
@@ -6643,9 +6801,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
@@ -6655,15 +6813,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
@@ -6673,9 +6831,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
@@ -6685,9 +6843,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
@@ -6697,9 +6855,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
@@ -6709,9 +6867,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"

View File

@@ -77,6 +77,7 @@ once_cell = { workspace = true }
percent-encoding = "2.3"
parking_lot = "0.12"
pgp = { version = "0.13.2", default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = "0.36"
quoted_printable = "0.5"
@@ -89,6 +90,7 @@ serde_json = { workspace = true }
serde_urlencoded = "0.7.1"
serde = { workspace = true, features = ["derive"] }
sha-1 = "0.10"
shadowsocks = { version = "1.20.2", default-features = false, features = ["aead-cipher-2022"] }
smallvec = "1.13.2"
strum = "0.26"
strum_macros = "0.26"

View File

@@ -403,11 +403,8 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `send_port` = SMTP-port, guessed if left out
* - `send_security`= SMTP-socket, one of @ref DC_SOCKET, defaults to #DC_SOCKET_AUTO
* - `server_flags` = IMAP-/SMTP-flags as a combination of @ref DC_LP flags, guessed if left out
* - `socks5_enabled` = SOCKS5 enabled
* - `socks5_host` = SOCKS5 proxy server host
* - `socks5_port` = SOCKS5 proxy server port
* - `socks5_user` = SOCKS5 proxy username
* - `socks5_password` = SOCKS5 proxy password
* - `proxy_enabled` = Proxy enabled. Disabled by default.
* - `proxy_url` = Proxy URL. May contain multiple URLs separated by newline, but only the first one is used.
* - `imap_certificate_checks` = how to check IMAP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `smtp_certificate_checks` = deprecated option, should be set to the same value as `imap_certificate_checks` but ignored by the new core
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way e.g. using CC, defaults to empty

View File

@@ -4537,19 +4537,16 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
let addr = to_string_lossy(addr);
let ctx = &*context;
let socks5_enabled = block_on(async move {
ctx.get_config_bool(config::Config::Socks5Enabled)
.await
.context("Can't get config")
.log_err(ctx)
});
let proxy_enabled = block_on(ctx.get_config_bool(config::Config::ProxyEnabled))
.context("Can't get config")
.log_err(ctx);
match socks5_enabled {
Ok(socks5_enabled) => {
match proxy_enabled {
Ok(proxy_enabled) => {
match block_on(provider::get_provider_info_by_addr(
ctx,
addr.as_str(),
socks5_enabled,
proxy_enabled,
))
.log_err(ctx)
.unwrap_or_default()

View File

@@ -321,12 +321,12 @@ impl CommandApi {
) -> Result<Option<ProviderInfo>> {
let ctx = self.get_context(account_id).await?;
let socks5_enabled = ctx
.get_config_bool(deltachat::config::Config::Socks5Enabled)
let proxy_enabled = ctx
.get_config_bool(deltachat::config::Config::ProxyEnabled)
.await?;
let provider_info =
get_provider_info(&ctx, email.split('@').last().unwrap_or(""), socks5_enabled).await;
get_provider_info(&ctx, email.split('@').last().unwrap_or(""), proxy_enabled).await;
Ok(ProviderInfo::from_dc_type(provider_info))
}

View File

@@ -83,7 +83,7 @@ mod tests {
assert_eq!(result, response.to_owned());
}
{
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":"","socks5_enabled":"0","socks5_host":"","socks5_port":"","socks5_user":"","socks5_password":""}]}"#;
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":""}]}"#;
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
session.handle_incoming(request).await;
let result = receiver.recv().await?;

View File

@@ -1249,10 +1249,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"providerinfo" => {
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
let socks5_enabled = context
.get_config_bool(config::Config::Socks5Enabled)
let proxy_enabled = context
.get_config_bool(config::Config::ProxyEnabled)
.await?;
match provider::get_provider_info(&context, arg1, socks5_enabled).await {
match provider::get_provider_info(&context, arg1, proxy_enabled).await {
Some(info) => {
println!("Information for provider belonging to {arg1}:");
println!("status: {}", info.status as u32);

View File

@@ -433,7 +433,7 @@ def test_provider_info(rpc) -> None:
assert provider_info["id"] == "gmail"
# Disable MX record resolution.
rpc.set_config(account_id, "socks5_enabled", "1")
rpc.set_config(account_id, "proxy_enabled", "1")
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info is None

View File

@@ -69,7 +69,7 @@ skip = [
{ name = "windows-core", version = "<0.54.0" },
{ name = "windows_i686_gnu", version = "<0.52" },
{ name = "windows_i686_msvc", version = "<0.52" },
{ name = "windows-sys", version = "<0.52" },
{ name = "windows-sys", version = "<0.59" },
{ name = "windows-targets", version = "<0.52" },
{ name = "windows", version = "<0.54.0" },
{ name = "windows_x86_64_gnullvm", version = "<0.52" },

View File

@@ -271,7 +271,7 @@ describe('Basic offline Tests', function () {
'sync_msgs',
'sentbox_watch',
'show_emails',
'socks5_enabled',
'proxy_enabled',
'sqlite_version',
'uptime',
'used_account_settings',

View File

@@ -91,21 +91,43 @@ pub enum Config {
/// Should not be extended in the future, create new config keys instead.
ServerFlags,
/// True if proxy is enabled.
///
/// Can be used to disable proxy without erasing known URLs.
ProxyEnabled,
/// Proxy URL.
///
/// Supported URLs schemes are `socks5://` (SOCKS5) and `ss://` (Shadowsocks).
///
/// May contain multiple URLs separated by newline, in which case the first one is used.
ProxyUrl,
/// True if SOCKS5 is enabled.
///
/// Can be used to disable SOCKS5 without erasing SOCKS5 configuration.
///
/// Deprecated in favor of `ProxyEnabled`.
Socks5Enabled,
/// SOCKS5 proxy server hostname or address.
///
/// Deprecated in favor of `ProxyUrl`.
Socks5Host,
/// SOCKS5 proxy server port.
///
/// Deprecated in favor of `ProxyUrl`.
Socks5Port,
/// SOCKS5 proxy server username.
///
/// Deprecated in favor of `ProxyUrl`.
Socks5User,
/// SOCKS5 proxy server password.
///
/// Deprecated in favor of `ProxyUrl`.
Socks5Password,
/// Own name to use in the `From:` field when sending messages.
@@ -638,6 +660,7 @@ impl Context {
fn check_config(key: Config, value: Option<&str>) -> Result<()> {
match key {
Config::Socks5Enabled
| Config::ProxyEnabled
| Config::BccSelf
| Config::E2eeEnabled
| Config::MdnsEnabled

View File

@@ -196,8 +196,8 @@ async fn get_configured_param(
param.smtp.password.clone()
};
let socks5_config = param.socks5_config.clone();
let socks5_enabled = socks5_config.is_some();
let proxy_config = param.proxy_config.clone();
let proxy_enabled = proxy_config.is_some();
let mut addr = param.addr.clone();
if param.oauth2 {
@@ -240,7 +240,7 @@ async fn get_configured_param(
"checking internal provider-info for offline autoconfig"
);
provider = provider::get_provider_info(ctx, &param_domain, socks5_enabled).await;
provider = provider::get_provider_info(ctx, &param_domain, proxy_enabled).await;
if let Some(provider) = provider {
if provider.server.is_empty() {
info!(ctx, "Offline autoconfig found, but no servers defined.");
@@ -356,7 +356,7 @@ async fn get_configured_param(
.collect(),
smtp_user: param.smtp.user.clone(),
smtp_password,
socks5_config: param.socks5_config.clone(),
proxy_config: param.proxy_config.clone(),
provider,
certificate_checks: match param.certificate_checks {
EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
@@ -388,7 +388,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
let smtp_param = configured_param.smtp.clone();
let smtp_password = configured_param.smtp_password.clone();
let smtp_addr = configured_param.addr.clone();
let smtp_socks5 = configured_param.socks5_config.clone();
let proxy_config = configured_param.proxy_config.clone();
let smtp_config_task = task::spawn(async move {
let mut smtp = Smtp::new();
@@ -396,7 +396,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
&context_smtp,
&smtp_param,
&smtp_password,
&smtp_socks5,
&proxy_config,
&smtp_addr,
strict_tls,
configured_param.oauth2,
@@ -414,7 +414,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
let mut imap = Imap::new(
configured_param.imap.clone(),
configured_param.imap_password.clone(),
configured_param.socks5_config.clone(),
configured_param.proxy_config.clone(),
&configured_param.addr,
strict_tls,
configured_param.oauth2,

View File

@@ -726,7 +726,7 @@ impl Context {
let request_msgs = message::get_request_msg_cnt(self).await;
let contacts = Contact::get_real_cnt(self).await?;
let is_configured = self.get_config_int(Config::Configured).await?;
let socks5_enabled = self.get_config_int(Config::Socks5Enabled).await?;
let proxy_enabled = self.get_config_int(Config::ProxyEnabled).await?;
let dbversion = self
.sql
.get_raw_config_int("dbversion")
@@ -807,7 +807,7 @@ impl Context {
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert("is_configured", is_configured.to_string());
res.insert("socks5_enabled", socks5_enabled.to_string());
res.insert("proxy_enabled", proxy_enabled.to_string());
res.insert("entered_account_settings", l.to_string());
res.insert("used_account_settings", l2);
@@ -1693,6 +1693,8 @@ mod tests {
"server_flags",
"skip_start_messages",
"smtp_certificate_checks",
"proxy_url", // May contain passwords, don't leak it to the logs.
"socks5_enabled", // SOCKS5 options are deprecated.
"socks5_host",
"socks5_port",
"socks5_user",

View File

@@ -37,12 +37,12 @@ use crate::login_param::{
};
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId, Viewtype};
use crate::mimeparser;
use crate::net::proxy::ProxyConfig;
use crate::oauth2::get_oauth2_access_token;
use crate::receive_imf::{
from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner, ReceivedMsg,
};
use crate::scheduler::connectivity::ConnectivityStore;
use crate::socks::Socks5Config;
use crate::sql;
use crate::stock_str;
use crate::tools::{self, create_id, duration_to_str};
@@ -80,8 +80,9 @@ pub(crate) struct Imap {
/// Password.
password: String,
/// SOCKS 5 configuration.
socks5_config: Option<Socks5Config>,
/// Proxy configuration.
proxy_config: Option<ProxyConfig>,
strict_tls: bool,
oauth2: bool,
@@ -237,7 +238,7 @@ impl Imap {
pub fn new(
lp: Vec<ConfiguredServerLoginParam>,
password: String,
socks5_config: Option<Socks5Config>,
proxy_config: Option<ProxyConfig>,
addr: &str,
strict_tls: bool,
oauth2: bool,
@@ -248,7 +249,7 @@ impl Imap {
addr: addr.to_string(),
lp,
password,
socks5_config,
proxy_config,
strict_tls,
oauth2,
login_failed_once: false,
@@ -271,7 +272,7 @@ impl Imap {
let imap = Self::new(
param.imap.clone(),
param.imap_password.clone(),
param.socks5_config.clone(),
param.proxy_config.clone(),
&param.addr,
param.strict_tls(),
param.oauth2,
@@ -336,7 +337,7 @@ impl Imap {
let connection_candidate = lp.connection.clone();
let client = match Client::connect(
context,
self.socks5_config.clone(),
self.proxy_config.clone(),
self.strict_tls,
connection_candidate,
)

View File

@@ -4,7 +4,6 @@ use std::ops::{Deref, DerefMut};
use anyhow::{Context as _, Result};
use async_imap::Client as ImapClient;
use async_imap::Session as ImapSession;
use fast_socks5::client::Socks5Stream;
use tokio::io::BufWriter;
use super::capabilities::Capabilities;
@@ -12,12 +11,12 @@ use super::session::Session;
use crate::context::Context;
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionStream;
use crate::net::tls::wrap_tls;
use crate::net::{
connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
};
use crate::socks::Socks5Config;
use crate::tools::time;
#[derive(Debug)]
@@ -157,25 +156,25 @@ impl Client {
pub async fn connect(
context: &Context,
socks5_config: Option<Socks5Config>,
proxy_config: Option<ProxyConfig>,
strict_tls: bool,
candidate: ConnectionCandidate,
) -> Result<Self> {
let host = &candidate.host;
let port = candidate.port;
let security = candidate.security;
if let Some(socks5_config) = socks5_config {
if let Some(proxy_config) = proxy_config {
let client = match security {
ConnectionSecurity::Tls => {
Client::connect_secure_socks5(context, host, port, strict_tls, socks5_config)
Client::connect_secure_proxy(context, host, port, strict_tls, proxy_config)
.await?
}
ConnectionSecurity::Starttls => {
Client::connect_starttls_socks5(context, host, port, socks5_config, strict_tls)
Client::connect_starttls_proxy(context, host, port, proxy_config, strict_tls)
.await?
}
ConnectionSecurity::Plain => {
Client::connect_insecure_socks5(context, host, port, socks5_config).await?
Client::connect_insecure_proxy(context, host, port, proxy_config).await?
}
};
Ok(client)
@@ -249,17 +248,17 @@ impl Client {
Ok(client)
}
async fn connect_secure_socks5(
async fn connect_secure_proxy(
context: &Context,
domain: &str,
port: u16,
strict_tls: bool,
socks5_config: Socks5Config,
proxy_config: ProxyConfig,
) -> Result<Self> {
let socks5_stream = socks5_config
let proxy_stream = proxy_config
.connect(context, domain, port, strict_tls)
.await?;
let tls_stream = wrap_tls(strict_tls, domain, alpn(port), socks5_stream).await?;
let tls_stream = wrap_tls(strict_tls, domain, alpn(port), proxy_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
@@ -270,14 +269,14 @@ impl Client {
Ok(client)
}
async fn connect_insecure_socks5(
async fn connect_insecure_proxy(
context: &Context,
domain: &str,
port: u16,
socks5_config: Socks5Config,
proxy_config: ProxyConfig,
) -> Result<Self> {
let socks5_stream = socks5_config.connect(context, domain, port, false).await?;
let buffered_stream = BufWriter::new(socks5_stream);
let proxy_stream = proxy_config.connect(context, domain, port, false).await?;
let buffered_stream = BufWriter::new(proxy_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
let _greeting = client
@@ -287,20 +286,20 @@ impl Client {
Ok(client)
}
async fn connect_starttls_socks5(
async fn connect_starttls_proxy(
context: &Context,
hostname: &str,
port: u16,
socks5_config: Socks5Config,
proxy_config: ProxyConfig,
strict_tls: bool,
) -> Result<Self> {
let socks5_stream = socks5_config
let proxy_stream = proxy_config
.connect(context, hostname, port, strict_tls)
.await?;
// Run STARTTLS command and convert the client back into a stream.
let buffered_socks5_stream = BufWriter::new(socks5_stream);
let mut client = ImapClient::new(buffered_socks5_stream);
let buffered_proxy_stream = BufWriter::new(proxy_stream);
let mut client = ImapClient::new(buffered_proxy_stream);
let _greeting = client
.read_response()
.await
@@ -309,10 +308,10 @@ impl Client {
.run_command_and_check_ok("STARTTLS", None)
.await
.context("STARTTLS command failed")?;
let buffered_socks5_stream = client.into_inner();
let socks5_stream: Socks5Stream<_> = buffered_socks5_stream.into_inner();
let buffered_proxy_stream = client.into_inner();
let proxy_stream = buffered_proxy_stream.into_inner();
let tls_stream = wrap_tls(strict_tls, hostname, &[], socks5_stream)
let tls_stream = wrap_tls(strict_tls, hostname, &[], proxy_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufWriter::new(tls_stream);

View File

@@ -84,7 +84,6 @@ mod scheduler;
pub mod securejoin;
mod simplify;
mod smtp;
mod socks;
pub mod stock_str;
mod sync;
mod timesmearing;

View File

@@ -11,8 +11,8 @@ use crate::configure::server_params::{expand_param_vector, ServerParams};
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
use crate::context::Context;
use crate::net::load_connection_timestamp;
use crate::net::proxy::ProxyConfig;
use crate::provider::{Protocol, Provider, Socket, UsernamePattern};
use crate::socks::Socks5Config;
use crate::sql::Sql;
/// User-entered setting for certificate checks.
@@ -116,7 +116,8 @@ pub struct EnteredLoginParam {
/// invalid hostnames
pub certificate_checks: EnteredCertificateChecks,
pub socks5_config: Option<Socks5Config>,
/// Proxy configuration.
pub proxy_config: Option<ProxyConfig>,
pub oauth2: bool,
}
@@ -195,7 +196,7 @@ impl EnteredLoginParam {
.unwrap_or_default();
let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2);
let socks5_config = Socks5Config::from_database(&context.sql).await?;
let proxy_config = ProxyConfig::load(context).await?;
Ok(EnteredLoginParam {
addr,
@@ -214,7 +215,7 @@ impl EnteredLoginParam {
password: send_pw,
},
certificate_checks,
socks5_config,
proxy_config,
oauth2,
})
}
@@ -380,7 +381,8 @@ pub struct ConfiguredLoginParam {
pub smtp_password: String,
pub socks5_config: Option<Socks5Config>,
/// Proxy configuration.
pub proxy_config: Option<ProxyConfig>,
pub provider: Option<&'static Provider>,
@@ -679,7 +681,7 @@ impl ConfiguredLoginParam {
}];
}
let socks5_config = Socks5Config::from_database(&context.sql).await?;
let proxy_config = ProxyConfig::load(context).await?;
Ok(Some(ConfiguredLoginParam {
addr,
@@ -691,7 +693,7 @@ impl ConfiguredLoginParam {
smtp_password: send_pw,
certificate_checks,
provider,
socks5_config,
proxy_config,
oauth2,
}))
}
@@ -778,7 +780,7 @@ impl ConfiguredLoginParam {
let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls);
match self.certificate_checks {
ConfiguredCertificateChecks::OldAutomatic => {
provider_strict_tls.unwrap_or(self.socks5_config.is_some())
provider_strict_tls.unwrap_or(self.proxy_config.is_some())
}
ConfiguredCertificateChecks::Automatic => provider_strict_tls.unwrap_or(true),
ConfiguredCertificateChecks::Strict => true,
@@ -863,8 +865,8 @@ mod tests {
}],
smtp_user: "".to_string(),
smtp_password: "bar".to_string(),
// socks5_config is not saved by `save_to_database`, using default value
socks5_config: None,
// proxy_config is not saved by `save_to_database`, using default value
proxy_config: None,
provider: None,
certificate_checks: ConfiguredCertificateChecks::Strict,
oauth2: false,
@@ -967,7 +969,7 @@ mod tests {
],
smtp_user: "alice@posteo.de".to_string(),
smtp_password: "foobarbaz".to_string(),
socks5_config: None,
proxy_config: None,
provider: get_provider_by_id("posteo"),
certificate_checks: ConfiguredCertificateChecks::Strict,
oauth2: false,

View File

@@ -17,6 +17,7 @@ use crate::tools::time;
pub(crate) mod dns;
pub(crate) mod http;
pub(crate) mod proxy;
pub(crate) mod session;
pub(crate) mod tls;

View File

@@ -8,9 +8,9 @@ use mime::Mime;
use serde::Serialize;
use crate::context::Context;
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionStream;
use crate::net::tls::wrap_tls;
use crate::socks::Socks5Config;
/// HTTP(S) GET response.
#[derive(Debug)]
@@ -43,7 +43,7 @@ where
{
let scheme = parsed_url.scheme_str().context("URL has no scheme")?;
let host = parsed_url.host().context("URL has no host")?;
let socks5_config_opt = Socks5Config::from_database(&context.sql).await?;
let proxy_config_opt = ProxyConfig::load(context).await?;
let stream: Box<dyn SessionStream> = match scheme {
"http" => {
@@ -54,11 +54,11 @@ where
// better resolve from scratch each time to prevent
// cache poisoning attacks from having lasting effects.
let load_cache = false;
if let Some(socks5_config) = socks5_config_opt {
let socks5_stream = socks5_config
if let Some(proxy_config) = proxy_config_opt {
let proxy_stream = proxy_config
.connect(context, host, port, load_cache)
.await?;
Box::new(socks5_stream)
Box::new(proxy_stream)
} else {
let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?;
Box::new(tcp_stream)
@@ -69,11 +69,11 @@ where
let load_cache = true;
let strict_tls = true;
if let Some(socks5_config) = socks5_config_opt {
let socks5_stream = socks5_config
if let Some(proxy_config) = proxy_config_opt {
let proxy_stream = proxy_config
.connect(context, host, port, load_cache)
.await?;
let tls_stream = wrap_tls(strict_tls, host, &[], socks5_stream).await?;
let tls_stream = wrap_tls(strict_tls, host, &[], proxy_stream).await?;
Box::new(tls_stream)
} else {
let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?;

435
src/net/proxy.rs Normal file
View File

@@ -0,0 +1,435 @@
//! # Proxy support.
//!
//! Delta Chat supports SOCKS5 and Shadowsocks protocols.
use std::fmt;
use std::pin::Pin;
use anyhow::{format_err, Context as _, Result};
use fast_socks5::client::Socks5Stream;
use fast_socks5::util::target_addr::ToTargetAddr;
use fast_socks5::AuthenticationMethod;
use fast_socks5::Socks5Command;
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
use pin_project::pin_project;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::TcpStream;
use tokio_io_timeout::TimeoutStream;
use crate::config::Config;
use crate::context::Context;
use crate::net::connect_tcp;
use crate::net::session::SessionStream;
use crate::sql::Sql;
/// Default SOCKS5 port according to [RFC 1928](https://tools.ietf.org/html/rfc1928).
pub const DEFAULT_SOCKS_PORT: u16 = 1080;
#[derive(Debug, Clone)]
pub struct ShadowsocksConfig {
pub server_config: shadowsocks::config::ServerConfig,
}
impl PartialEq for ShadowsocksConfig {
fn eq(&self, other: &Self) -> bool {
self.server_config.to_url() == other.server_config.to_url()
}
}
impl Eq for ShadowsocksConfig {}
/// Wrapper for Shadowsocks stream implementing
/// `Debug` and `SessionStream`.
///
/// Passes `AsyncRead` and `AsyncWrite` traits through.
#[pin_project]
pub(crate) struct ShadowsocksStream<S> {
#[pin]
pub(crate) stream: shadowsocks::ProxyClientStream<S>,
}
impl<S> std::fmt::Debug for ShadowsocksStream<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ShadowsocksStream")
}
}
impl<S> AsyncRead for ShadowsocksStream<S>
where
S: AsyncRead + AsyncWrite + Unpin,
{
fn poll_read(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<std::io::Result<()>> {
self.project().stream.poll_read(cx, buf)
}
}
impl<S> AsyncWrite for ShadowsocksStream<S>
where
S: AsyncRead + AsyncWrite + Unpin,
{
fn poll_write(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<Result<usize, std::io::Error>> {
self.project().stream.poll_write(cx, buf)
}
fn poll_flush(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), std::io::Error>> {
self.project().stream.poll_flush(cx)
}
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), std::io::Error>> {
self.project().stream.poll_shutdown(cx)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Socks5Config {
pub host: String,
pub port: u16,
pub user_password: Option<(String, String)>,
}
impl Socks5Config {
async fn connect(
&self,
context: &Context,
target_host: &str,
target_port: u16,
load_dns_cache: bool,
) -> Result<Socks5Stream<Pin<Box<TimeoutStream<TcpStream>>>>> {
let tcp_stream = connect_tcp(context, &self.host, self.port, load_dns_cache)
.await
.context("Failed to connect to SOCKS5 proxy")?;
let authentication_method = if let Some((username, password)) = self.user_password.as_ref()
{
Some(AuthenticationMethod::Password {
username: username.into(),
password: password.into(),
})
} else {
None
};
let mut socks_stream =
Socks5Stream::use_stream(tcp_stream, authentication_method, Default::default()).await?;
let target_addr = (target_host, target_port).to_target_addr()?;
socks_stream
.request(Socks5Command::TCPConnect, target_addr)
.await?;
Ok(socks_stream)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProxyConfig {
Socks5(Socks5Config),
Shadowsocks(ShadowsocksConfig),
}
impl ProxyConfig {
/// Creates a new proxy configuration by parsing given proxy URL.
fn from_url(url: &str) -> Result<Self> {
let url = url::Url::parse(url).context("Cannot parse proxy URL")?;
match url.scheme() {
"ss" => {
let server_config = shadowsocks::config::ServerConfig::from_url(url.as_str())?;
let shadowsocks_config = ShadowsocksConfig { server_config };
Ok(Self::Shadowsocks(shadowsocks_config))
}
// Because of `curl` convention,
// `socks5` URL scheme may be expected to resolve domain names locally
// with `socks5h` URL scheme meaning that hostnames are passed to the proxy.
// Resolving hostnames locally is not supported
// in Delta Chat when using a proxy
// to prevent DNS leaks.
// Because of this we do not distinguish
// between `socks5` and `socks5h`.
"socks5" => {
let host = url
.host_str()
.context("socks5 URL has no host")?
.to_string();
let port = url.port().unwrap_or(DEFAULT_SOCKS_PORT);
let user_password = if let Some(password) = url.password() {
let username = percent_encoding::percent_decode_str(url.username())
.decode_utf8()
.context("SOCKS5 username is not a valid UTF-8")?
.to_string();
let password = percent_encoding::percent_decode_str(password)
.decode_utf8()
.context("SOCKS5 password is not a valid UTF-8")?
.to_string();
Some((username, password))
} else {
None
};
let socks5_config = Socks5Config {
host,
port,
user_password,
};
Ok(Self::Socks5(socks5_config))
}
scheme => Err(format_err!("Unknown URL scheme {scheme:?}")),
}
}
/// Migrates legacy `socks5_host`, `socks5_port`, `socks5_user` and `socks5_password`
/// config into `proxy_url` if `proxy_url` is unset or empty.
///
/// Unsets `socks5_host`, `socks5_port`, `socks5_user` and `socks5_password` in any case.
async fn migrate_socks_config(sql: &Sql) -> Result<()> {
if sql.get_raw_config("proxy_url").await?.is_none() {
// Load legacy SOCKS5 settings.
if let Some(host) = sql
.get_raw_config("socks5_host")
.await?
.filter(|s| !s.is_empty())
{
let port: u16 = sql
.get_raw_config_int("socks5_port")
.await?
.unwrap_or(DEFAULT_SOCKS_PORT.into()) as u16;
let user = sql.get_raw_config("socks5_user").await?.unwrap_or_default();
let pass = sql
.get_raw_config("socks5_password")
.await?
.unwrap_or_default();
let mut proxy_url = "socks5://".to_string();
if !pass.is_empty() {
proxy_url += &percent_encode(user.as_bytes(), NON_ALPHANUMERIC).to_string();
proxy_url += ":";
proxy_url += &percent_encode(pass.as_bytes(), NON_ALPHANUMERIC).to_string();
proxy_url += "@";
};
proxy_url += &host;
proxy_url += ":";
proxy_url += &port.to_string();
sql.set_raw_config("proxy_url", Some(&proxy_url)).await?;
} else {
sql.set_raw_config("proxy_url", Some("")).await?;
}
let socks5_enabled = sql.get_raw_config("socks5_enabled").await?;
sql.set_raw_config("proxy_enabled", socks5_enabled.as_deref())
.await?;
}
sql.set_raw_config("socks5_enabled", None).await?;
sql.set_raw_config("socks5_host", None).await?;
sql.set_raw_config("socks5_port", None).await?;
sql.set_raw_config("socks5_user", None).await?;
sql.set_raw_config("socks5_password", None).await?;
Ok(())
}
/// Reads proxy configuration from the database.
pub async fn load(context: &Context) -> Result<Option<Self>> {
Self::migrate_socks_config(&context.sql)
.await
.context("Failed to migrate legacy SOCKS config")?;
let enabled = context.get_config_bool(Config::ProxyEnabled).await?;
if !enabled {
return Ok(None);
}
let proxy_url = context
.get_config(Config::ProxyUrl)
.await?
.unwrap_or_default();
let proxy_url = proxy_url
.split_once('\n')
.map_or(proxy_url.clone(), |(first_url, _rest)| {
first_url.to_string()
});
let proxy_config = Self::from_url(&proxy_url).context("Failed to parse proxy URL")?;
Ok(Some(proxy_config))
}
/// If `load_dns_cache` is true, loads cached DNS resolution results.
/// Use this only if the connection is going to be protected with TLS checks.
pub async fn connect(
&self,
context: &Context,
target_host: &str,
target_port: u16,
load_dns_cache: bool,
) -> Result<Box<dyn SessionStream>> {
match self {
ProxyConfig::Socks5(socks5_config) => {
let socks5_stream = socks5_config
.connect(context, target_host, target_port, load_dns_cache)
.await?;
Ok(Box::new(socks5_stream))
}
ProxyConfig::Shadowsocks(ShadowsocksConfig { server_config }) => {
let shadowsocks_context = shadowsocks::context::Context::new_shared(
shadowsocks::config::ServerType::Local,
);
let tcp_stream = {
let server_addr = server_config.addr();
let host = server_addr.host();
let port = server_addr.port();
connect_tcp(context, &host, port, load_dns_cache)
.await
.context("Failed to connect to Shadowsocks proxy")?
};
let proxy_client_stream = shadowsocks::ProxyClientStream::from_stream(
shadowsocks_context,
tcp_stream,
server_config,
(target_host.to_string(), target_port),
);
let shadowsocks_stream = ShadowsocksStream {
stream: proxy_client_stream,
};
Ok(Box::new(shadowsocks_stream))
}
}
}
}
impl fmt::Display for Socks5Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"host:{},port:{},user_password:{}",
self.host,
self.port,
if let Some(user_password) = self.user_password.clone() {
format!("user: {}, password: ***", user_password.0)
} else {
"user: None".to_string()
}
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::test_utils::TestContext;
#[test]
fn test_socks5_url() {
let proxy_config = ProxyConfig::from_url("socks5://127.0.0.1:9050").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(),
port: 9050,
user_password: None
})
);
let proxy_config = ProxyConfig::from_url("socks5://foo:bar@127.0.0.1:9150").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(),
port: 9150,
user_password: Some(("foo".to_string(), "bar".to_string()))
})
);
let proxy_config = ProxyConfig::from_url("socks5://%66oo:b%61r@127.0.0.1:9150").unwrap();
assert_eq!(
proxy_config,
ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(),
port: 9150,
user_password: Some(("foo".to_string(), "bar".to_string()))
})
);
}
#[test]
fn test_shadowsocks_url() {
// Example URL from <https://shadowsocks.org/doc/sip002.html>.
let proxy_config =
ProxyConfig::from_url("ss://YWVzLTEyOC1nY206dGVzdA@192.168.100.1:8888#Example1")
.unwrap();
assert!(matches!(proxy_config, ProxyConfig::Shadowsocks(_)));
}
#[test]
fn test_invalid_proxy_url() {
assert!(ProxyConfig::from_url("foobar://127.0.0.1:9050").is_err());
assert!(ProxyConfig::from_url("abc").is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_socks5_migration() -> Result<()> {
let t = TestContext::new().await;
// Test that config is migrated on attempt to load even if disabled.
t.set_config(Config::Socks5Host, Some("127.0.0.1")).await?;
t.set_config(Config::Socks5Port, Some("9050")).await?;
let proxy_config = ProxyConfig::load(&t).await?;
// Even though proxy is not enabled, config should be migrated.
assert_eq!(proxy_config, None);
assert_eq!(
t.get_config(Config::ProxyUrl).await?.unwrap(),
"socks5://127.0.0.1:9050"
);
Ok(())
}
// Test SOCKS5 setting migration if proxy was never configured.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_socks5_migration_unconfigured() -> Result<()> {
let t = TestContext::new().await;
// Try to load config to trigger migration.
assert_eq!(ProxyConfig::load(&t).await?, None);
assert_eq!(t.get_config(Config::ProxyEnabled).await?, None);
assert_eq!(
t.get_config(Config::ProxyUrl).await?.unwrap(),
String::new()
);
Ok(())
}
// Test SOCKS5 setting migration if SOCKS5 host is empty.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_socks5_migration_empty() -> Result<()> {
let t = TestContext::new().await;
t.set_config(Config::Socks5Host, Some("")).await?;
// Try to load config to trigger migration.
assert_eq!(ProxyConfig::load(&t).await?, None);
assert_eq!(t.get_config(Config::ProxyEnabled).await?, None);
assert_eq!(
t.get_config(Config::ProxyUrl).await?.unwrap(),
String::new()
);
Ok(())
}
}

View File

@@ -1,3 +1,4 @@
use crate::net::proxy::ShadowsocksStream;
use async_native_tls::TlsStream;
use fast_socks5::client::Socks5Stream;
use std::pin::Pin;
@@ -44,6 +45,11 @@ impl<T: SessionStream> SessionStream for Socks5Stream<T> {
self.get_socket_mut().set_read_timeout(timeout)
}
}
impl<T: SessionStream> SessionStream for ShadowsocksStream<T> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.stream.get_mut().set_read_timeout(timeout)
}
}
/// Session stream with a read buffer.
pub(crate) trait SessionBufStream: SessionStream + AsyncBufRead {}

View File

@@ -62,8 +62,8 @@ pub async fn get_oauth2_url(
addr: &str,
redirect_uri: &str,
) -> Result<Option<String>> {
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
if let Some(oauth2) = Oauth2::from_address(context, addr, socks5_enabled).await {
let proxy_enabled = context.get_config_bool(Config::ProxyEnabled).await?;
if let Some(oauth2) = Oauth2::from_address(context, addr, proxy_enabled).await {
context
.sql
.set_raw_config("oauth2_pending_redirect_uri", Some(redirect_uri))
@@ -83,8 +83,8 @@ pub(crate) async fn get_oauth2_access_token(
code: &str,
regenerate: bool,
) -> Result<Option<String>> {
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
if let Some(oauth2) = Oauth2::from_address(context, addr, socks5_enabled).await {
let proxy_enabled = context.get_config_bool(Config::ProxyEnabled).await?;
if let Some(oauth2) = Oauth2::from_address(context, addr, proxy_enabled).await {
let lock = context.oauth2_mutex.lock().await;
// read generated token
@@ -232,8 +232,8 @@ pub(crate) async fn get_oauth2_addr(
addr: &str,
code: &str,
) -> Result<Option<String>> {
let socks5_enabled = context.get_config_bool(Config::Socks5Enabled).await?;
let oauth2 = match Oauth2::from_address(context, addr, socks5_enabled).await {
let proxy_enabled = context.get_config_bool(Config::ProxyEnabled).await?;
let oauth2 = match Oauth2::from_address(context, addr, proxy_enabled).await {
Some(o) => o,
None => return Ok(None),
};

View File

@@ -7,7 +7,7 @@ use anyhow::{anyhow, bail, ensure, Context as _, Result};
pub use dclogin_scheme::LoginOptions;
use deltachat_contact_tools::{addr_normalize, may_be_valid_addr, ContactAddress};
use once_cell::sync::Lazy;
use percent_encoding::percent_decode_str;
use percent_encoding::{percent_decode_str, percent_encode, NON_ALPHANUMERIC};
use serde::Deserialize;
use self::dclogin_scheme::configure_from_login_qr;
@@ -20,6 +20,7 @@ use crate::events::EventType;
use crate::key::Fingerprint;
use crate::message::Message;
use crate::net::http::post_empty;
use crate::net::proxy::DEFAULT_SOCKS_PORT;
use crate::peerstate::Peerstate;
use crate::token;
use crate::tools::validate_id;
@@ -541,16 +542,15 @@ fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
let url = url::Url::parse(qr).context("Invalid t.me/socks url")?;
const SOCKS5_DEFAULT_PORT: u16 = 1080;
let mut host: Option<String> = None;
let mut port: u16 = SOCKS5_DEFAULT_PORT;
let mut port: u16 = DEFAULT_SOCKS_PORT;
let mut user: Option<String> = None;
let mut pass: Option<String> = None;
for (key, value) in url.query_pairs() {
if key == "server" {
host = Some(value.to_string());
} else if key == "port" {
port = value.parse().unwrap_or(SOCKS5_DEFAULT_PORT);
port = value.parse().unwrap_or(DEFAULT_SOCKS_PORT);
} else if key == "user" {
user = Some(value.to_string());
} else if key == "pass" {
@@ -661,22 +661,33 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
user,
pass,
} => {
// disable proxy before changing settings to not use a combination of old and new
context
.set_config_bool(Config::Socks5Enabled, false)
.await?;
let mut proxy_url = "socks5://".to_string();
if let Some(pass) = pass {
proxy_url += &percent_encode(user.unwrap_or_default().as_bytes(), NON_ALPHANUMERIC)
.to_string();
proxy_url += ":";
proxy_url += &percent_encode(pass.as_bytes(), NON_ALPHANUMERIC).to_string();
proxy_url += "@";
};
proxy_url += &host;
proxy_url += ":";
proxy_url += &port.to_string();
context.set_config(Config::Socks5Host, Some(&host)).await?;
let old_proxy_url_value = context
.get_config(Config::ProxyUrl)
.await?
.unwrap_or_default();
let proxy_urls: Vec<&str> = std::iter::once(proxy_url.as_str())
.chain(
old_proxy_url_value
.split('\n')
.filter(|s| !s.is_empty() && *s != proxy_url),
)
.collect();
context
.set_config_u32(Config::Socks5Port, u32::from(port))
.set_config(Config::ProxyUrl, Some(&proxy_urls.join("\n")))
.await?;
context
.set_config(Config::Socks5User, user.as_deref())
.await?;
context
.set_config(Config::Socks5Password, pass.as_deref())
.await?;
context.set_config_bool(Config::Socks5Enabled, true).await?;
context.set_config_bool(Config::ProxyEnabled, true).await?;
}
Qr::WithdrawVerifyContact {
invitenumber,
@@ -1630,50 +1641,48 @@ mod tests {
async fn test_set_socks5_proxy_config_from_qr() -> Result<()> {
let t = TestContext::new().await;
assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, false);
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, false);
let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await;
assert!(res.is_ok());
assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, true);
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
assert_eq!(
t.get_config(Config::Socks5Host).await?,
Some("foo".to_string())
t.get_config(Config::ProxyUrl).await?,
Some("socks5://foo:666".to_string())
);
assert_eq!(t.get_config_u32(Config::Socks5Port).await?, 666);
assert_eq!(t.get_config(Config::Socks5User).await?, None);
assert_eq!(t.get_config(Config::Socks5Password).await?, None);
// make sure, user&password are reset when not specified in the URL
t.set_config(Config::Socks5User, Some("alice")).await?;
t.set_config(Config::Socks5Password, Some("secret")).await?;
// Test URL without port.
let res = set_config_from_qr(&t, "https://t.me/socks?server=1.2.3.4").await;
assert!(res.is_ok());
assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, true);
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
assert_eq!(
t.get_config(Config::Socks5Host).await?,
Some("1.2.3.4".to_string())
t.get_config(Config::ProxyUrl).await?,
Some("socks5://1.2.3.4:1080\nsocks5://foo:666".to_string())
);
assert_eq!(t.get_config_u32(Config::Socks5Port).await?, 1080);
assert_eq!(t.get_config(Config::Socks5User).await?, None);
assert_eq!(t.get_config(Config::Socks5Password).await?, None);
// make sure, user&password are set when specified in the URL
// Password is an URL-encoded "x&%$X".
let res =
set_config_from_qr(&t, "https://t.me/socks?server=jau&user=Da&pass=x%26%25%24X").await;
assert!(res.is_ok());
assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, true);
assert_eq!(
t.get_config(Config::Socks5Host).await?,
Some("jau".to_string())
t.get_config(Config::ProxyUrl).await?,
Some(
"socks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080\nsocks5://foo:666"
.to_string()
)
);
assert_eq!(t.get_config_u32(Config::Socks5Port).await?, 1080);
// Scanning existing proxy brings it to the top in the list.
let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await;
assert!(res.is_ok());
assert_eq!(t.get_config_bool(Config::ProxyEnabled).await?, true);
assert_eq!(
t.get_config(Config::Socks5User).await?,
Some("Da".to_string())
);
assert_eq!(
t.get_config(Config::Socks5Password).await?,
Some("x&%$X".to_string())
t.get_config(Config::ProxyUrl).await?,
Some(
"socks5://foo:666\nsocks5://Da:x%26%25%24X@jau:1080\nsocks5://1.2.3.4:1080"
.to_string()
)
);
Ok(())

View File

@@ -18,9 +18,9 @@ use crate::login_param::{ConfiguredLoginParam, ConfiguredServerLoginParam};
use crate::message::Message;
use crate::message::{self, MsgId};
use crate::mimefactory::MimeFactory;
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionBufStream;
use crate::scheduler::connectivity::ConnectivityStore;
use crate::socks::Socks5Config;
use crate::sql;
use crate::stock_str::unencrypted_email;
use crate::tools::{self, time_elapsed};
@@ -95,7 +95,7 @@ impl Smtp {
context,
&lp.smtp,
&lp.smtp_password,
&lp.socks5_config,
&lp.proxy_config,
&lp.addr,
lp.strict_tls(),
lp.oauth2,
@@ -110,7 +110,7 @@ impl Smtp {
context: &Context,
login_params: &[ConfiguredServerLoginParam],
password: &str,
socks5_config: &Option<Socks5Config>,
proxy_config: &Option<ProxyConfig>,
addr: &str,
strict_tls: bool,
oauth2: bool,
@@ -130,7 +130,7 @@ impl Smtp {
info!(context, "SMTP trying to connect to {}.", &lp.connection);
let transport = match connect::connect_and_auth(
context,
socks5_config,
proxy_config,
strict_tls,
lp.connection.clone(),
oauth2,

View File

@@ -9,13 +9,13 @@ use tokio::io::BufStream;
use crate::context::Context;
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
use crate::net::proxy::ProxyConfig;
use crate::net::session::SessionBufStream;
use crate::net::tls::wrap_tls;
use crate::net::{
connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
};
use crate::oauth2::get_oauth2_access_token;
use crate::socks::Socks5Config;
use crate::tools::time;
/// Converts port number to ALPN list.
@@ -31,7 +31,7 @@ fn alpn(port: u16) -> &'static [&'static str] {
#[allow(clippy::too_many_arguments)]
pub(crate) async fn connect_and_auth(
context: &Context,
socks5_config: &Option<Socks5Config>,
proxy_config: &Option<ProxyConfig>,
strict_tls: bool,
candidate: ConnectionCandidate,
oauth2: bool,
@@ -40,7 +40,7 @@ pub(crate) async fn connect_and_auth(
password: &str,
) -> Result<SmtpTransport<Box<dyn SessionBufStream>>> {
let session_stream =
connect_stream(context, socks5_config.clone(), strict_tls, candidate).await?;
connect_stream(context, proxy_config.clone(), strict_tls, candidate).await?;
let client = async_smtp::SmtpClient::new()
.smtp_utf8(true)
.without_greeting();
@@ -127,7 +127,7 @@ async fn connection_attempt(
/// to unify the result regardless of whether TLS or STARTTLS is used.
async fn connect_stream(
context: &Context,
socks5_config: Option<Socks5Config>,
proxy_config: Option<ProxyConfig>,
strict_tls: bool,
candidate: ConnectionCandidate,
) -> Result<Box<dyn SessionBufStream>> {
@@ -135,18 +135,17 @@ async fn connect_stream(
let port = candidate.port;
let security = candidate.security;
if let Some(socks5_config) = socks5_config {
if let Some(proxy_config) = proxy_config {
let stream = match security {
ConnectionSecurity::Tls => {
connect_secure_socks5(context, host, port, strict_tls, socks5_config.clone())
.await?
connect_secure_proxy(context, host, port, strict_tls, proxy_config.clone()).await?
}
ConnectionSecurity::Starttls => {
connect_starttls_socks5(context, host, port, strict_tls, socks5_config.clone())
connect_starttls_proxy(context, host, port, strict_tls, proxy_config.clone())
.await?
}
ConnectionSecurity::Plain => {
connect_insecure_socks5(context, host, port, socks5_config.clone()).await?
connect_insecure_proxy(context, host, port, proxy_config.clone()).await?
}
};
Ok(stream)
@@ -192,37 +191,37 @@ async fn skip_smtp_greeting<R: tokio::io::AsyncBufReadExt + Unpin>(stream: &mut
}
}
async fn connect_secure_socks5(
async fn connect_secure_proxy(
context: &Context,
hostname: &str,
port: u16,
strict_tls: bool,
socks5_config: Socks5Config,
proxy_config: ProxyConfig,
) -> Result<Box<dyn SessionBufStream>> {
let socks5_stream = socks5_config
let proxy_stream = proxy_config
.connect(context, hostname, port, strict_tls)
.await?;
let tls_stream = wrap_tls(strict_tls, hostname, alpn(port), socks5_stream).await?;
let tls_stream = wrap_tls(strict_tls, hostname, alpn(port), proxy_stream).await?;
let mut buffered_stream = BufStream::new(tls_stream);
skip_smtp_greeting(&mut buffered_stream).await?;
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
Ok(session_stream)
}
async fn connect_starttls_socks5(
async fn connect_starttls_proxy(
context: &Context,
hostname: &str,
port: u16,
strict_tls: bool,
socks5_config: Socks5Config,
proxy_config: ProxyConfig,
) -> Result<Box<dyn SessionBufStream>> {
let socks5_stream = socks5_config
let proxy_stream = proxy_config
.connect(context, hostname, port, strict_tls)
.await?;
// Run STARTTLS command and convert the client back into a stream.
let client = SmtpClient::new().smtp_utf8(true);
let transport = SmtpTransport::new(client, BufStream::new(socks5_stream)).await?;
let transport = SmtpTransport::new(client, BufStream::new(proxy_stream)).await?;
let tcp_stream = transport.starttls().await?.into_inner();
let tls_stream = wrap_tls(strict_tls, hostname, &[], tcp_stream)
.await
@@ -232,16 +231,14 @@ async fn connect_starttls_socks5(
Ok(session_stream)
}
async fn connect_insecure_socks5(
async fn connect_insecure_proxy(
context: &Context,
hostname: &str,
port: u16,
socks5_config: Socks5Config,
proxy_config: ProxyConfig,
) -> Result<Box<dyn SessionBufStream>> {
let socks5_stream = socks5_config
.connect(context, hostname, port, false)
.await?;
let mut buffered_stream = BufStream::new(socks5_stream);
let proxy_stream = proxy_config.connect(context, hostname, port, false).await?;
let mut buffered_stream = BufStream::new(proxy_stream);
skip_smtp_greeting(&mut buffered_stream).await?;
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
Ok(session_stream)

View File

@@ -1,101 +0,0 @@
//! # SOCKS5 support.
use std::fmt;
use std::pin::Pin;
use anyhow::Result;
use fast_socks5::client::{Config, Socks5Stream};
use fast_socks5::util::target_addr::ToTargetAddr;
use fast_socks5::AuthenticationMethod;
use fast_socks5::Socks5Command;
use tokio::net::TcpStream;
use tokio_io_timeout::TimeoutStream;
use crate::context::Context;
use crate::net::connect_tcp;
use crate::sql::Sql;
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct Socks5Config {
pub host: String,
pub port: u16,
pub user_password: Option<(String, String)>,
}
impl Socks5Config {
/// Reads SOCKS5 configuration from the database.
pub async fn from_database(sql: &Sql) -> Result<Option<Self>> {
let enabled = sql.get_raw_config_bool("socks5_enabled").await?;
if enabled {
let host = sql.get_raw_config("socks5_host").await?.unwrap_or_default();
let port: u16 = sql
.get_raw_config_int("socks5_port")
.await?
.unwrap_or_default() as u16;
let user = sql.get_raw_config("socks5_user").await?.unwrap_or_default();
let password = sql
.get_raw_config("socks5_password")
.await?
.unwrap_or_default();
let socks5_config = Self {
host,
port,
user_password: if !user.is_empty() {
Some((user, password))
} else {
None
},
};
Ok(Some(socks5_config))
} else {
Ok(None)
}
}
/// If `load_dns_cache` is true, loads cached DNS resolution results.
/// Use this only if the connection is going to be protected with TLS checks.
pub async fn connect(
&self,
context: &Context,
target_host: &str,
target_port: u16,
load_dns_cache: bool,
) -> Result<Socks5Stream<Pin<Box<TimeoutStream<TcpStream>>>>> {
let tcp_stream = connect_tcp(context, &self.host, self.port, load_dns_cache).await?;
let authentication_method = if let Some((username, password)) = self.user_password.as_ref()
{
Some(AuthenticationMethod::Password {
username: username.into(),
password: password.into(),
})
} else {
None
};
let mut socks_stream =
Socks5Stream::use_stream(tcp_stream, authentication_method, Config::default()).await?;
let target_addr = (target_host, target_port).to_target_addr()?;
socks_stream
.request(Socks5Command::TCPConnect, target_addr)
.await?;
Ok(socks_stream)
}
}
impl fmt::Display for Socks5Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"host:{},port:{},user_password:{}",
self.host,
self.port,
if let Some(user_password) = self.user_password.clone() {
format!("user: {}, password: ***", user_password.0)
} else {
"user: None".to_string()
}
)
}
}