diff --git a/.evergreen/config.yml b/.evergreen/config.yml index fb68dea9d..204f9a954 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -399,6 +399,35 @@ buildvariants: tasks: - happy-eyeballs-task-group + - name: socks5-proxy-no-tls + display_name: "SOCKS5 Proxy (No TLS)" + # patchable: false + run_on: + - rhel87-small + tasks: + - test-socks5-proxy + + - name: socks5-proxy-tls + display_name: "SOCKS5 Proxy (TLS)" + # patchable: false + run_on: + - rhel87-small + expansions: + SSL: ssl + tasks: + - test-socks5-proxy + + - name: socks5-proxy-openssl + display_name: "SOCKS5 Proxy (OpenSSL)" + # patchable: false + run_on: + - rhel87-small + expansions: + SSL: ssl + OPENSSL: true + tasks: + - test-socks5-proxy + #- name: graviton-legacy # display_name: "Graviton (legacy versions)" # run_on: @@ -1246,6 +1275,26 @@ tasks: include_expansions_in_env: - PROJECT_DIRECTORY + - name: test-socks5-proxy + commands: + - func: bootstrap mongo-orchestration + vars: + MONGODB_VERSION: rapid + TOPOLOGY: replica_set + - command: subprocess.exec + type: test + params: + working_dir: src + binary: bash + args: + - .evergreen/run-socks5-proxy-test.sh + include_expansions_in_env: + - PROJECT_DIRECTORY + - MONGODB_URI + - DRIVERS_TOOLS + - PYTHON3 + - OPENSSL + ############# # Functions # ############# diff --git a/.evergreen/run-socks5-proxy-test.sh b/.evergreen/run-socks5-proxy-test.sh new file mode 100644 index 000000000..eba5037ab --- /dev/null +++ b/.evergreen/run-socks5-proxy-test.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -o errexit + +source .evergreen/env.sh +source .evergreen/cargo-test.sh + +FEATURE_FLAGS+=("socks5-proxy") +CARGO_OPTIONS+=("--ignore-default-filter") + +if [ "$OPENSSL" = true ]; then + FEATURE_FLAGS+=("openssl-tls") +fi + +# with auth +$PYTHON3 $DRIVERS_TOOLS/.evergreen/socks5srv.py --port 1080 --auth "username:p4ssw0rd" --map "localhost:12345 to localhost:27017" & +AUTH_PID=$! +# without auth +$PYTHON3 $DRIVERS_TOOLS/.evergreen/socks5srv.py --port 1081 --map "localhost:12345 to localhost:27017" & +NOAUTH_PID=$! + +set +o errexit + +cargo_test socks5_proxy +cargo_test run_uri_options_spec_tests + +kill $AUTH_PID +kill $NOAUTH_PID + +exit ${CARGO_RESULT} diff --git a/Cargo.lock b/Cargo.lock index 403a38842..f6ababed7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -635,7 +635,7 @@ dependencies = [ "serde_bytes", "serde_json", "simdutf8", - "thiserror", + "thiserror 2.0.17", "time", "uuid", ] @@ -1120,7 +1120,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.0", +] + +[[package]] +name = "fast-socks5" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d09fe4a491909a716088083eeb5bcc25427330fdbcd4ecd3dfa5469b3da795df" +dependencies = [ + "anyhow", + "async-trait", + "log", + "thiserror 1.0.69", + "tokio", + "tokio-stream", ] [[package]] @@ -1406,7 +1420,7 @@ dependencies = [ "once_cell", "rand", "ring", - "thiserror", + "thiserror 2.0.17", "tinyvec", "tokio", "tracing", @@ -1429,7 +1443,7 @@ dependencies = [ "rand", "resolv-conf", "smallvec 1.15.0", - "thiserror", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -1616,7 +1630,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.1", "system-configuration", "tokio", "tower-service", @@ -2099,6 +2113,7 @@ dependencies = [ "cross-krb5", "derive-where", "derive_more", + "fast-socks5", "flate2", "function_name", "futures", @@ -2148,7 +2163,7 @@ dependencies = [ "stringprep", "strsim", "take_mut", - "thiserror", + "thiserror 2.0.17", "time", "tokio", "tokio-openssl", @@ -2339,7 +2354,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror", + "thiserror 2.0.17", "tracing", ] @@ -2355,7 +2370,7 @@ dependencies = [ "opentelemetry", "percent-encoding", "rand", - "thiserror", + "thiserror 2.0.17", "tokio", "tokio-stream", ] @@ -2573,8 +2588,8 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", - "thiserror", + "socket2 0.6.1", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -2595,7 +2610,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -2610,9 +2625,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -2865,7 +2880,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.0", ] [[package]] @@ -3343,7 +3358,16 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", ] [[package]] @@ -3352,7 +3376,18 @@ version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] diff --git a/README.md b/README.md index bdc3774ab..6d3d83650 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ features = ["sync"] | `text-indexes-unstable` | Enables support for text indexes in explicit encryption. This feature is in preview and should be used for experimental workloads only. This feature is unstable and its security is not guaranteed until released as Generally Available (GA). The GA version of this feature may not be backwards compatible with the preview version. | | `error-backtrace` | Capture backtraces in `Error` values. This can be slow, memory intensive, and very verbose. | | `bson-3` | Use version 3.x of the `bson` crate; for backwards compatibility, without this feature enabled `bson` 2.x is used. | +| `socks5-proxy` | Enable SOCKS5 proxy support. | ## Web Framework Examples diff --git a/benchmarks/Cargo.lock b/benchmarks/Cargo.lock index 4888a65a3..51488fb09 100644 --- a/benchmarks/Cargo.lock +++ b/benchmarks/Cargo.lock @@ -39,21 +39,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "ansi_term" version = "0.12.1" @@ -112,12 +97,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.22.1" @@ -163,15 +142,15 @@ version = "2.15.0" source = "git+https://github.com/mongodb/bson-rust?branch=2.15.x#f6f163095b5159ce175424b0e02f9bd7acfaddf2" dependencies = [ "ahash", - "base64 0.22.1", + "base64", "bitvec", "getrandom 0.2.15", "getrandom 0.3.3", "hex", - "indexmap 2.7.1", + "indexmap", "js-sys", "once_cell", - "rand 0.9.2", + "rand", "serde", "serde_bytes", "serde_json", @@ -212,19 +191,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chrono" -version = "0.4.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "num-traits", - "serde", - "windows-targets 0.52.6", -] - [[package]] name = "clap" version = "2.34.0" @@ -274,26 +240,44 @@ dependencies = [ ] [[package]] -name = "convert_case" -version = "0.4.0" +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" [[package]] -name = "core-foundation-sys" -version = "0.8.7" +name = "crossbeam-channel" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "crossbeam-epoch" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "libc", + "crossbeam-utils", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crunchy" version = "0.2.3" @@ -358,7 +342,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", - "serde", ] [[package]] @@ -385,15 +368,23 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.18" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ - "convert_case", "proc-macro2", "quote", - "rustc_version", "syn 2.0.96", + "unicode-xid", ] [[package]] @@ -595,12 +586,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.15.2" @@ -630,9 +615,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hickory-proto" -version = "0.24.2" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447afdcdb8afb9d0a852af6dc65d9b285ce720ed7a59e42a8bf2e931c67bc1b5" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" dependencies = [ "async-trait", "cfg-if", @@ -644,7 +629,8 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.8.5", + "rand", + "ring", "thiserror", "tinyvec", "tokio", @@ -654,18 +640,18 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.24.2" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2e2aba9c389ce5267d31cf1e4dace82390ae276b0b364ea55630b1fa1b44b4" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" dependencies = [ "cfg-if", "futures-util", "hickory-proto", "ipconfig", - "lru-cache", + "moka", "once_cell", "parking_lot", - "rand 0.8.5", + "rand", "resolv-conf", "smallvec", "thiserror", @@ -693,29 +679,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "iana-time-zone" -version = "0.1.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "icu_collections" version = "1.5.0" @@ -861,17 +824,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - [[package]] name = "indexmap" version = "2.7.1" @@ -879,8 +831,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", - "hashbrown 0.15.2", - "serde", + "hashbrown", ] [[package]] @@ -901,7 +852,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.8", "widestring", "windows-sys 0.48.0", "winreg", @@ -937,15 +888,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.169" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" - -[[package]] -name = "linked-hash-map" -version = "0.5.6" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "litemap" @@ -969,15 +914,6 @@ version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" -[[package]] -name = "lru-cache" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "macro_magic" version = "0.5.1" @@ -1068,6 +1004,24 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "moka" +version = "0.12.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "rustc_version", + "smallvec", + "tagptr", + "uuid", +] + [[package]] name = "mongocrypt" version = "0.3.1" @@ -1088,10 +1042,9 @@ source = "git+https://github.com/mongodb/libmongocrypt-rust.git?branch=main#1327 name = "mongodb" version = "3.3.0" dependencies = [ - "base64 0.13.1", - "bitflags 1.3.2", + "base64", + "bitflags 2.8.0", "bson", - "chrono", "derive-where", "derive_more", "futures-core", @@ -1107,24 +1060,26 @@ dependencies = [ "mongodb-internal-macros", "pbkdf2", "percent-encoding", - "rand 0.8.5", + "rand", "rustc_version_runtime", "rustls", "rustversion", "serde", + "serde_bytes", "serde_with", "sha1", "sha2", - "socket2", + "socket2 0.6.1", "stringprep", "strsim 0.11.1", "take_mut", "thiserror", "tokio", "tokio-rustls", + "tokio-util", "typed-builder", "uuid", - "webpki-roots 0.26.11", + "webpki-roots", ] [[package]] @@ -1143,15 +1098,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - [[package]] name = "num_enum" version = "0.5.11" @@ -1193,6 +1139,10 @@ name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "parking_lot" @@ -1244,6 +1194,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + [[package]] name = "powerfmt" version = "0.2.0" @@ -1305,35 +1261,14 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "rand_chacha", + "rand_core", ] [[package]] @@ -1343,16 +1278,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.15", + "rand_core", ] [[package]] @@ -1565,7 +1491,7 @@ version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" dependencies = [ - "indexmap 2.7.1", + "indexmap", "itoa", "memchr", "ryu", @@ -1578,16 +1504,9 @@ version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.7.1", "serde", "serde_derive", - "serde_json", "serde_with_macros", - "time", ] [[package]] @@ -1664,6 +1583,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "spin" version = "0.9.8" @@ -1738,6 +1667,12 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "take_mut" version = "0.2.2" @@ -1761,18 +1696,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.69" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.69" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -1856,7 +1791,7 @@ dependencies = [ "mio", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.8", "tokio-macros", "windows-sys 0.52.0", ] @@ -1901,6 +1836,7 @@ checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -1918,7 +1854,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.7.1", + "indexmap", "toml_datetime", "winnow", ] @@ -1930,21 +1866,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", -] - [[package]] name = "tracing-core" version = "0.1.33" @@ -1956,18 +1880,18 @@ dependencies = [ [[package]] name = "typed-builder" -version = "0.20.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd9d30e3a08026c78f246b173243cf07b3696d274debd26680773b6773c2afc7" +checksum = "398a3a3c918c96de527dc11e6e846cd549d4508030b8a33e1da12789c856b81a" dependencies = [ "typed-builder-macro", ] [[package]] name = "typed-builder-macro" -version = "0.20.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c36781cc0e46a83726d9879608e4cf6c2505237e263a8eb8c24502989cfdb28" +checksum = "0e48cea23f68d1f78eb7bc092881b6bb88d3d6b5b7e6234f6f9c911da1ffb221" dependencies = [ "proc-macro2", "quote", @@ -2019,6 +1943,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -2153,15 +2083,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.2", -] - [[package]] name = "webpki-roots" version = "1.0.2" @@ -2200,13 +2121,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-core" -version = "0.52.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" @@ -2235,6 +2153,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -2259,13 +2186,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "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]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -2278,6 +2222,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -2290,6 +2240,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -2302,12 +2258,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -2320,6 +2288,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -2332,6 +2306,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -2344,6 +2324,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -2356,6 +2342,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.5.40" diff --git a/driver/Cargo.toml b/driver/Cargo.toml index 37ca61f8a..3e3b725bb 100644 --- a/driver/Cargo.toml +++ b/driver/Cargo.toml @@ -33,6 +33,7 @@ rustls-tls = ["dep:rustls", "dep:tokio-rustls", "dep:webpki-roots"] openssl-tls = ["dep:openssl", "dep:openssl-probe", "dep:tokio-openssl"] dns-resolver = ["dep:hickory-resolver", "dep:hickory-proto"] cert-key-password = ["dep:pem", "dep:pkcs8"] +socks5-proxy = ["dep:fast-socks5"] # Enable support for MONGODB-AWS authentication. aws-auth = ["dep:aws-config", "dep:aws-credential-types", "dep:aws-sigv4", "dep:chrono", "dep:http"] @@ -87,6 +88,7 @@ chrono = { version = "0.4.32", default-features = false, optional = true, featur ] } derive_more = { version = "2", features = ["display", "from"] } derive-where = "1.2.7" +fast-socks5 = { version = "0.10", optional = true } flate2 = { version = "1.0", optional = true } futures-io = "0.3.21" futures-core = "0.3.14" diff --git a/driver/src/client/csfle.rs b/driver/src/client/csfle.rs index 7a93631f1..d2b8a5f16 100644 --- a/driver/src/client/csfle.rs +++ b/driver/src/client/csfle.rs @@ -72,6 +72,10 @@ impl ClientState { mongocryptd_opts, mongocryptd_client, aux_clients.metadata_client, + #[cfg(feature = "socks5-proxy")] + client.options().socks5_proxy.clone(), + #[cfg(not(feature = "socks5-proxy"))] + None, ) .await?; diff --git a/driver/src/client/csfle/state_machine.rs b/driver/src/client/csfle/state_machine.rs index 0cd12ca26..2dca106ef 100644 --- a/driver/src/client/csfle/state_machine.rs +++ b/driver/src/client/csfle/state_machine.rs @@ -5,7 +5,6 @@ use std::{ time::Duration, }; -use crate::bson::{rawdoc, Document, RawDocument, RawDocumentBuf}; use futures_util::{stream, TryStreamExt}; use mongocrypt::ctx::{Ctx, KmsCtx, KmsProviderType, State}; use rayon::ThreadPool; @@ -15,10 +14,11 @@ use tokio::{ }; use crate::{ + bson::{rawdoc, Document, RawDocument, RawDocumentBuf}, client::{csfle::options::KmsProvidersTlsOptions, options::ServerAddress, WeakClient}, error::{Error, Result}, operation::{raw_output::RawOutput, run_command::RunCommand}, - options::ReadConcern, + options::{ReadConcern, Socks5Proxy}, runtime::{process::Process, AsyncStream, TlsConfig}, Client, Namespace, @@ -37,6 +37,7 @@ pub(crate) struct CryptExecutor { metadata_client: Option, #[cfg(feature = "azure-kms")] azure: azure::ExecutorState, + proxy: Option, } impl CryptExecutor { @@ -60,6 +61,7 @@ impl CryptExecutor { metadata_client: None, #[cfg(feature = "azure-kms")] azure: azure::ExecutorState::new()?, + proxy: None, }) } @@ -70,6 +72,7 @@ impl CryptExecutor { mongocryptd_opts: Option, mongocryptd_client: Option, metadata_client: Option, + proxy: Option, ) -> Result { let mongocryptd = match mongocryptd_opts { Some(opts) => Some(Mongocryptd::new(opts).await?), @@ -79,6 +82,7 @@ impl CryptExecutor { exec.mongocryptd = mongocryptd; exec.mongocryptd_client = mongocryptd_client; exec.metadata_client = metadata_client; + exec.proxy = proxy; Ok(exec) } @@ -180,6 +184,7 @@ impl CryptExecutor { async fn execute( kms_ctx: &mut KmsCtx<'_>, tls_options: Option<&KmsProvidersTlsOptions>, + proxy: Option<&Socks5Proxy>, ) -> Result<()> { let endpoint = kms_ctx.endpoint()?; let addr = ServerAddress::parse(endpoint)?; @@ -189,7 +194,8 @@ impl CryptExecutor { .cloned() .unwrap_or_default(); let mut stream = - AsyncStream::connect(addr, Some(&TlsConfig::new(tls_options)?)).await?; + AsyncStream::connect(addr, Some(&TlsConfig::new(tls_options)?), proxy) + .await?; stream.write_all(kms_ctx.message()?).await?; let mut buf = vec![0]; while kms_ctx.bytes_needed() > 0 { @@ -220,8 +226,12 @@ impl CryptExecutor { tokio::time::sleep(Duration::from_micros(sleep_micros)).await; } - if let Err(error) = - execute(&mut kms_ctx, self.kms_providers.tls_options()).await + if let Err(error) = execute( + &mut kms_ctx, + self.kms_providers.tls_options(), + self.proxy.as_ref(), + ) + .await { if !kms_ctx.retry_failure() { return Err(error); diff --git a/driver/src/client/options.rs b/driver/src/client/options.rs index b2c6cdcaf..cadedfdb6 100644 --- a/driver/src/client/options.rs +++ b/driver/src/client/options.rs @@ -59,6 +59,10 @@ const TLS_INSECURE: &str = "tlsinsecure"; const TLS_ALLOW_INVALID_CERTIFICATES: &str = "tlsallowinvalidcertificates"; #[cfg(feature = "openssl-tls")] const TLS_ALLOW_INVALID_HOSTNAMES: &str = "tlsallowinvalidhostnames"; +const PROXY_HOST: &str = "proxyhost"; +const PROXY_PORT: &str = "proxyport"; +const PROXY_USERNAME: &str = "proxyusername"; +const PROXY_PASSWORD: &str = "proxypassword"; const URI_OPTIONS: &[&str] = &[ "appname", "authmechanism", @@ -75,6 +79,10 @@ const URI_OPTIONS: &[&str] = &[ "maxpoolsize", "minpoolsize", "maxconnecting", + PROXY_HOST, + PROXY_PORT, + PROXY_USERNAME, + PROXY_PASSWORD, "readconcernlevel", "readpreference", "readpreferencetags", @@ -407,6 +415,60 @@ pub struct ServerApi { pub deprecation_errors: Option, } +/// Configuration for connecting to a SOCKS5 proxy. +#[cfg(feature = "socks5-proxy")] +#[derive(Clone, Debug, Deserialize, PartialEq, TypedBuilder)] +#[builder(field_defaults(default, setter(into)))] +#[non_exhaustive] +pub struct Socks5Proxy { + /// The hostname or IP address on which the proxy is listening. + #[builder(!default)] + pub host: String, + + /// The port on which the proxy is listening. Defaults to 1080 if unset. + pub port: Option, + + /// A username/password pair to authenticate to the proxy. + pub authentication: Option<(String, String)>, +} + +/// Dummy struct for internal use. +#[cfg(not(feature = "socks5-proxy"))] +#[derive(Clone, Debug)] +pub(crate) struct Socks5Proxy; + +#[cfg(feature = "socks5-proxy")] +impl Socks5Proxy { + fn serialize( + proxy: &Option, + serializer: S, + ) -> std::result::Result + where + S: serde::Serializer, + { + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + struct Helper<'a> { + proxy_host: &'a String, + proxy_port: Option, + proxy_username: Option<&'a String>, + proxy_password: Option<&'a String>, + } + + if let Some(proxy) = proxy.as_ref() { + let helper = Helper { + proxy_host: &proxy.host, + proxy_port: proxy.port, + proxy_username: proxy.authentication.as_ref().map(|auth| &auth.0), + proxy_password: proxy.authentication.as_ref().map(|auth| &auth.1), + }; + helper.serialize(serializer) + } else { + serializer.serialize_none() + } + } +} + /// Contains the options that can be used to create a new [`Client`](../struct.Client.html). #[derive(Clone, Deserialize, TypedBuilder)] #[builder(field_defaults(default, setter(into)))] @@ -616,6 +678,10 @@ pub struct ClientOptions { #[cfg(feature = "opentelemetry")] pub tracing: Option, + /// Configuration for connecting to a SOCKS5 proxy. + #[cfg(feature = "socks5-proxy")] + pub socks5_proxy: Option, + /// Information from the SRV URI that generated these client options, if applicable. #[builder(setter(skip))] #[serde(skip)] @@ -748,6 +814,10 @@ impl Serialize for ClientOptions { srvmaxhosts: Option, srvservicename: &'a Option, + + #[cfg(feature = "socks5-proxy")] + #[serde(flatten, serialize_with = "Socks5Proxy::serialize")] + socks5proxy: &'a Option, } let client_options = ClientOptionsHelper { @@ -782,6 +852,8 @@ impl Serialize for ClientOptions { .transpose() .map_err(serde::ser::Error::custom)?, srvservicename: &self.srv_service_name, + #[cfg(feature = "socks5-proxy")] + socks5proxy: &self.socks5_proxy, }; client_options.serialize(serializer) @@ -977,6 +1049,11 @@ pub struct ConnectionString { /// Overrides the default "mongodb" service name for SRV lookup in both discovery and polling pub srv_service_name: Option, + /// Configuration for connecting to a SOCKS5 proxy. + #[cfg(feature = "socks5-proxy")] + #[serde(serialize_with = "Socks5Proxy::serialize")] + pub socks5_proxy: Option, + #[serde(serialize_with = "serde_util::serialize_duration_option_as_int_millis")] wait_queue_timeout: Option, tls_insecure: Option, @@ -995,6 +1072,14 @@ struct ConnectionStringParts { auth_mechanism_properties: Option, zlib_compression: Option, auth_source: Option, + #[cfg(feature = "socks5-proxy")] + proxy_host: Option, + #[cfg(feature = "socks5-proxy")] + proxy_port: Option, + #[cfg(feature = "socks5-proxy")] + proxy_username: Option, + #[cfg(feature = "socks5-proxy")] + proxy_password: Option, } /// Specification for mongodb server connections. @@ -1317,6 +1402,29 @@ impl ClientOptions { } } + #[cfg(feature = "socks5-proxy")] + { + if let Some(proxy) = self.socks5_proxy.as_ref() { + if self + .hosts + .iter() + .any(|address| !matches!(address, ServerAddress::Tcp { .. })) + { + return Err(Error::invalid_argument( + "cannot specify a non-TCP address when connected to a proxy host", + )); + } + + if let Some((username, password)) = proxy.authentication.as_ref() { + if username.is_empty() || password.is_empty() { + return Err(Error::invalid_argument( + "cannot specify an empty username or password for proxy host", + )); + } + } + } + } + Ok(()) } @@ -1664,6 +1772,44 @@ impl ConnectionString { conn_str.tls = Some(Tls::Enabled(Default::default())); } + #[cfg(feature = "socks5-proxy")] + { + if let Some(host) = parts.proxy_host { + let mut proxy = Socks5Proxy::builder().host(host).build(); + if let Some(port) = parts.proxy_port { + proxy.port = Some(port); + } + match (parts.proxy_username, parts.proxy_password) { + (Some(username), Some(password)) => { + proxy.authentication = Some((username, password)) + } + (None, None) => {} + _ => { + return Err(Error::invalid_argument( + "proxy username and password must both be specified as nonempty \ + strings or unset", + )); + } + } + conn_str.socks5_proxy = Some(proxy); + } else { + let error = |option: &str| { + Error::invalid_argument(format!( + "{option} cannot be set if {PROXY_HOST} is unspecified" + )) + }; + if parts.proxy_port.is_some() { + return Err(error(PROXY_PORT)); + } + if parts.proxy_username.is_some() { + return Err(error(PROXY_USERNAME)); + } + if parts.proxy_password.is_some() { + return Err(error(PROXY_PASSWORD)); + } + } + } + Ok(conn_str) } @@ -1704,6 +1850,8 @@ impl ConnectionString { srv_service_name: _, wait_queue_timeout, tls_insecure, + #[cfg(feature = "socks5-proxy")] + socks5_proxy, #[cfg(test)] original_uri: _, } = self; @@ -2005,6 +2153,19 @@ impl ConnectionString { opts.push_str(&format!("&srvMaxHosts={srv_max_hosts}")); } + #[cfg(feature = "socks5-proxy")] + if let Some(proxy) = socks5_proxy { + opts.push_str(&format!("&proxyHost={}", proxy.host)); + if let Some(port) = proxy.port { + opts.push_str(&format!("&proxyPort={port}")); + } + if let Some((username, password)) = proxy.authentication.as_ref() { + opts.push_str(&format!( + "&proxyUsername={username}&proxyPassword={password}" + )); + } + } + if !opts.is_empty() { opts.replace_range(0..1, "?"); // mark start of options res.push_str(&opts); @@ -2583,6 +2744,24 @@ impl ConnectionString { parts.zlib_compression = Some(i); } + #[cfg(feature = "socks5-proxy")] + PROXY_HOST => parts.proxy_host = Some(value.to_string()), + #[cfg(feature = "socks5-proxy")] + PROXY_PORT => { + let port = u16::from_str(value) + .map_err(|_| Error::invalid_argument(format!("invalid proxy port: {value}")))?; + parts.proxy_port = Some(port); + } + #[cfg(feature = "socks5-proxy")] + PROXY_USERNAME if !value.is_empty() => parts.proxy_username = Some(value.to_string()), + #[cfg(feature = "socks5-proxy")] + PROXY_PASSWORD if !value.is_empty() => parts.proxy_password = Some(value.to_string()), + #[cfg(not(feature = "socks5-proxy"))] + PROXY_HOST | PROXY_PORT | PROXY_USERNAME | PROXY_PASSWORD => { + return Err(Error::invalid_argument(format!( + "cannot specify {key} if socks5-proxy feature is not enabled" + ))); + } other => { let (jaro_winkler, option) = URI_OPTIONS.iter().fold((0.0, ""), |acc, option| { diff --git a/driver/src/client/options/parse.rs b/driver/src/client/options/parse.rs index 1aab30163..9f228d719 100644 --- a/driver/src/client/options/parse.rs +++ b/driver/src/client/options/parse.rs @@ -164,6 +164,8 @@ impl ClientOptions { srv_service_name: conn_str.srv_service_name, #[cfg(feature = "opentelemetry")] tracing: None, + #[cfg(feature = "socks5-proxy")] + socks5_proxy: conn_str.socks5_proxy, } } } diff --git a/driver/src/client/options/test.rs b/driver/src/client/options/test.rs index cf03f50f6..843736747 100644 --- a/driver/src/client/options/test.rs +++ b/driver/src/client/options/test.rs @@ -218,11 +218,10 @@ async fn run_tests(path: &[&str], skipped_files: &[&str]) { #[tokio::test] async fn run_uri_options_spec_tests() { - let mut skipped_files = vec![ - "single-threaded-options.json", - // TODO RUST-1054 unskip this file - "proxy-options.json", - ]; + let mut skipped_files = vec!["single-threaded-options.json"]; + if cfg!(not(feature = "socks5-proxy")) { + skipped_files.push("proxy-options.json"); + } if cfg!(not(feature = "gssapi-auth")) { skipped_files.push("auth-options.json"); } diff --git a/driver/src/cmap/establish.rs b/driver/src/cmap/establish.rs index d364f13ed..1cb018641 100644 --- a/driver/src/cmap/establish.rs +++ b/driver/src/cmap/establish.rs @@ -2,17 +2,6 @@ pub(crate) mod handshake; use std::time::{Duration, Instant}; -use self::handshake::{Handshaker, HandshakerOptions}; -use super::{ - conn::{ - pooled::PooledConnection, - ConnectionGeneration, - LoadBalancedGeneration, - PendingConnection, - }, - Connection, - PoolGeneration, -}; #[cfg(test)] use crate::options::ClientOptions; use crate::{ @@ -22,10 +11,25 @@ use crate::{ }, error::{Error as MongoError, ErrorKind, Result}, hello::HelloReply, - runtime::{self, stream::DEFAULT_CONNECT_TIMEOUT, AsyncStream, TlsConfig}, + options::Socks5Proxy, + runtime, + runtime::{stream::DEFAULT_CONNECT_TIMEOUT, AsyncStream, TlsConfig}, sdam::{topology::TopologySpec, HandshakePhase}, }; +use super::{ + conn::{ + pooled::PooledConnection, + ConnectionGeneration, + LoadBalancedGeneration, + PendingConnection, + }, + Connection, + PoolGeneration, +}; + +use handshake::{Handshaker, HandshakerOptions}; + /// Contains the logic to establish a connection, including handshaking, authenticating, and /// potentially more. #[derive(Clone)] @@ -38,6 +42,8 @@ pub(crate) struct ConnectionEstablisher { connect_timeout: Duration, + proxy: Option, + #[cfg(test)] test_patch_reply: Option)>, } @@ -46,6 +52,8 @@ pub(crate) struct EstablisherOptions { handshake_options: HandshakerOptions, tls_options: Option, connect_timeout: Option, + #[allow(unused)] + proxy: Option, #[cfg(test)] pub(crate) test_patch_reply: Option)>, } @@ -58,6 +66,10 @@ impl From<&TopologySpec> for EstablisherOptions { connect_timeout: spec.options.connect_timeout, #[cfg(test)] test_patch_reply: None, + #[cfg(feature = "socks5-proxy")] + proxy: spec.options.socks5_proxy.clone(), + #[cfg(not(feature = "socks5-proxy"))] + proxy: None, } } } @@ -92,13 +104,17 @@ impl ConnectionEstablisher { connect_timeout, #[cfg(test)] test_patch_reply: options.test_patch_reply, + #[cfg(feature = "socks5-proxy")] + proxy: options.proxy, + #[cfg(not(feature = "socks5-proxy"))] + proxy: None, }) } async fn make_stream(&self, address: ServerAddress) -> Result { runtime::timeout( self.connect_timeout, - AsyncStream::connect(address, self.tls_config.as_ref()), + AsyncStream::connect(address, self.tls_config.as_ref(), self.proxy.as_ref()), ) .await? } diff --git a/driver/src/error.rs b/driver/src/error.rs index f5fbed585..a8fc0d940 100644 --- a/driver/src/error.rs +++ b/driver/src/error.rs @@ -337,6 +337,10 @@ impl Error { } pub(crate) fn is_network_error(&self) -> bool { + #[cfg(feature = "socks5-proxy")] + if matches!(self.kind.as_ref(), ErrorKind::ProxyConnect { .. }) { + return true; + } matches!( self.kind.as_ref(), ErrorKind::Io(..) | ErrorKind::ConnectionPoolCleared { .. } @@ -578,6 +582,8 @@ impl Error { | ErrorKind::Custom(_) | ErrorKind::Shutdown | ErrorKind::GridFs(_) => {} + #[cfg(feature = "socks5-proxy")] + ErrorKind::ProxyConnect { .. } => {} #[cfg(feature = "in-use-encryption")] ErrorKind::Encryption(_) => {} #[cfg(feature = "bson-3")] @@ -766,6 +772,12 @@ pub enum ErrorKind { /// A method was called on a client that was shut down. #[error("Client has been shut down")] Shutdown, + + /// An error occurred when connecting to a proxy host. + #[error("An error occurred when connecting to a proxy host: {message}")] + #[non_exhaustive] + #[cfg(feature = "socks5-proxy")] + ProxyConnect { message: String }, } impl ErrorKind { @@ -812,6 +824,8 @@ impl ErrorKind { ErrorKind::Encryption(..) => "Encryption", ErrorKind::Custom(..) => "Custom", ErrorKind::Shutdown => "Shutdown", + #[cfg(feature = "socks5-proxy")] + ErrorKind::ProxyConnect { .. } => "ProxyConnect", } } } diff --git a/driver/src/runtime/stream.rs b/driver/src/runtime/stream.rs index 52fe15d84..6485e8ee7 100644 --- a/driver/src/runtime/stream.rs +++ b/driver/src/runtime/stream.rs @@ -6,11 +6,14 @@ use std::{ time::Duration, }; -use tokio::{io::AsyncWrite, net::TcpStream}; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + net::TcpStream, +}; use crate::{ error::{Error, ErrorKind, Result}, - options::ServerAddress, + options::{ServerAddress, Socks5Proxy}, runtime, }; @@ -33,20 +36,82 @@ pub(crate) enum AsyncStream { Tcp(TcpStream), /// A TLS connection over TCP. - Tls(TlsStream), + Tls(TlsStream), /// A Unix domain socket connection. #[cfg(unix)] Unix(tokio::net::UnixStream), + + /// A connection to a SOCKS5 proxy. + #[cfg(feature = "socks5-proxy")] + Socks5(fast_socks5::client::Socks5Stream), + + /// A TLS connection to a SOCKS5 proxy. + #[cfg(feature = "socks5-proxy")] + Socks5Tls(TlsStream>), } +#[cfg(feature = "socks5-proxy")] +impl Socks5Proxy { + async fn connect( + &self, + host: String, + port: Option, + ) -> Result> { + use crate::options::DEFAULT_PORT; + use fast_socks5::{ + client::{Config, Socks5Stream}, + SocksError, + }; + + let proxy_address = format!("{}:{}", self.host, self.port.unwrap_or(1080)); + let port = port.unwrap_or(DEFAULT_PORT); + + let stream = if let Some((username, password)) = self.authentication.as_ref() { + Socks5Stream::connect_with_password( + proxy_address, + host, + port, + username.clone(), + password.clone(), + Config::default(), + ) + .await + } else { + Socks5Stream::connect(proxy_address, host, port, Config::default()).await + } + .map_err(|error| { + if let SocksError::Io(io_error) = error { + ErrorKind::Io(std::sync::Arc::new(io_error)) + } else { + ErrorKind::ProxyConnect { + message: error.to_string(), + } + } + })?; + Ok(stream) + } +} impl AsyncStream { pub(crate) async fn connect( address: ServerAddress, tls_cfg: Option<&TlsConfig>, + #[allow(unused)] proxy: Option<&Socks5Proxy>, ) -> Result { match &address { - ServerAddress::Tcp { host, .. } => { + #[allow(unused)] // port is unused when socks5-proxy is not enabled + ServerAddress::Tcp { host, port } => { + #[cfg(feature = "socks5-proxy")] + if let Some(proxy) = proxy { + let inner = proxy.connect(host.clone(), *port).await?; + return match tls_cfg { + Some(cfg) => { + Ok(AsyncStream::Socks5Tls(tls_connect(host, inner, cfg).await?)) + } + None => Ok(AsyncStream::Socks5(inner)), + }; + } + let resolved: Vec<_> = runtime::resolve_address(&address).await?.collect(); if resolved.is_empty() { return Err(ErrorKind::DnsResolve { @@ -163,7 +228,7 @@ fn interleave(left: Vec, right: Vec) -> Vec { out } -impl tokio::io::AsyncRead for AsyncStream { +impl AsyncRead for AsyncStream { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, @@ -171,10 +236,14 @@ impl tokio::io::AsyncRead for AsyncStream { ) -> Poll> { match self.deref_mut() { Self::Null => Poll::Ready(Ok(())), - Self::Tcp(ref mut inner) => tokio::io::AsyncRead::poll_read(Pin::new(inner), cx, buf), - Self::Tls(ref mut inner) => tokio::io::AsyncRead::poll_read(Pin::new(inner), cx, buf), + Self::Tcp(ref mut inner) => AsyncRead::poll_read(Pin::new(inner), cx, buf), + Self::Tls(ref mut inner) => AsyncRead::poll_read(Pin::new(inner), cx, buf), #[cfg(unix)] - Self::Unix(ref mut inner) => tokio::io::AsyncRead::poll_read(Pin::new(inner), cx, buf), + Self::Unix(ref mut inner) => AsyncRead::poll_read(Pin::new(inner), cx, buf), + #[cfg(feature = "socks5-proxy")] + Self::Socks5(ref mut inner) => AsyncRead::poll_read(Pin::new(inner), cx, buf), + #[cfg(feature = "socks5-proxy")] + Self::Socks5Tls(ref mut inner) => AsyncRead::poll_read(Pin::new(inner), cx, buf), } } } @@ -191,6 +260,10 @@ impl AsyncWrite for AsyncStream { Self::Tls(ref mut inner) => Pin::new(inner).poll_write(cx, buf), #[cfg(unix)] Self::Unix(ref mut inner) => AsyncWrite::poll_write(Pin::new(inner), cx, buf), + #[cfg(feature = "socks5-proxy")] + Self::Socks5(ref mut inner) => AsyncWrite::poll_write(Pin::new(inner), cx, buf), + #[cfg(feature = "socks5-proxy")] + Self::Socks5Tls(ref mut inner) => AsyncWrite::poll_write(Pin::new(inner), cx, buf), } } @@ -201,6 +274,10 @@ impl AsyncWrite for AsyncStream { Self::Tls(ref mut inner) => Pin::new(inner).poll_flush(cx), #[cfg(unix)] Self::Unix(ref mut inner) => AsyncWrite::poll_flush(Pin::new(inner), cx), + #[cfg(feature = "socks5-proxy")] + Self::Socks5(ref mut inner) => AsyncWrite::poll_flush(Pin::new(inner), cx), + #[cfg(feature = "socks5-proxy")] + Self::Socks5Tls(ref mut inner) => AsyncWrite::poll_flush(Pin::new(inner), cx), } } @@ -211,6 +288,10 @@ impl AsyncWrite for AsyncStream { Self::Tls(ref mut inner) => Pin::new(inner).poll_shutdown(cx), #[cfg(unix)] Self::Unix(ref mut inner) => Pin::new(inner).poll_shutdown(cx), + #[cfg(feature = "socks5-proxy")] + Self::Socks5(ref mut inner) => Pin::new(inner).poll_shutdown(cx), + #[cfg(feature = "socks5-proxy")] + Self::Socks5Tls(ref mut inner) => Pin::new(inner).poll_shutdown(cx), } } @@ -225,6 +306,10 @@ impl AsyncWrite for AsyncStream { Self::Tls(ref mut inner) => Pin::new(inner).poll_write_vectored(cx, bufs), #[cfg(unix)] Self::Unix(ref mut inner) => Pin::new(inner).poll_write_vectored(cx, bufs), + #[cfg(feature = "socks5-proxy")] + Self::Socks5(ref mut inner) => Pin::new(inner).poll_write_vectored(cx, bufs), + #[cfg(feature = "socks5-proxy")] + Self::Socks5Tls(ref mut inner) => Pin::new(inner).poll_write_vectored(cx, bufs), } } @@ -235,6 +320,10 @@ impl AsyncWrite for AsyncStream { Self::Tls(ref inner) => inner.is_write_vectored(), #[cfg(unix)] Self::Unix(ref inner) => inner.is_write_vectored(), + #[cfg(feature = "socks5-proxy")] + Self::Socks5(ref inner) => inner.is_write_vectored(), + #[cfg(feature = "socks5-proxy")] + Self::Socks5Tls(ref inner) => inner.is_write_vectored(), } } } diff --git a/driver/src/runtime/tls_openssl.rs b/driver/src/runtime/tls_openssl.rs index ecb0847e8..dedd4ad83 100644 --- a/driver/src/runtime/tls_openssl.rs +++ b/driver/src/runtime/tls_openssl.rs @@ -4,7 +4,7 @@ use openssl::{ error::ErrorStack, ssl::{SslConnector, SslFiletype, SslMethod, SslVerifyMode}, }; -use tokio::net::TcpStream; +use tokio::io::{AsyncRead, AsyncWrite}; use tokio_openssl::SslStream; use crate::{ @@ -12,7 +12,7 @@ use crate::{ error::{Error, ErrorKind, Result}, }; -pub(super) type TlsStream = SslStream; +pub(super) type TlsStream = SslStream; /// Configuration required to use TLS. Creating this is expensive, so its best to cache this value /// and reuse it for multiple connections. @@ -40,11 +40,11 @@ impl TlsConfig { } } -pub(super) async fn tls_connect( +pub(super) async fn tls_connect( host: &str, - tcp_stream: TcpStream, + tcp_stream: T, cfg: &TlsConfig, -) -> Result { +) -> Result> { let mut stream = make_ssl_stream(host, tcp_stream, cfg).map_err(|err| { Error::from(ErrorKind::InvalidTlsConfig { message: err.to_string(), @@ -120,11 +120,11 @@ fn make_openssl_connector(cfg: TlsOptions) -> Result { Ok(builder.build()) } -fn make_ssl_stream( +fn make_ssl_stream( host: &str, - tcp_stream: TcpStream, + tcp_stream: T, cfg: &TlsConfig, -) -> std::result::Result, ErrorStack> { +) -> std::result::Result, ErrorStack> { let ssl = cfg .connector .configure()? diff --git a/driver/src/runtime/tls_rustls.rs b/driver/src/runtime/tls_rustls.rs index 2f9b9e34a..cf18556ee 100644 --- a/driver/src/runtime/tls_rustls.rs +++ b/driver/src/runtime/tls_rustls.rs @@ -12,7 +12,7 @@ use rustls::{ Error as TlsError, RootCertStore, }; -use tokio::net::TcpStream; +use tokio::io::{AsyncRead, AsyncWrite}; use tokio_rustls::TlsConnector; use webpki_roots::TLS_SERVER_ROOTS; @@ -21,7 +21,7 @@ use crate::{ error::{ErrorKind, Result}, }; -pub(super) type TlsStream = tokio_rustls::client::TlsStream; +pub(super) type TlsStream = tokio_rustls::client::TlsStream; /// Configuration required to use TLS. Creating this is expensive, so its best to cache this value /// and reuse it for multiple connections. @@ -42,11 +42,11 @@ impl TlsConfig { } } -pub(super) async fn tls_connect( +pub(super) async fn tls_connect( host: &str, - tcp_stream: TcpStream, + tcp_stream: T, cfg: &TlsConfig, -) -> Result { +) -> Result> { let name = ServerName::try_from(host) .map_err(|e| ErrorKind::DnsResolve { message: format!("could not resolve {host:?}: {e}"), diff --git a/driver/src/test/client.rs b/driver/src/test/client.rs index 19a780c04..ecd9d9b2a 100644 --- a/driver/src/test/client.rs +++ b/driver/src/test/client.rs @@ -1020,3 +1020,120 @@ fn server_address_from_socket_addr_ipv6() { _ => panic!("ServerAddress should have been Tcp variant"), } } + +#[tokio::test] +#[cfg(feature = "socks5-proxy")] +async fn socks5_proxy_skip_ci() { + use crate::{ + error::Result, + options::{ClientOptions, Tls}, + }; + + async fn test_hello(uri: String) -> Result { + let mut options = ClientOptions::parse(uri).await.unwrap(); + // error cases will spin for serverSelectionTimeoutMS while trying to create a connection + options.server_selection_timeout = Some(Duration::from_secs(2)); + let client = Client::with_options(options).unwrap(); + client.database("db").run_command(doc! { "hello": 1 }).await + } + + let options = get_client_options().await; + let mapped_host = "localhost:12345"; + let all_hosts = options + .hosts + .iter() + .map(ToString::to_string) + .collect::>() + .join(","); + let tls = if let Some(Tls::Enabled(ref tls)) = options.tls { + let ca_file = tls.ca_file_path.as_ref().unwrap().to_string_lossy(); + format!("&tls=true&tlsCAFile={ca_file}") + } else { + String::new() + }; + + // fast_socks5 parses "localhost" into an IPV6 address, which is not accepted by the mock server + // used by the tests + let ipv4_localhost = std::net::Ipv4Addr::LOCALHOST.to_string(); + + test_hello(format!( + "mongodb://{mapped_host}/?proxyHost={ipv4_localhost}&proxyPort=1080&\ + directConnection=true{tls}" + )) + .await + .unwrap_err(); + test_hello(format!( + "mongodb://{mapped_host}/?proxyHost={ipv4_localhost}&proxyPort=1081&\ + directConnection=true{tls}" + )) + .await + .unwrap(); + test_hello(format!( + "mongodb://{all_hosts}/?proxyHost={ipv4_localhost}&proxyPort=1080{tls}" + )) + .await + .unwrap_err(); + test_hello(format!( + "mongodb://{all_hosts}/?proxyHost={ipv4_localhost}&proxyPort=1081{tls}" + )) + .await + .unwrap(); + test_hello(format!( + "mongodb://{mapped_host}/?proxyHost={ipv4_localhost}&proxyPort=1080&\ + proxyUsername=nonexistentuser&proxyPassword=badauth&directConnection=true{tls}" + )) + .await + .unwrap_err(); + test_hello(format!( + "mongodb://{mapped_host}/?proxyHost={ipv4_localhost}&proxyPort=1081&\ + proxyUsername=nonexistentuser&proxyPassword=badauth&directConnection=true{tls}" + )) + .await + .unwrap(); + test_hello(format!( + "mongodb://{all_hosts}/?proxyHost={ipv4_localhost}&proxyPort=1081&\ + proxyUsername=nonexistentuser&proxyPassword=badauth{tls}" + )) + .await + .unwrap(); + test_hello(format!( + "mongodb://{mapped_host}/?proxyHost={ipv4_localhost}&proxyPort=1080&\ + proxyUsername=username&proxyPassword=p4ssw0rd&directConnection=true{tls}" + )) + .await + .unwrap(); + test_hello(format!( + "mongodb://{mapped_host}/?proxyHost={ipv4_localhost}&proxyPort=1081&\ + directConnection=true{tls}" + )) + .await + .unwrap(); + test_hello(format!( + "mongodb://{all_hosts}/?proxyHost={ipv4_localhost}&proxyPort=1080&proxyUsername=username&\ + proxyPassword=p4ssw0rd{tls}" + )) + .await + .unwrap(); + test_hello(format!( + "mongodb://{all_hosts}/?proxyHost={ipv4_localhost}&proxyPort=1081{tls}" + )) + .await + .unwrap(); + + // From the spec: Drivers MUST verify for at least one of the connection strings marked + // (succeeds) that command monitoring events do not reference the SOCKS5 proxy host where the + // MongoDB service server/port are referenced. + let uri = format!( + "mongodb://{mapped_host}/?proxyHost={ipv4_localhost}&proxyPort=1081&\ + directConnection=true{tls}" + ); + let options = ClientOptions::parse(uri).await.unwrap(); + let client = Client::for_test().options(options).monitor_events().await; + client + .database("db") + .run_command(doc! { "ping": 1 }) + .await + .unwrap(); + let (started, _) = client.events.get_successful_command_execution("ping"); + assert_eq!(&started.connection.address.to_string(), mapped_host); +}