├── .cargo └── config.toml ├── .github ├── dependabot.yml └── workflows │ ├── auto-merge.yml │ ├── install-cross.sh │ ├── publish-exe.yml │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── build-aarch64-apple-ios-debug.sh ├── build-aarch64-apple-ios.sh ├── build-apple.sh ├── cbindgen.toml ├── rustfmt.toml └── src ├── android.rs ├── api.rs ├── bin └── dns2socks.rs ├── config.rs ├── dns.rs ├── dump_logger.rs └── lib.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | # target = ["x86_64-unknown-linux-gnu"] 3 | # target = ["aarch64-linux-android"] 4 | # target = ["aarch64-apple-ios"] 5 | # target = ["x86_64-pc-windows-msvc"] 6 | # target = ["x86_64-apple-darwin"] 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Auto Merge 2 | 3 | on: 4 | pull_request_target: 5 | types: [labeled] 6 | 7 | jobs: 8 | auto: 9 | if: github.actor == 'dependabot[bot]' 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | - name: Auto approve pull request, then squash and merge 16 | uses: ahmadnassri/action-dependabot-auto-merge@v2 17 | with: 18 | # target: minor 19 | github-token: ${{ secrets.PAT_REPO_ADMIN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/install-cross.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl -s https://api.github.com/repos/cross-rs/cross/releases/latest \ 4 | | grep cross-x86_64-unknown-linux-gnu.tar.gz \ 5 | | cut -d : -f 2,3 \ 6 | | tr -d \" \ 7 | | wget -qi - 8 | 9 | tar -zxvf cross-x86_64-unknown-linux-gnu.tar.gz -C /usr/bin 10 | rm -f cross-x86_64-unknown-linux-gnu.tar.gz 11 | 12 | -------------------------------------------------------------------------------- /.github/workflows/publish-exe.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "v*.*.*" 5 | 6 | name: Publish Releases 7 | 8 | jobs: 9 | build_publish: 10 | name: Publishing Tasks 11 | strategy: 12 | matrix: 13 | target: 14 | - x86_64-unknown-linux-gnu 15 | - x86_64-unknown-linux-musl 16 | - i686-unknown-linux-musl 17 | - aarch64-unknown-linux-gnu 18 | - armv7-unknown-linux-gnueabihf 19 | - x86_64-apple-darwin 20 | - aarch64-apple-darwin 21 | - x86_64-pc-windows-msvc 22 | - i686-pc-windows-msvc 23 | 24 | include: 25 | - target: x86_64-unknown-linux-gnu 26 | host_os: ubuntu-latest 27 | - target: x86_64-unknown-linux-musl 28 | host_os: ubuntu-latest 29 | - target: i686-unknown-linux-musl 30 | host_os: ubuntu-latest 31 | - target: aarch64-unknown-linux-gnu 32 | host_os: ubuntu-latest 33 | - target: armv7-unknown-linux-gnueabihf 34 | host_os: ubuntu-latest 35 | - target: x86_64-apple-darwin 36 | host_os: macos-latest 37 | - target: aarch64-apple-darwin 38 | host_os: macos-latest 39 | - target: x86_64-pc-windows-msvc 40 | host_os: windows-latest 41 | - target: i686-pc-windows-msvc 42 | host_os: windows-latest 43 | 44 | runs-on: ${{ matrix.host_os }} 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: dtolnay/rust-toolchain@stable 48 | 49 | - name: Prepare 50 | shell: bash 51 | run: | 52 | cargo install cbindgen 53 | mkdir mypubdir4 54 | rustup target add ${{ matrix.target }} 55 | if [[ "${{ matrix.host_os }}" == "ubuntu-latest" ]]; then 56 | sudo .github/workflows/install-cross.sh 57 | fi 58 | 59 | - name: Build 60 | shell: bash 61 | run: | 62 | if [[ "${{ matrix.host_os }}" == "ubuntu-latest" ]]; then 63 | cross build --all-features --release --target ${{ matrix.target }} 64 | else 65 | cargo build --all-features --release --target ${{ matrix.target }} 66 | fi 67 | cbindgen --config cbindgen.toml -o target/dns2socks.h 68 | if [[ "${{ matrix.host_os }}" == "windows-latest" ]]; then 69 | powershell -Command "(Get-Item README.md).LastWriteTime = Get-Date" 70 | powershell Compress-Archive -Path target/${{ matrix.target }}/release/dns2socks.exe, README.md, target/dns2socks.h, target/${{ matrix.target }}/release/dns2socks.dll -DestinationPath mypubdir4/dns2socks-${{ matrix.target }}.zip 71 | elif [[ "${{ matrix.host_os }}" == "macos-latest" ]]; then 72 | zip -j mypubdir4/dns2socks-${{ matrix.target }}.zip target/${{ matrix.target }}/release/dns2socks README.md target/dns2socks.h target/${{ matrix.target }}/release/libdns2socks.dylib 73 | if [[ "${{ matrix.target }}" == "x86_64-apple-darwin" ]]; then 74 | ./build-aarch64-apple-ios.sh 75 | zip -r mypubdir4/dns2socks-aarch64-apple-ios-xcframework.zip ./dns2socks.xcframework/ 76 | ./build-apple.sh 77 | zip -r mypubdir4/dns2socks-apple-xcframework.zip ./dns2socks.xcframework/ 78 | fi 79 | elif [[ "${{ matrix.host_os }}" == "ubuntu-latest" ]]; then 80 | zip -j mypubdir4/dns2socks-${{ matrix.target }}.zip target/${{ matrix.target }}/release/dns2socks README.md target/dns2socks.h target/${{ matrix.target }}/release/libdns2socks.so 81 | fi 82 | 83 | - name: Publish 84 | uses: softprops/action-gh-release@v2 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | with: 88 | files: mypubdir4/* 89 | 90 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Push or PR 2 | 3 | on: 4 | [push, pull_request] 5 | 6 | env: 7 | CARGO_TERM_COLOR: always 8 | 9 | jobs: 10 | build_n_test: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, macos-latest, windows-latest] 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: dtolnay/rust-toolchain@stable 20 | - name: rustfmt 21 | run: | 22 | rustc --version 23 | cargo fmt --all -- --check 24 | - name: check 25 | run: cargo check --verbose 26 | - name: clippy 27 | run: cargo clippy --all-targets --all-features -- -D warnings 28 | - name: Build 29 | run: cargo build --verbose --tests --all-features 30 | 31 | build_android: 32 | strategy: 33 | fail-fast: false 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: dtolnay/rust-toolchain@stable 39 | - name: Install cargo ndk and rust compiler for android target 40 | if: ${{ !cancelled() }} 41 | run: | 42 | cargo install --locked cargo-ndk 43 | rustup target add x86_64-linux-android 44 | - name: clippy 45 | if: ${{ !cancelled() }} 46 | run: cargo ndk -t x86_64 clippy --all-features -- -D warnings 47 | - name: Build 48 | if: ${{ !cancelled() }} 49 | run: cargo ndk -t x86_64 rustc --verbose --all-features --lib --crate-type=cdylib 50 | - name: Abort on error 51 | if: ${{ failure() }} 52 | run: echo "Android build job failed" && false 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dns2socks.xcframework/ 2 | Cargo.lock 3 | .vscode/ 4 | .VSCodeCounter/ 5 | /target 6 | tmp/ 7 | build/ 8 | .idea/ 9 | .env 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dns2socks" 3 | version = "0.2.0" 4 | edition = "2024" 5 | license = "MIT" 6 | repository = "https://github.com/ssrlive/dns2socks" 7 | authors = ["ssrlive"] 8 | description = "Convert DNS requests to SOCKS5 proxy." 9 | readme = "README.md" 10 | 11 | [lib] 12 | crate-type = ["staticlib", "cdylib", "rlib"] 13 | 14 | [dependencies] 15 | chrono = "0.4" 16 | clap = { version = "4", features = ["derive", "wrap_help"] } 17 | ctrlc2 = { version = "3", features = ["tokio", "termination"] } 18 | dotenvy = "0.15" 19 | env_logger = "0.11" 20 | hickory-proto = "0.25" 21 | log = "0.4" 22 | moka = { version = "0.12", default-features = false, features = ["future"] } 23 | percent-encoding = "2" 24 | rand = "0.9" 25 | socks5-impl = { version = "0.7", default-features = false, features = [ 26 | "client", 27 | ] } 28 | tokio = { version = "1", features = ["full"] } 29 | tokio-util = "0.7" 30 | url = "2" 31 | 32 | [target.'cfg(target_os="android")'.dependencies] 33 | android_logger = "0.15" 34 | jni = { version = "0.21", default-features = false } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ssrlive 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dns2socks 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/dns2socks.svg)](https://crates.io/crates/dns2socks) 4 | ![dns2socks](https://docs.rs/dns2socks/badge.svg) 5 | [![Documentation](https://img.shields.io/badge/docs-release-brightgreen.svg?style=flat)](https://docs.rs/dns2socks) 6 | [![Download](https://img.shields.io/crates/d/dns2socks.svg)](https://crates.io/crates/dns2socks) 7 | [![License](https://img.shields.io/crates/l/dns2socks.svg?style=flat)](https://github.com/ssrlive/dns2socks/blob/master/LICENSE) 8 | 9 | A DNS server that forwards DNS requests to a SOCKS5 server. 10 | 11 | ## Installation 12 | 13 | ### Precompiled Binaries 14 | 15 | Download binary from [releases](https://github.com/ssrlive/dns2socks/releases) and put it in your `$PATH`. 16 | 17 | ### Install from Crates.io 18 | 19 | If you have [Rust](https://rustup.rs/) toolchain installed, you can install `dns2socks` with the following command: 20 | ```sh 21 | cargo install dns2socks 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```plaintext 27 | dns2socks -h 28 | 29 | Proxy server to routing DNS query to SOCKS5 server 30 | 31 | Usage: dns2socks [OPTIONS] 32 | 33 | Options: 34 | -l, --listen-addr Listen address [default: 0.0.0.0:53] 35 | -d, --dns-remote-server Remote DNS server address [default: 8.8.8.8:53] 36 | -s, --socks5-settings SOCKS5 URL in the form socks5://[username[:password]@]host:port, Username and password are encoded in percent 37 | encoding. For example: socks5://myname:pass%40word@127.0.0.1:1080 [default: socks5://127.0.0.1:1080] 38 | -f, --force-tcp Force to use TCP to proxy DNS query 39 | -c, --cache-records Cache DNS query records 40 | -v, --verbosity Verbosity level [default: info] [possible values: off, error, warn, info, debug, trace] 41 | -t, --timeout Timeout for DNS query [default: 5] 42 | -h, --help Print help 43 | -V, --version Print version 44 | ``` 45 | -------------------------------------------------------------------------------- /build-aarch64-apple-ios-debug.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | echo "Setting up the rust environment..." 4 | rustup target add aarch64-apple-ios 5 | cargo install cbindgen 6 | 7 | echo "Building target aarch64-apple-ios..." 8 | cargo build --target aarch64-apple-ios 9 | 10 | echo "Generating includes..." 11 | mkdir -p target/include/ 12 | rm -rf target/include/* 13 | cbindgen --config cbindgen.toml -o target/include/dns2socks.h 14 | cat > target/include/dns2socks.modulemap < target/include/dns2socks.modulemap < target/include/dns2socks.modulemap <> = std::sync::Mutex::new(None); 11 | 12 | /// # Safety 13 | /// 14 | /// Start dns2socks 15 | /// Parameters: 16 | /// - listen_addr: the listen address, e.g. "172.19.0.1:53", or null to use the default value 17 | /// - dns_remote_server: the dns remote server, e.g. "8.8.8.8:53", or null to use the default value 18 | /// - socks5_settings: the socks5 server, e.g. "socks5://[username[:password]@]host:port", or null to use the default value 19 | /// - force_tcp: whether to force tcp, true or false, default is false 20 | /// - cache_records: whether to cache dns records, true or false, default is false 21 | /// - verbosity: the verbosity level, see ArgVerbosity enum, default is ArgVerbosity::Info 22 | /// - timeout: the timeout in seconds, default is 5 23 | #[unsafe(no_mangle)] 24 | pub unsafe extern "C" fn Java_com_github_shadowsocks_bg_Dns2socks_start( 25 | mut env: JNIEnv, 26 | _clazz: JClass, 27 | listen_addr: JString, 28 | dns_remote_server: JString, 29 | socks5_settings: JString, 30 | force_tcp: jboolean, 31 | cache_records: jboolean, 32 | verbosity: jint, 33 | timeout: jint, 34 | ) -> jint { 35 | let verbosity: ArgVerbosity = verbosity.try_into().unwrap_or_default(); 36 | let filter_str = &format!("off,dns2socks={verbosity}"); 37 | let filter = android_logger::FilterBuilder::new().parse(filter_str).build(); 38 | android_logger::init_once( 39 | android_logger::Config::default() 40 | .with_tag("dns2socks") 41 | .with_max_level(log::LevelFilter::Trace) 42 | .with_filter(filter), 43 | ); 44 | 45 | let listen_addr = match get_java_string(&mut env, &listen_addr) { 46 | Ok(addr) => addr, 47 | Err(_e) => "0.0.0.0:53".to_string(), 48 | }; 49 | let dns_remote_server = match get_java_string(&mut env, &dns_remote_server) { 50 | Ok(addr) => addr, 51 | Err(_e) => "8.8.8.8:53".to_string(), 52 | }; 53 | let socks5_settings = match get_java_string(&mut env, &socks5_settings) { 54 | Ok(addr) => addr, 55 | Err(_e) => "socks5://127.0.0.1:1080".to_string(), 56 | }; 57 | let force_tcp = force_tcp != 0; 58 | let cache_records = cache_records != 0; 59 | let timeout = if timeout < 3 { 5 } else { timeout as u64 }; 60 | 61 | let shutdown_token = tokio_util::sync::CancellationToken::new(); 62 | if let Ok(mut lock) = TUN_QUIT.lock() { 63 | if lock.is_some() { 64 | return -1; 65 | } 66 | *lock = Some(shutdown_token.clone()); 67 | } else { 68 | return -2; 69 | } 70 | 71 | let main_loop = async move { 72 | let mut cfg = Config::default(); 73 | cfg.verbosity(verbosity) 74 | .timeout(timeout) 75 | .force_tcp(force_tcp) 76 | .cache_records(cache_records) 77 | .listen_addr(listen_addr.parse()?) 78 | .dns_remote_server(dns_remote_server.parse()?) 79 | .socks5_settings(ArgProxy::try_from(socks5_settings.as_str())?); 80 | 81 | if let Err(err) = main_entry(cfg, shutdown_token).await { 82 | log::error!("main loop error: {}", err); 83 | return Err(err); 84 | } 85 | Ok(()) 86 | }; 87 | 88 | let exit_code = match tokio::runtime::Builder::new_multi_thread().enable_all().build() { 89 | Err(_e) => -3, 90 | Ok(rt) => match rt.block_on(main_loop) { 91 | Ok(_) => 0, 92 | Err(_e) => -4, 93 | }, 94 | }; 95 | 96 | exit_code 97 | } 98 | 99 | /// # Safety 100 | /// 101 | /// Shutdown dns2socks 102 | #[unsafe(no_mangle)] 103 | pub unsafe extern "C" fn Java_com_github_shadowsocks_bg_Dns2socks_stop(_env: JNIEnv, _: JClass) -> jint { 104 | if let Ok(mut lock) = TUN_QUIT.lock() { 105 | if let Some(shutdown_token) = lock.take() { 106 | shutdown_token.cancel(); 107 | return 0; 108 | } 109 | } 110 | -1 111 | } 112 | 113 | fn get_java_string(env: &mut JNIEnv, string: &JString) -> std::io::Result { 114 | use std::io::{Error, ErrorKind::Other}; 115 | Ok(env.get_string(string).map_err(|e| Error::new(Other, e))?.into()) 116 | } 117 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | use crate::{ArgVerbosity, main_entry}; 2 | use std::ffi::{c_char, c_int}; 3 | 4 | static TUN_QUIT: std::sync::Mutex> = std::sync::Mutex::new(None); 5 | 6 | /// # Safety 7 | /// 8 | /// Run the dns2socks component with some arguments. 9 | /// Parameters: 10 | /// - listen_addr: the listen address, e.g. "0.0.0.0:53", or null to use the default value 11 | /// - dns_remote_server: the dns remote server, e.g. "8.8.8.8:53", or null to use the default value 12 | /// - socks5_settings: the socks5 server, e.g. "socks5://[username[:password]@]host:port", or null to use the default value 13 | /// - force_tcp: whether to force tcp, true or false, default is false 14 | /// - cache_records: whether to cache dns records, true or false, default is false 15 | /// - verbosity: the verbosity level, see ArgVerbosity enum, default is ArgVerbosity::Info 16 | /// - timeout: the timeout in seconds, default is 5 17 | #[unsafe(no_mangle)] 18 | pub unsafe extern "C" fn dns2socks_start( 19 | listen_addr: *const c_char, 20 | dns_remote_server: *const c_char, 21 | socks5_settings: *const c_char, 22 | force_tcp: bool, 23 | cache_records: bool, 24 | verbosity: ArgVerbosity, 25 | timeout: i32, 26 | ) -> c_int { 27 | let shutdown_token = tokio_util::sync::CancellationToken::new(); 28 | { 29 | if let Ok(mut lock) = TUN_QUIT.lock() { 30 | if lock.is_some() { 31 | return -1; 32 | } 33 | *lock = Some(shutdown_token.clone()); 34 | } else { 35 | return -2; 36 | } 37 | } 38 | 39 | log::set_max_level(verbosity.into()); 40 | if let Err(err) = log::set_boxed_logger(Box::::default()) { 41 | log::warn!("set logger error: {}", err); 42 | } 43 | 44 | let mut config = crate::Config::default(); 45 | config 46 | .verbosity(verbosity) 47 | .timeout(timeout as u64) 48 | .force_tcp(force_tcp) 49 | .cache_records(cache_records); 50 | if !listen_addr.is_null() { 51 | let listen_addr = unsafe { std::ffi::CStr::from_ptr(listen_addr) }.to_str().unwrap(); 52 | config.listen_addr(listen_addr.parse().unwrap()); 53 | } 54 | if !dns_remote_server.is_null() { 55 | let dns_remote_server = unsafe { std::ffi::CStr::from_ptr(dns_remote_server) }.to_str().unwrap(); 56 | config.dns_remote_server(dns_remote_server.parse().unwrap()); 57 | } 58 | if !socks5_settings.is_null() { 59 | let socks5_settings = unsafe { std::ffi::CStr::from_ptr(socks5_settings) }.to_str().unwrap(); 60 | config.socks5_settings(crate::config::ArgProxy::try_from(socks5_settings).unwrap()); 61 | } 62 | 63 | let main_loop = async move { 64 | if let Err(err) = main_entry(config, shutdown_token).await { 65 | log::error!("main loop error: {}", err); 66 | return Err(err); 67 | } 68 | Ok(()) 69 | }; 70 | 71 | let exit_code = match tokio::runtime::Builder::new_multi_thread().enable_all().build() { 72 | Err(_e) => -3, 73 | Ok(rt) => match rt.block_on(main_loop) { 74 | Ok(_) => 0, 75 | Err(_e) => -4, 76 | }, 77 | }; 78 | 79 | exit_code 80 | } 81 | 82 | /// # Safety 83 | /// 84 | /// Shutdown the dns2socks component. 85 | #[unsafe(no_mangle)] 86 | pub unsafe extern "C" fn dns2socks_stop() -> c_int { 87 | if let Ok(mut lock) = TUN_QUIT.lock() { 88 | if let Some(shutdown_token) = lock.take() { 89 | shutdown_token.cancel(); 90 | return 0; 91 | } 92 | } 93 | -1 94 | } 95 | -------------------------------------------------------------------------------- /src/bin/dns2socks.rs: -------------------------------------------------------------------------------- 1 | use dns2socks::{Config, main_entry}; 2 | 3 | #[tokio::main] 4 | async fn main() -> Result<(), Box> { 5 | let config = Config::parse_args(); 6 | 7 | dotenvy::dotenv().ok(); 8 | 9 | let default = format!("{}={:?}", module_path!(), config.verbosity); 10 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default)).init(); 11 | 12 | let shutdown_token = tokio_util::sync::CancellationToken::new(); 13 | let join_handle = tokio::spawn({ 14 | let shutdown_token = shutdown_token.clone(); 15 | async move { 16 | if let Err(err) = main_entry(config, shutdown_token).await { 17 | log::error!("main loop error: {}", err); 18 | } 19 | } 20 | }); 21 | 22 | ctrlc2::set_async_handler(async move { 23 | log::info!("Ctrl-C received, exiting..."); 24 | shutdown_token.cancel(); 25 | }) 26 | .await; 27 | 28 | if let Err(err) = join_handle.await { 29 | log::error!("main_entry error {}", err); 30 | } 31 | 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use socks5_impl::protocol::UserKey; 2 | use std::net::{SocketAddr, ToSocketAddrs as _}; 3 | 4 | /// Proxy server to routing DNS query to SOCKS5 server 5 | #[derive(clap::Parser, Debug, Clone, PartialEq, Eq)] 6 | #[command(author, version, about = "Proxy server to routing DNS query to SOCKS5 server", long_about = None)] 7 | pub struct Config { 8 | /// Listen address 9 | #[clap(short, long, value_name = "IP:port", default_value = "0.0.0.0:53")] 10 | pub listen_addr: SocketAddr, 11 | 12 | /// Remote DNS server address 13 | #[clap(short, long, value_name = "IP:port", default_value = "8.8.8.8:53")] 14 | pub dns_remote_server: SocketAddr, 15 | 16 | /// SOCKS5 URL in the form socks5://[username[:password]@]host:port, 17 | /// Username and password are encoded in percent encoding. For example: 18 | /// socks5://myname:pass%40word@127.0.0.1:1080 19 | #[arg(short, long, value_parser = |s: &str| ArgProxy::try_from(s), value_name = "URL", default_value = "socks5://127.0.0.1:1080")] 20 | pub socks5_settings: ArgProxy, 21 | 22 | /// Force to use TCP to proxy DNS query 23 | #[clap(short, long)] 24 | pub force_tcp: bool, 25 | 26 | /// Cache DNS query records 27 | #[clap(short, long)] 28 | pub cache_records: bool, 29 | 30 | /// Verbosity level 31 | #[arg(short, long, value_name = "level", value_enum, default_value = "info")] 32 | pub verbosity: ArgVerbosity, 33 | 34 | /// Timeout for DNS query 35 | #[clap(short, long, value_name = "seconds", default_value = "5")] 36 | pub timeout: u64, 37 | } 38 | 39 | impl Default for Config { 40 | fn default() -> Self { 41 | Config { 42 | listen_addr: "0.0.0.0:53".parse().unwrap(), 43 | dns_remote_server: "8.8.8.8:53".parse().unwrap(), 44 | socks5_settings: ArgProxy::default(), 45 | force_tcp: false, 46 | cache_records: false, 47 | verbosity: ArgVerbosity::default(), 48 | timeout: 5, 49 | } 50 | } 51 | } 52 | 53 | impl Config { 54 | pub fn parse_args() -> Self { 55 | clap::Parser::parse() 56 | } 57 | 58 | pub fn listen_addr(&mut self, listen_addr: SocketAddr) -> &mut Self { 59 | self.listen_addr = listen_addr; 60 | self 61 | } 62 | 63 | pub fn dns_remote_server(&mut self, dns_remote_server: SocketAddr) -> &mut Self { 64 | self.dns_remote_server = dns_remote_server; 65 | self 66 | } 67 | 68 | pub fn socks5_settings(&mut self, socks5_settings: ArgProxy) -> &mut Self { 69 | self.socks5_settings = socks5_settings; 70 | self 71 | } 72 | 73 | pub fn force_tcp(&mut self, force_tcp: bool) -> &mut Self { 74 | self.force_tcp = force_tcp; 75 | self 76 | } 77 | 78 | pub fn cache_records(&mut self, cache_records: bool) -> &mut Self { 79 | self.cache_records = cache_records; 80 | self 81 | } 82 | 83 | pub fn verbosity(&mut self, verbosity: ArgVerbosity) -> &mut Self { 84 | self.verbosity = verbosity; 85 | self 86 | } 87 | 88 | pub fn timeout(&mut self, timeout: u64) -> &mut Self { 89 | self.timeout = timeout; 90 | self 91 | } 92 | } 93 | 94 | #[repr(C)] 95 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum, Default)] 96 | pub enum ArgVerbosity { 97 | Off = 0, 98 | Error, 99 | Warn, 100 | #[default] 101 | Info, 102 | Debug, 103 | Trace, 104 | } 105 | 106 | impl std::fmt::Display for ArgVerbosity { 107 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 108 | match self { 109 | ArgVerbosity::Off => write!(f, "off"), 110 | ArgVerbosity::Error => write!(f, "error"), 111 | ArgVerbosity::Warn => write!(f, "warn"), 112 | ArgVerbosity::Info => write!(f, "info"), 113 | ArgVerbosity::Debug => write!(f, "debug"), 114 | ArgVerbosity::Trace => write!(f, "trace"), 115 | } 116 | } 117 | } 118 | 119 | impl From for ArgVerbosity { 120 | fn from(level: log::Level) -> Self { 121 | match level { 122 | log::Level::Error => ArgVerbosity::Error, 123 | log::Level::Warn => ArgVerbosity::Warn, 124 | log::Level::Info => ArgVerbosity::Info, 125 | log::Level::Debug => ArgVerbosity::Debug, 126 | log::Level::Trace => ArgVerbosity::Trace, 127 | } 128 | } 129 | } 130 | 131 | impl From for log::LevelFilter { 132 | fn from(level: ArgVerbosity) -> Self { 133 | match level { 134 | ArgVerbosity::Off => log::LevelFilter::Off, 135 | ArgVerbosity::Error => log::LevelFilter::Error, 136 | ArgVerbosity::Warn => log::LevelFilter::Warn, 137 | ArgVerbosity::Info => log::LevelFilter::Info, 138 | ArgVerbosity::Debug => log::LevelFilter::Debug, 139 | ArgVerbosity::Trace => log::LevelFilter::Trace, 140 | } 141 | } 142 | } 143 | 144 | impl TryFrom for ArgVerbosity { 145 | type Error = std::io::Error; 146 | 147 | fn try_from(value: i32) -> Result>::Error> { 148 | match value { 149 | 0 => Ok(ArgVerbosity::Off), 150 | 1 => Ok(ArgVerbosity::Error), 151 | 2 => Ok(ArgVerbosity::Warn), 152 | 3 => Ok(ArgVerbosity::Info), 153 | 4 => Ok(ArgVerbosity::Debug), 154 | 5 => Ok(ArgVerbosity::Trace), 155 | _ => Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid verbosity level")), 156 | } 157 | } 158 | } 159 | 160 | #[derive(Clone, Debug, PartialEq, Eq)] 161 | pub struct ArgProxy { 162 | pub proxy_type: ProxyType, 163 | pub addr: SocketAddr, 164 | pub credentials: Option, 165 | } 166 | 167 | impl Default for ArgProxy { 168 | fn default() -> Self { 169 | ArgProxy { 170 | proxy_type: ProxyType::Socks5, 171 | addr: "127.0.0.1:1080".parse().unwrap(), 172 | credentials: None, 173 | } 174 | } 175 | } 176 | 177 | impl std::fmt::Display for ArgProxy { 178 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 179 | let auth = match &self.credentials { 180 | Some(creds) => format!("{}", creds), 181 | None => "".to_owned(), 182 | }; 183 | if auth.is_empty() { 184 | write!(f, "{}://{}", &self.proxy_type, &self.addr) 185 | } else { 186 | write!(f, "{}://{}@{}", &self.proxy_type, auth, &self.addr) 187 | } 188 | } 189 | } 190 | 191 | impl TryFrom<&str> for ArgProxy { 192 | type Error = std::io::Error; 193 | fn try_from(s: &str) -> Result { 194 | use std::io::{Error, ErrorKind::InvalidData}; 195 | let e = format!("`{s}` is not a valid proxy URL"); 196 | let url = url::Url::parse(s).map_err(|_| Error::new(InvalidData, e))?; 197 | let e = format!("`{s}` does not contain a host"); 198 | let host = url.host_str().ok_or(Error::new(InvalidData, e))?; 199 | 200 | let e = format!("`{s}` does not contain a port"); 201 | let port = url.port_or_known_default().ok_or(Error::new(InvalidData, e))?; 202 | 203 | let e2 = format!("`{host}` does not resolve to a usable IP address"); 204 | let addr = (host, port).to_socket_addrs()?.next().ok_or(Error::new(InvalidData, e2))?; 205 | 206 | let credentials = if url.username() == "" && url.password().is_none() { 207 | None 208 | } else { 209 | let username = percent_encoding::percent_decode(url.username().as_bytes()) 210 | .decode_utf8() 211 | .map_err(|e| Error::new(InvalidData, e))?; 212 | let password = percent_encoding::percent_decode(url.password().unwrap_or("").as_bytes()) 213 | .decode_utf8() 214 | .map_err(|e| Error::new(InvalidData, e))?; 215 | Some(UserKey::new(username, password)) 216 | }; 217 | 218 | let proxy_type = url.scheme().to_ascii_lowercase().as_str().try_into()?; 219 | 220 | Ok(ArgProxy { 221 | proxy_type, 222 | addr, 223 | credentials, 224 | }) 225 | } 226 | } 227 | 228 | #[repr(C)] 229 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default)] 230 | pub enum ProxyType { 231 | // Http = 0, 232 | // Socks4, 233 | #[default] 234 | Socks5, 235 | } 236 | 237 | impl TryFrom<&str> for ProxyType { 238 | type Error = std::io::Error; 239 | fn try_from(value: &str) -> Result { 240 | use std::io::{Error, ErrorKind::InvalidData}; 241 | match value { 242 | // "http" => Ok(ProxyType::Http), 243 | // "socks4" => Ok(ProxyType::Socks4), 244 | "socks5" => Ok(ProxyType::Socks5), 245 | scheme => Err(Error::new(InvalidData, format!("`{scheme}` is an invalid proxy type"))), 246 | } 247 | } 248 | } 249 | 250 | impl std::fmt::Display for ProxyType { 251 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 252 | match self { 253 | // ProxyType::Http => write!(f, "http"), 254 | // ProxyType::Socks4 => write!(f, "socks4"), 255 | ProxyType::Socks5 => write!(f, "socks5"), 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/dns.rs: -------------------------------------------------------------------------------- 1 | use hickory_proto::{ 2 | op::{Message, ResponseCode, header::MessageType, op_code::OpCode, query::Query}, 3 | rr::{Name, RData, record_type::RecordType}, 4 | }; 5 | use std::io::{Error, ErrorKind}; 6 | use std::{net::IpAddr, str::FromStr}; 7 | 8 | #[allow(dead_code)] 9 | pub fn build_dns_query(domain: &str, query_type: RecordType, used_by_tcp: bool) -> std::io::Result> { 10 | let name = Name::from_str(domain).map_err(|e| Error::new(ErrorKind::InvalidInput, e.to_string()))?; 11 | let query = Query::query(name, query_type); 12 | let mut msg = Message::new(); 13 | msg.add_query(query) 14 | .set_id(rand::Rng::random::(&mut rand::rng())) 15 | .set_op_code(OpCode::Query) 16 | .set_message_type(MessageType::Query) 17 | .set_recursion_desired(true); 18 | let mut msg_buf = msg.to_vec().map_err(|e| Error::new(ErrorKind::InvalidInput, e.to_string()))?; 19 | if used_by_tcp { 20 | let mut buf = (msg_buf.len() as u16).to_be_bytes().to_vec(); 21 | buf.append(&mut msg_buf); 22 | Ok(buf) 23 | } else { 24 | Ok(msg_buf) 25 | } 26 | } 27 | 28 | pub fn extract_ipaddr_from_dns_message(message: &Message) -> std::io::Result { 29 | if message.response_code() != ResponseCode::NoError { 30 | return Err(Error::new(ErrorKind::InvalidData, format!("{:?}", message.response_code()))); 31 | } 32 | let mut cname = None; 33 | for answer in message.answers() { 34 | match answer.data() { 35 | RData::A(addr) => { 36 | return Ok(IpAddr::V4((*addr).into())); 37 | } 38 | RData::AAAA(addr) => { 39 | return Ok(IpAddr::V6((*addr).into())); 40 | } 41 | RData::CNAME(name) => { 42 | cname = Some(name.to_utf8()); 43 | } 44 | _ => {} 45 | } 46 | } 47 | if let Some(cname) = cname { 48 | return Err(Error::new(ErrorKind::InvalidData, format!("CNAME: {}", cname))); 49 | } 50 | Err(Error::new(ErrorKind::InvalidData, format!("{:?}", message.answers()))) 51 | } 52 | 53 | pub fn extract_domain_from_dns_message(message: &Message) -> std::io::Result { 54 | let err = Error::new(ErrorKind::InvalidData, "DnsRequest no query body"); 55 | let query = message.queries().first().ok_or(err)?; 56 | let name = query.name().to_string(); 57 | Ok(name) 58 | } 59 | 60 | pub fn parse_data_to_dns_message(data: &[u8], used_by_tcp: bool) -> std::io::Result { 61 | if used_by_tcp { 62 | let err = Error::new(ErrorKind::InvalidData, "invalid dns data"); 63 | if data.len() < 2 { 64 | return Err(err); 65 | } 66 | let len = u16::from_be_bytes([data[0], data[1]]) as usize; 67 | let data = data.get(2..len + 2).ok_or(err)?; 68 | return parse_data_to_dns_message(data, false); 69 | } 70 | let message = Message::from_vec(data).map_err(|e| Error::new(ErrorKind::InvalidData, e.to_string()))?; 71 | Ok(message) 72 | } 73 | -------------------------------------------------------------------------------- /src/dump_logger.rs: -------------------------------------------------------------------------------- 1 | use crate::ArgVerbosity; 2 | use std::os::raw::{c_char, c_void}; 3 | 4 | static DUMP_CALLBACK: std::sync::OnceLock> = std::sync::OnceLock::new(); 5 | 6 | /// # Safety 7 | /// 8 | /// set dump log info callback. 9 | #[unsafe(no_mangle)] 10 | pub unsafe extern "C" fn dns2socks_set_log_callback( 11 | callback: Option, 12 | ctx: *mut c_void, 13 | ) { 14 | if let Some(_cb) = DUMP_CALLBACK.get_or_init(|| Some(DumpCallback(callback, ctx))) { 15 | log::info!("dump log callback set success"); 16 | } else { 17 | log::warn!("dump log callback already set"); 18 | } 19 | } 20 | 21 | #[derive(Clone, Debug)] 22 | struct DumpCallback(Option, *mut c_void); 23 | 24 | impl DumpCallback { 25 | unsafe fn call(self, dump_level: ArgVerbosity, info: *const c_char) { 26 | if let Some(cb) = self.0 { 27 | unsafe { cb(dump_level, info, self.1) }; 28 | } 29 | } 30 | } 31 | 32 | unsafe impl Send for DumpCallback {} 33 | unsafe impl Sync for DumpCallback {} 34 | 35 | #[derive(Debug, Clone, PartialEq, Eq, Default)] 36 | pub(crate) struct DumpLogger; 37 | 38 | impl log::Log for DumpLogger { 39 | fn enabled(&self, metadata: &log::Metadata) -> bool { 40 | metadata.level() <= log::Level::Trace 41 | } 42 | 43 | fn log(&self, record: &log::Record) { 44 | if self.enabled(record.metadata()) { 45 | let current_crate_name = env!("CARGO_CRATE_NAME"); 46 | if record.module_path().unwrap_or("").starts_with(current_crate_name) { 47 | self.do_dump_log(record); 48 | } 49 | } 50 | } 51 | 52 | fn flush(&self) {} 53 | } 54 | 55 | impl DumpLogger { 56 | fn do_dump_log(&self, record: &log::Record) { 57 | let timestamp: chrono::DateTime = chrono::Local::now(); 58 | let msg = format!( 59 | "[{} {:<5} {}] - {}", 60 | timestamp.format("%Y-%m-%d %H:%M:%S"), 61 | record.level(), 62 | record.module_path().unwrap_or(""), 63 | record.args() 64 | ); 65 | let c_msg = std::ffi::CString::new(msg).unwrap(); 66 | let ptr = c_msg.as_ptr(); 67 | if let Some(Some(cb)) = DUMP_CALLBACK.get() { 68 | unsafe { cb.clone().call(record.level().into(), ptr) }; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod android; 2 | mod api; 3 | mod config; 4 | mod dns; 5 | mod dump_logger; 6 | 7 | use hickory_proto::op::{Message, Query}; 8 | use moka::future::Cache; 9 | use socks5_impl::{ 10 | Error, Result, client, 11 | protocol::{Address, UserKey}, 12 | }; 13 | use std::{net::SocketAddr, sync::Arc, time::Duration}; 14 | use tokio::{ 15 | io::{AsyncReadExt, AsyncWriteExt, BufStream}, 16 | net::{TcpListener, TcpStream, ToSocketAddrs, UdpSocket}, 17 | }; 18 | 19 | pub use ::tokio_util::sync::CancellationToken; 20 | pub use api::{dns2socks_start, dns2socks_stop}; 21 | pub use config::{ArgProxy, ArgVerbosity, Config, ProxyType}; 22 | pub use dump_logger::dns2socks_set_log_callback; 23 | 24 | const MAX_BUFFER_SIZE: usize = 4096; 25 | 26 | pub async fn main_entry(config: Config, shutdown_token: tokio_util::sync::CancellationToken) -> Result<()> { 27 | log::info!("Starting DNS2Socks listening on {}...", config.listen_addr); 28 | let user_key = config.socks5_settings.credentials.clone(); 29 | 30 | let timeout = Duration::from_secs(config.timeout); 31 | 32 | let cache = create_dns_cache(); 33 | 34 | fn handle_error(res: Result, tokio::task::JoinError>, protocol: &str) { 35 | match res { 36 | Ok(Err(e)) => log::error!("{} error \"{}\"", protocol, e), 37 | Err(e) => log::error!("{} error \"{}\"", protocol, e), 38 | _ => {} 39 | } 40 | } 41 | 42 | tokio::select! { 43 | _ = shutdown_token.cancelled() => { 44 | log::info!("Shutdown received"); 45 | }, 46 | res = tokio::spawn(udp_thread(config.clone(), user_key.clone(), cache.clone(), timeout)) => { 47 | handle_error(res, "UDP"); 48 | }, 49 | res = tokio::spawn(tcp_thread(config, user_key, cache, timeout)) => { 50 | handle_error(res, "TCP"); 51 | }, 52 | } 53 | 54 | log::info!("DNS2Socks stopped"); 55 | 56 | Ok(()) 57 | } 58 | 59 | pub(crate) async fn udp_thread(opt: Config, user_key: Option, cache: Cache, Message>, timeout: Duration) -> Result<()> { 60 | let listener = match UdpSocket::bind(&opt.listen_addr).await { 61 | Ok(listener) => listener, 62 | Err(e) => { 63 | log::error!("UDP listener {} error \"{}\"", opt.listen_addr, e); 64 | return Err(e.into()); 65 | } 66 | }; 67 | let listener = Arc::new(listener); 68 | log::info!("Udp listening on: {}", opt.listen_addr); 69 | 70 | loop { 71 | let listener = listener.clone(); 72 | let opt = opt.clone(); 73 | let cache = cache.clone(); 74 | let auth = user_key.clone(); 75 | let block = async move { 76 | let mut buf = vec![0u8; MAX_BUFFER_SIZE]; 77 | let (len, src) = listener.recv_from(&mut buf).await?; 78 | buf.resize(len, 0); 79 | tokio::spawn(async move { 80 | if let Err(e) = udp_incoming_handler(listener, buf, src, opt, cache, auth, timeout).await { 81 | log::error!("DNS query via UDP incoming handler error \"{}\"", e); 82 | } 83 | }); 84 | Ok::<(), Error>(()) 85 | }; 86 | if let Err(e) = block.await { 87 | log::error!("UDP listener error \"{}\"", e); 88 | } 89 | } 90 | } 91 | 92 | async fn udp_incoming_handler( 93 | listener: Arc, 94 | mut buf: Vec, 95 | src: SocketAddr, 96 | opt: Config, 97 | cache: Cache, Message>, 98 | auth: Option, 99 | timeout: Duration, 100 | ) -> Result<()> { 101 | let message = dns::parse_data_to_dns_message(&buf, false)?; 102 | let domain = dns::extract_domain_from_dns_message(&message)?; 103 | 104 | if opt.cache_records { 105 | if let Some(cached_message) = dns_cache_get_message(&cache, &message).await { 106 | let data = cached_message.to_vec().map_err(|e| e.to_string())?; 107 | listener.send_to(&data, &src).await?; 108 | log_dns_message("DNS query via UDP cache hit", &domain, &cached_message); 109 | return Ok(()); 110 | } 111 | } 112 | 113 | let proxy_addr = opt.socks5_settings.addr; 114 | let dest_addr = opt.dns_remote_server; 115 | 116 | let data = if opt.force_tcp { 117 | let mut new_buf = (buf.len() as u16).to_be_bytes().to_vec(); 118 | new_buf.append(&mut buf); 119 | tcp_via_socks5_server(proxy_addr, dest_addr, auth, &new_buf, timeout) 120 | .await 121 | .map_err(|e| format!("querying \"{domain}\" {e}"))? 122 | } else { 123 | client::UdpClientImpl::datagram(proxy_addr, dest_addr, auth) 124 | .await 125 | .map_err(|e| format!("preparing to query \"{domain}\" {e}"))? 126 | .transfer_data(&buf, timeout) 127 | .await 128 | .map_err(|e| format!("querying \"{domain}\" {e}"))? 129 | }; 130 | let message = dns::parse_data_to_dns_message(&data, opt.force_tcp)?; 131 | let msg_buf = message.to_vec().map_err(|e| e.to_string())?; 132 | 133 | listener.send_to(&msg_buf, &src).await?; 134 | 135 | let prefix = format!("DNS query via {}", if opt.force_tcp { "TCP" } else { "UDP" }); 136 | log_dns_message(&prefix, &domain, &message); 137 | if opt.cache_records { 138 | dns_cache_put_message(&cache, &message).await; 139 | } 140 | Ok::<(), Error>(()) 141 | } 142 | 143 | pub(crate) async fn tcp_thread(opt: Config, user_key: Option, cache: Cache, Message>, timeout: Duration) -> Result<()> { 144 | let listener = match TcpListener::bind(&opt.listen_addr).await { 145 | Ok(listener) => listener, 146 | Err(e) => { 147 | log::error!("TCP listener {} error \"{}\"", opt.listen_addr, e); 148 | return Err(e.into()); 149 | } 150 | }; 151 | log::info!("TCP listening on: {}", opt.listen_addr); 152 | 153 | while let Ok((mut incoming, _)) = listener.accept().await { 154 | let opt = opt.clone(); 155 | let user_key = user_key.clone(); 156 | let cache = cache.clone(); 157 | tokio::spawn(async move { 158 | if let Err(e) = handle_tcp_incoming(&opt, user_key, cache, &mut incoming, timeout).await { 159 | log::error!("TCP error \"{}\"", e); 160 | } 161 | }); 162 | } 163 | Ok(()) 164 | } 165 | 166 | async fn handle_tcp_incoming( 167 | opt: &Config, 168 | auth: Option, 169 | cache: Cache, Message>, 170 | incoming: &mut TcpStream, 171 | timeout: Duration, 172 | ) -> Result<()> { 173 | let mut buf = [0u8; MAX_BUFFER_SIZE]; 174 | let n = incoming.read(&mut buf).await?; 175 | 176 | let message = dns::parse_data_to_dns_message(&buf[..n], true)?; 177 | let domain = dns::extract_domain_from_dns_message(&message)?; 178 | 179 | if opt.cache_records { 180 | if let Some(cached_message) = dns_cache_get_message(&cache, &message).await { 181 | let data = cached_message.to_vec().map_err(|e| e.to_string())?; 182 | let len = u16::try_from(data.len()).map_err(|e| e.to_string())?.to_be_bytes().to_vec(); 183 | let data = [len, data].concat(); 184 | incoming.write_all(&data).await?; 185 | log_dns_message("DNS query via TCP cache hit", &domain, &cached_message); 186 | return Ok(()); 187 | } 188 | } 189 | 190 | let proxy_addr = opt.socks5_settings.addr; 191 | let target_server = opt.dns_remote_server; 192 | let buf = tcp_via_socks5_server(proxy_addr, target_server, auth, &buf[..n], timeout).await?; 193 | 194 | incoming.write_all(&buf[..n]).await?; 195 | 196 | let message = dns::parse_data_to_dns_message(&buf[..n], true)?; 197 | log_dns_message("DNS query via TCP", &domain, &message); 198 | 199 | if opt.cache_records { 200 | dns_cache_put_message(&cache, &message).await; 201 | } 202 | 203 | Ok(()) 204 | } 205 | 206 | async fn tcp_via_socks5_server( 207 | proxy_addr: A, 208 | target_server: B, 209 | auth: Option, 210 | buf: &[u8], 211 | timeout: Duration, 212 | ) -> Result> 213 | where 214 | A: ToSocketAddrs, 215 | B: Into
, 216 | { 217 | let s5_proxy = TcpStream::connect(proxy_addr).await?; 218 | let mut stream = BufStream::new(s5_proxy); 219 | let _addr = client::connect(&mut stream, target_server, auth).await?; 220 | 221 | stream.write_all(buf).await?; 222 | stream.flush().await?; 223 | 224 | let mut buf = vec![0; MAX_BUFFER_SIZE]; 225 | let n = tokio::time::timeout(timeout, stream.read(&mut buf)).await??; 226 | Ok(buf[..n].to_vec()) 227 | } 228 | 229 | fn log_dns_message(prefix: &str, domain: &str, message: &Message) { 230 | let ipaddr = match dns::extract_ipaddr_from_dns_message(message) { 231 | Ok(ipaddr) => { 232 | format!("{:?}", ipaddr) 233 | } 234 | Err(e) => e.to_string(), 235 | }; 236 | log::trace!("{} {:?} <==> {:?}", prefix, domain, ipaddr); 237 | } 238 | 239 | pub(crate) fn create_dns_cache() -> Cache, Message> { 240 | Cache::builder() 241 | .time_to_live(Duration::from_secs(30 * 60)) 242 | .time_to_idle(Duration::from_secs(5 * 60)) 243 | .build() 244 | } 245 | 246 | pub(crate) async fn dns_cache_get_message(cache: &Cache, Message>, message: &Message) -> Option { 247 | if let Some(mut cached_message) = cache.get(&message.queries().to_vec()).await { 248 | cached_message.set_id(message.id()); 249 | return Some(cached_message); 250 | } 251 | None 252 | } 253 | 254 | pub(crate) async fn dns_cache_put_message(cache: &Cache, Message>, message: &Message) { 255 | cache.insert(message.queries().to_vec(), message.clone()).await; 256 | } 257 | --------------------------------------------------------------------------------