├── rustfmt.toml ├── .gitignore ├── .cargo └── config.toml ├── .github ├── dependabot.yml └── workflows │ ├── install-cross.sh │ ├── auto-merge.yml │ ├── rust.yml │ └── publish-exe.yml ├── cbindgen.toml ├── Cargo.toml ├── LICENSE ├── src ├── bin │ └── dns2socks.rs ├── dump_logger.rs ├── dns.rs ├── api.rs ├── android.rs ├── config.rs └── lib.rs └── README.md /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 140 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dns2socks.xcframework/ 2 | Cargo.lock 3 | .vscode/ 4 | .VSCodeCounter/ 5 | /target 6 | tmp/ 7 | build/ 8 | .idea/ 9 | .env 10 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /cbindgen.toml: -------------------------------------------------------------------------------- 1 | language = "C" 2 | cpp_compat = true 3 | 4 | [export] 5 | include = [ 6 | "dns2socks_start", 7 | "dns2socks_stop", 8 | "dns2socks_set_log_callback", 9 | ] 10 | exclude = [ 11 | "Java_com_github_shadowsocks_bg_Dns2socks_start", 12 | "Java_com_github_shadowsocks_bg_Dns2socks_stop", 13 | ] 14 | 15 | [export.rename] 16 | "ArgVerbosity" = "Dns2socksVerbosity" 17 | 18 | [enum] 19 | prefix_with_name = true 20 | -------------------------------------------------------------------------------- /.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@v5 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 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dns2socks" 3 | version = "0.2.3" 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 | name = "dns2socks_core" 13 | crate-type = ["staticlib", "cdylib", "rlib"] 14 | 15 | [dependencies] 16 | chrono = "0.4" 17 | clap = { version = "4", features = ["derive", "wrap_help"] } 18 | ctrlc2 = { version = "3", features = ["async", "termination"] } 19 | dotenvy = "0.15" 20 | env_logger = "0.11" 21 | hickory-proto = "0.25" 22 | log = "0.4" 23 | moka = { version = "0.12", default-features = false, features = ["future"] } 24 | percent-encoding = "2" 25 | rand = "0.9" 26 | socks5-impl = { version = "0.7", default-features = false, features = [ 27 | "client", 28 | ] } 29 | tokio = { version = "1", features = ["full"] } 30 | tokio-util = "0.7" 31 | url = "2" 32 | 33 | [target.'cfg(target_os="android")'.dependencies] 34 | android_logger = "0.15" 35 | jni = { version = "0.21", default-features = false } 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/bin/dns2socks.rs: -------------------------------------------------------------------------------- 1 | use dns2socks_core::{Config, LIB_NAME, 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!("{}={:?},{LIB_NAME}={:?}", module_path!(), config.verbosity, 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 | let async_ctrlc = ctrlc2::AsyncCtrlC::new(move || { 23 | log::info!("Ctrl-C received, exiting..."); 24 | shutdown_token.cancel(); 25 | true 26 | })?; 27 | 28 | if let Err(err) = join_handle.await { 29 | log::error!("main_entry error {}", err); 30 | } 31 | 32 | tokio::time::timeout(std::time::Duration::from_millis(100), async_ctrlc).await??; 33 | 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /.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 | if: ${{ !startsWith(github.ref, 'refs/tags/') }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | 16 | runs-on: ${{ matrix.os }} 17 | 18 | steps: 19 | - uses: actions/checkout@v5 20 | - uses: dtolnay/rust-toolchain@stable 21 | - name: rustfmt 22 | run: | 23 | rustc --version 24 | cargo fmt --all -- --check 25 | - name: check 26 | run: cargo check --verbose 27 | - name: clippy 28 | run: cargo clippy --all-targets --all-features -- -D warnings 29 | - name: Build 30 | run: cargo build --verbose --tests --all-features 31 | 32 | build_android: 33 | if: ${{ !startsWith(github.ref, 'refs/tags/') }} 34 | strategy: 35 | fail-fast: false 36 | runs-on: ubuntu-latest 37 | 38 | steps: 39 | - uses: actions/checkout@v5 40 | - uses: dtolnay/rust-toolchain@stable 41 | - name: Install cargo ndk and rust compiler for android target 42 | if: ${{ !cancelled() }} 43 | run: | 44 | cargo install --locked cargo-ndk 45 | rustup target add x86_64-linux-android 46 | - name: clippy 47 | if: ${{ !cancelled() }} 48 | run: cargo ndk -t x86_64 clippy --all-features -- -D warnings 49 | - name: Build 50 | if: ${{ !cancelled() }} 51 | run: cargo ndk -t x86_64 rustc --verbose --all-features --lib --crate-type=cdylib 52 | - name: Abort on error 53 | if: ${{ failure() }} 54 | run: echo "Android build job failed" && false 55 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 Ok(c_msg) = std::ffi::CString::new(msg) else { 66 | return; 67 | }; 68 | let ptr = c_msg.as_ptr(); 69 | if let Some(Some(cb)) = DUMP_CALLBACK.get() { 70 | unsafe { cb.clone().call(record.level().into(), ptr) }; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /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/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 Ok(listen_addr) = unsafe { std::ffi::CStr::from_ptr(listen_addr) }.to_str() else { 52 | return -3; 53 | }; 54 | let Ok(addr) = listen_addr.parse() else { 55 | return -4; 56 | }; 57 | config.listen_addr(addr); 58 | } 59 | if !dns_remote_server.is_null() { 60 | let Ok(dns_remote_server) = unsafe { std::ffi::CStr::from_ptr(dns_remote_server) }.to_str() else { 61 | return -5; 62 | }; 63 | let Ok(addr) = dns_remote_server.parse() else { 64 | return -6; 65 | }; 66 | config.dns_remote_server(addr); 67 | } 68 | if !socks5_settings.is_null() { 69 | let Ok(socks5_settings) = unsafe { std::ffi::CStr::from_ptr(socks5_settings) }.to_str() else { 70 | return -7; 71 | }; 72 | let Ok(proxy_settings) = crate::config::ArgProxy::try_from(socks5_settings) else { 73 | return -8; 74 | }; 75 | config.socks5_settings(proxy_settings); 76 | } 77 | 78 | let main_loop = async move { 79 | if let Err(err) = main_entry(config, shutdown_token).await { 80 | log::error!("main loop error: {}", err); 81 | return Err(err); 82 | } 83 | Ok(()) 84 | }; 85 | 86 | match tokio::runtime::Builder::new_multi_thread().enable_all().build() { 87 | Err(_e) => -9, 88 | Ok(rt) => match rt.block_on(main_loop) { 89 | Ok(_) => 0, 90 | Err(_e) => -10, 91 | }, 92 | } 93 | } 94 | 95 | /// # Safety 96 | /// 97 | /// Shutdown the dns2socks component. 98 | #[unsafe(no_mangle)] 99 | pub unsafe extern "C" fn dns2socks_stop() -> c_int { 100 | if let Ok(mut lock) = TUN_QUIT.lock() 101 | && let Some(shutdown_token) = lock.take() 102 | { 103 | shutdown_token.cancel(); 104 | return 0; 105 | } 106 | -1 107 | } 108 | -------------------------------------------------------------------------------- /.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 | fail-fast: false 13 | matrix: 14 | target: 15 | - x86_64-unknown-linux-gnu 16 | - x86_64-unknown-linux-musl 17 | - i686-unknown-linux-musl 18 | - aarch64-unknown-linux-gnu 19 | - armv7-unknown-linux-gnueabihf 20 | - x86_64-apple-darwin 21 | - aarch64-apple-darwin 22 | - x86_64-pc-windows-msvc 23 | - i686-pc-windows-msvc 24 | 25 | include: 26 | - target: x86_64-unknown-linux-gnu 27 | host_os: ubuntu-latest 28 | - target: x86_64-unknown-linux-musl 29 | host_os: ubuntu-latest 30 | - target: i686-unknown-linux-musl 31 | host_os: ubuntu-latest 32 | - target: aarch64-unknown-linux-gnu 33 | host_os: ubuntu-latest 34 | - target: armv7-unknown-linux-gnueabihf 35 | host_os: ubuntu-latest 36 | - target: x86_64-apple-darwin 37 | host_os: macos-latest 38 | - target: aarch64-apple-darwin 39 | host_os: macos-latest 40 | - target: x86_64-pc-windows-msvc 41 | host_os: windows-latest 42 | - target: i686-pc-windows-msvc 43 | host_os: windows-latest 44 | 45 | runs-on: ${{ matrix.host_os }} 46 | steps: 47 | - uses: actions/checkout@v5 48 | - uses: dtolnay/rust-toolchain@stable 49 | 50 | - name: Prepare 51 | if: ${{ !cancelled() }} 52 | shell: bash 53 | run: | 54 | cargo install cbindgen 55 | mkdir mypubdir4 56 | rustup target add ${{ matrix.target }} 57 | if [[ "${{ matrix.host_os }}" == "ubuntu-latest" ]]; then 58 | cargo install cross --git https://github.com/cross-rs/cross.git --rev e281947 59 | fi 60 | 61 | - name: Build 62 | if: ${{ !cancelled() }} 63 | shell: bash 64 | run: | 65 | if [[ "${{ matrix.host_os }}" == "ubuntu-latest" ]]; then 66 | cross build --all-features --release --target ${{ matrix.target }} 67 | else 68 | cargo build --all-features --release --target ${{ matrix.target }} 69 | fi 70 | cbindgen --config cbindgen.toml -o target/dns2socks.h 71 | if [[ "${{ matrix.host_os }}" == "windows-latest" ]]; then 72 | powershell -Command "(Get-Item README.md).LastWriteTime = Get-Date" 73 | powershell Compress-Archive -Path target/${{ matrix.target }}/release/dns2socks.exe, README.md, target/dns2socks.h, target/${{ matrix.target }}/release/dns2socks_core.dll -DestinationPath mypubdir4/dns2socks-${{ matrix.target }}.zip 74 | elif [[ "${{ matrix.host_os }}" == "macos-latest" ]]; then 75 | zip -j mypubdir4/dns2socks-${{ matrix.target }}.zip target/${{ matrix.target }}/release/dns2socks README.md target/dns2socks.h target/${{ matrix.target }}/release/libdns2socks_core.dylib 76 | if [[ "${{ matrix.target }}" == "x86_64-apple-darwin" ]]; then 77 | ./build-aarch64-apple-ios.sh 78 | zip -r mypubdir4/dns2socks-aarch64-apple-ios-xcframework.zip ./dns2socks.xcframework/ 79 | ./build-apple.sh 80 | zip -r mypubdir4/dns2socks-apple-xcframework.zip ./dns2socks.xcframework/ 81 | fi 82 | elif [[ "${{ matrix.host_os }}" == "ubuntu-latest" ]]; then 83 | zip -j mypubdir4/dns2socks-${{ matrix.target }}.zip target/${{ matrix.target }}/release/dns2socks README.md target/dns2socks.h target/${{ matrix.target }}/release/libdns2socks_core.so 84 | fi 85 | 86 | - name: Publish 87 | if: ${{ !cancelled() }} 88 | uses: softprops/action-gh-release@v2 89 | env: 90 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 91 | with: 92 | files: mypubdir4/* 93 | 94 | - name: Abort on error 95 | if: ${{ failure() }} 96 | run: echo "Publish job failed" && false 97 | -------------------------------------------------------------------------------- /src/android.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_os = "android")] 2 | 3 | use crate::{ArgProxy, ArgVerbosity, Config, LIB_NAME, main_entry}; 4 | use jni::{ 5 | JNIEnv, 6 | objects::{JClass, JString}, 7 | sys::{jboolean, jint}, 8 | }; 9 | 10 | static TUN_QUIT: std::sync::Mutex> = 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,{LIB_NAME}={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 | 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 | 97 | /// # Safety 98 | /// 99 | /// Shutdown dns2socks 100 | #[unsafe(no_mangle)] 101 | pub unsafe extern "C" fn Java_com_github_shadowsocks_bg_Dns2socks_stop(_env: JNIEnv, _: JClass) -> jint { 102 | if let Ok(mut lock) = TUN_QUIT.lock() 103 | && let Some(shutdown_token) = lock.take() 104 | { 105 | shutdown_token.cancel(); 106 | return 0; 107 | } 108 | -1 109 | } 110 | 111 | fn get_java_string(env: &mut JNIEnv, string: &JString) -> std::io::Result { 112 | use std::io::{Error, ErrorKind::Other}; 113 | Ok(env.get_string(string).map_err(|e| Error::new(Other, e))?.into()) 114 | } 115 | -------------------------------------------------------------------------------- /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/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 | pub const LIB_NAME: &str = "dns2socks_core"; 25 | 26 | const MAX_BUFFER_SIZE: usize = 4096; 27 | 28 | pub async fn main_entry(config: Config, shutdown_token: tokio_util::sync::CancellationToken) -> Result<()> { 29 | log::info!("Starting DNS2Socks listening on {}...", config.listen_addr); 30 | let user_key = config.socks5_settings.credentials.clone(); 31 | 32 | let timeout = Duration::from_secs(config.timeout); 33 | 34 | let cache = create_dns_cache(); 35 | 36 | fn handle_error(res: Result, tokio::task::JoinError>, protocol: &str) { 37 | match res { 38 | Ok(Err(e)) => log::error!("{} error \"{}\"", protocol, e), 39 | Err(e) => log::error!("{} error \"{}\"", protocol, e), 40 | _ => {} 41 | } 42 | } 43 | 44 | tokio::select! { 45 | _ = shutdown_token.cancelled() => { 46 | log::info!("Shutdown received"); 47 | }, 48 | res = tokio::spawn(udp_thread(config.clone(), user_key.clone(), cache.clone(), timeout)) => { 49 | handle_error(res, "UDP"); 50 | }, 51 | res = tokio::spawn(tcp_thread(config, user_key, cache, timeout)) => { 52 | handle_error(res, "TCP"); 53 | }, 54 | } 55 | 56 | log::info!("DNS2Socks stopped"); 57 | 58 | Ok(()) 59 | } 60 | 61 | pub(crate) async fn udp_thread(opt: Config, user_key: Option, cache: Cache, Message>, timeout: Duration) -> Result<()> { 62 | let listener = match UdpSocket::bind(&opt.listen_addr).await { 63 | Ok(listener) => listener, 64 | Err(e) => { 65 | log::error!("UDP listener {} error \"{}\"", opt.listen_addr, e); 66 | return Err(e.into()); 67 | } 68 | }; 69 | let listener = Arc::new(listener); 70 | log::info!("Udp listening on: {}", opt.listen_addr); 71 | 72 | loop { 73 | let listener = listener.clone(); 74 | let opt = opt.clone(); 75 | let cache = cache.clone(); 76 | let auth = user_key.clone(); 77 | let block = async move { 78 | let mut buf = vec![0u8; MAX_BUFFER_SIZE]; 79 | let (len, src) = listener.recv_from(&mut buf).await?; 80 | buf.resize(len, 0); 81 | tokio::spawn(async move { 82 | if let Err(e) = udp_incoming_handler(listener, buf, src, opt, cache, auth, timeout).await { 83 | log::error!("DNS query via UDP incoming handler error \"{}\"", e); 84 | } 85 | }); 86 | Ok::<(), Error>(()) 87 | }; 88 | if let Err(e) = block.await { 89 | log::error!("UDP listener error \"{}\"", e); 90 | } 91 | } 92 | } 93 | 94 | async fn udp_incoming_handler( 95 | listener: Arc, 96 | mut buf: Vec, 97 | src: SocketAddr, 98 | opt: Config, 99 | cache: Cache, Message>, 100 | auth: Option, 101 | timeout: Duration, 102 | ) -> Result<()> { 103 | let message = dns::parse_data_to_dns_message(&buf, false)?; 104 | let domain = dns::extract_domain_from_dns_message(&message)?; 105 | 106 | if opt.cache_records 107 | && let Some(cached_message) = dns_cache_get_message(&cache, &message).await 108 | { 109 | let data = cached_message.to_vec().map_err(|e| e.to_string())?; 110 | listener.send_to(&data, &src).await?; 111 | log_dns_message("DNS query via UDP cache hit", &domain, &cached_message); 112 | return Ok(()); 113 | } 114 | 115 | let proxy_addr = opt.socks5_settings.addr; 116 | let dest_addr = opt.dns_remote_server; 117 | 118 | let data = if opt.force_tcp { 119 | let mut new_buf = (buf.len() as u16).to_be_bytes().to_vec(); 120 | new_buf.append(&mut buf); 121 | tcp_via_socks5_server(proxy_addr, dest_addr, auth, &new_buf, timeout) 122 | .await 123 | .map_err(|e| format!("querying \"{domain}\" {e}"))? 124 | } else { 125 | client::UdpClientImpl::datagram(proxy_addr, dest_addr, auth) 126 | .await 127 | .map_err(|e| format!("preparing to query \"{domain}\" {e}"))? 128 | .transfer_data(&buf, timeout) 129 | .await 130 | .map_err(|e| format!("querying \"{domain}\" {e}"))? 131 | }; 132 | let message = dns::parse_data_to_dns_message(&data, opt.force_tcp)?; 133 | let msg_buf = message.to_vec().map_err(|e| e.to_string())?; 134 | 135 | listener.send_to(&msg_buf, &src).await?; 136 | 137 | let prefix = format!("DNS query via {}", if opt.force_tcp { "TCP" } else { "UDP" }); 138 | log_dns_message(&prefix, &domain, &message); 139 | if opt.cache_records { 140 | dns_cache_put_message(&cache, &message).await; 141 | } 142 | Ok::<(), Error>(()) 143 | } 144 | 145 | pub(crate) async fn tcp_thread(opt: Config, user_key: Option, cache: Cache, Message>, timeout: Duration) -> Result<()> { 146 | let listener = match TcpListener::bind(&opt.listen_addr).await { 147 | Ok(listener) => listener, 148 | Err(e) => { 149 | log::error!("TCP listener {} error \"{}\"", opt.listen_addr, e); 150 | return Err(e.into()); 151 | } 152 | }; 153 | log::info!("TCP listening on: {}", opt.listen_addr); 154 | 155 | while let Ok((mut incoming, _)) = listener.accept().await { 156 | let opt = opt.clone(); 157 | let user_key = user_key.clone(); 158 | let cache = cache.clone(); 159 | tokio::spawn(async move { 160 | if let Err(e) = handle_tcp_incoming(&opt, user_key, cache, &mut incoming, timeout).await { 161 | log::error!("TCP error \"{}\"", e); 162 | } 163 | }); 164 | } 165 | Ok(()) 166 | } 167 | 168 | async fn handle_tcp_incoming( 169 | opt: &Config, 170 | auth: Option, 171 | cache: Cache, Message>, 172 | incoming: &mut TcpStream, 173 | timeout: Duration, 174 | ) -> Result<()> { 175 | let mut buf = [0u8; MAX_BUFFER_SIZE]; 176 | let n = tokio::time::timeout(timeout, incoming.read(&mut buf)).await??; 177 | 178 | let message = dns::parse_data_to_dns_message(&buf[..n], true)?; 179 | let domain = dns::extract_domain_from_dns_message(&message)?; 180 | 181 | if opt.cache_records 182 | && let Some(cached_message) = dns_cache_get_message(&cache, &message).await 183 | { 184 | let data = cached_message.to_vec().map_err(|e| e.to_string())?; 185 | let len = u16::try_from(data.len()).map_err(|e| e.to_string())?.to_be_bytes().to_vec(); 186 | let data = [len, data].concat(); 187 | incoming.write_all(&data).await?; 188 | log_dns_message("DNS query via TCP cache hit", &domain, &cached_message); 189 | return Ok(()); 190 | } 191 | 192 | let proxy_addr = opt.socks5_settings.addr; 193 | let target_server = opt.dns_remote_server; 194 | let response_buf = tcp_via_socks5_server(proxy_addr, target_server, auth, &buf[..n], timeout).await?; 195 | 196 | incoming.write_all(&response_buf).await?; 197 | 198 | let message = dns::parse_data_to_dns_message(&response_buf, true)?; 199 | log_dns_message("DNS query via TCP", &domain, &message); 200 | 201 | if opt.cache_records { 202 | dns_cache_put_message(&cache, &message).await; 203 | } 204 | 205 | Ok(()) 206 | } 207 | 208 | async fn tcp_via_socks5_server( 209 | proxy_addr: A, 210 | target_server: B, 211 | auth: Option, 212 | buf: &[u8], 213 | timeout: Duration, 214 | ) -> Result> 215 | where 216 | A: ToSocketAddrs, 217 | B: Into
, 218 | { 219 | let s5_proxy = TcpStream::connect(proxy_addr).await?; 220 | let mut stream = BufStream::new(s5_proxy); 221 | let _addr = client::connect(&mut stream, target_server, auth).await?; 222 | 223 | stream.write_all(buf).await?; 224 | stream.flush().await?; 225 | 226 | let mut buf = vec![0; MAX_BUFFER_SIZE]; 227 | let n = tokio::time::timeout(timeout, stream.read(&mut buf)).await??; 228 | Ok(buf[..n].to_vec()) 229 | } 230 | 231 | fn log_dns_message(prefix: &str, domain: &str, message: &Message) { 232 | let ipaddr = match dns::extract_ipaddr_from_dns_message(message) { 233 | Ok(ipaddr) => { 234 | format!("{:?}", ipaddr) 235 | } 236 | Err(e) => e.to_string(), 237 | }; 238 | log::trace!("{} {:?} <==> {:?}", prefix, domain, ipaddr); 239 | } 240 | 241 | pub(crate) fn create_dns_cache() -> Cache, Message> { 242 | Cache::builder() 243 | .time_to_live(Duration::from_secs(30 * 60)) 244 | .time_to_idle(Duration::from_secs(5 * 60)) 245 | .build() 246 | } 247 | 248 | pub(crate) async fn dns_cache_get_message(cache: &Cache, Message>, message: &Message) -> Option { 249 | if let Some(mut cached_message) = cache.get(&message.queries().to_vec()).await { 250 | cached_message.set_id(message.id()); 251 | return Some(cached_message); 252 | } 253 | None 254 | } 255 | 256 | pub(crate) async fn dns_cache_put_message(cache: &Cache, Message>, message: &Message) { 257 | cache.insert(message.queries().to_vec(), message.clone()).await; 258 | } 259 | --------------------------------------------------------------------------------