Compare commits

...

16 commits

Author SHA1 Message Date
87f9336cc2 Update manpage and exclude binaries folder 2025-08-02 14:02:25 -05:00
12318ce9c0 Update builder.sh 2025-08-02 13:55:38 -05:00
c372db9eab (chore): update builder.sh
- Disable docker checks, we now use podman
- Use cross for all builds, otherwise the build ends up in glibc versions mismatch
2025-08-02 13:00:51 -05:00
9b329daa6a (chore): bump version. 2025-07-27 23:46:01 -05:00
206c392fc0 (fix): only print valid data to stdout
A regression was introduced during the refactoring that makes rusolver print all the domains, even if they were invalid, this fixes that
Fixes #14
2025-07-27 23:45:27 -05:00
28b29ca515 (fix): only print data if quiet_mode is not set. Also do not print wildcards 2025-07-27 17:43:22 -05:00
3bb0ac4b35 (chore): update manpage 2025-07-27 17:00:24 -05:00
55fd2cb2be (chore): update profile.release 2025-07-27 16:52:51 -05:00
37020360c2 (feat): refactor code
This refactor is aimed to provide better readability for future maintenance, at the same time that provides several performance improvements reducing the number of calls that we do to resolvers.
2025-07-27 16:19:33 -05:00
e06eeffb48 Update deps 2025-04-20 14:49:02 -05:00
7e683bd1ae Clippy fixes. 2025-04-20 14:48:57 -05:00
Eduard Tolosa
cf75cafee7
Use LibOptions for library options.
Signed-off-by: Eduard Tolosa <edu4rdshl@protonmail.com>
2022-04-29 12:08:50 -05:00
Eduard Tolosa
7755533e1e
Create make-release.yml 2022-04-12 05:46:39 -05:00
Edu4rdSHL
88ee0f9b58
Update builder.sh
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2022-03-11 15:32:21 -05:00
Edu4rdSHL
624a3febb9
Add function to return CNAME data.
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2022-03-09 22:52:06 -05:00
Edu4rdSHL
06ea060de9
Update custom trust-dns-resolver branch.
Signed-off-by: Edu4rdSHL <edu4rdshl@protonmail.com>
2022-02-13 14:27:25 -05:00
12 changed files with 2020 additions and 873 deletions

128
.github/workflows/make-release.yml vendored Normal file
View file

@ -0,0 +1,128 @@
name: Create and upload artifacts
on:
workflow_dispatch:
jobs:
build-nix:
env:
IN_PIPELINE: true
runs-on: ${{ matrix.os }}
strategy:
matrix:
type: [ubuntu-x64, ubuntu-x86, armv7, aarch64]
include:
- type: ubuntu-x64
os: ubuntu-latest
target: x86_64-unknown-linux-musl
name: rusolver-linux
path: target/x86_64-unknown-linux-musl/release/rusolver
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
- type: ubuntu-x86
os: ubuntu-latest
target: i686-unknown-linux-musl
name: rusolver-linux-i386
path: target/i686-unknown-linux-musl/release/rusolver
pkg_config_path: /usr/lib/i686-linux-gnu/pkgconfig
- type: armv7
os: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
name: rusolver-armv7
path: target/armv7-unknown-linux-gnueabihf/release/rusolver
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
- type: aarch64
os: ubuntu-latest
target: aarch64-unknown-linux-gnu
name: rusolver-aarch64
path: target/aarch64-unknown-linux-gnu/release/rusolver
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
steps:
- uses: actions/checkout@v2
- name: Install System Dependencies
run: |
env
sudo apt-get update
sudo apt-get install -y --no-install-recommends libssl-dev pkg-config gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: ${{ matrix.target }}
override: true
- uses: actions-rs/cargo@v1
env:
PKG_CONFIG_PATH: ${{ matrix.pkg_config_path }}
OPENSSL_DIR: /usr/lib/ssl
with:
use-cross: true
command: build
args: --release --target=${{ matrix.target }}
- name: Strip symbols from binary
run: |
strip -s ${{ matrix.path }} || arm-linux-gnueabihf-strip -s ${{ matrix.path }} || aarch64-linux-gnu-strip -s ${{ matrix.path }}
- uses: actions/upload-artifact@v2
with:
name: ${{ matrix.name }}
path: ${{ matrix.path }}
build-macos:
env:
IN_PIPELINE: true
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: x86_64-apple-darwin
override: true
- uses: actions-rs/cargo@v1
with:
use-cross: true
command: build
args: --release --target=x86_64-apple-darwin
- name: Strip symbols from binary
run: |
strip -u -r target/x86_64-apple-darwin/release/rusolver
- uses: actions/upload-artifact@v2
with:
name: rusolver-osx
path: target/x86_64-apple-darwin/release/rusolver
- uses: actions/upload-artifact@v2
with:
name: rusolver-osx
path: rusolver-osx
build-windows:
env:
IN_PIPELINE: true
runs-on: ${{ matrix.os }}
strategy:
matrix:
type: [windows-x64, windows-x86]
include:
- type: windows-x64
os: windows-latest
target: x86_64-pc-windows-msvc
name: rusolver-windows.exe
path: target\x86_64-pc-windows-msvc\release\rusolver.exe
- type: windows-x86
os: windows-latest
target: i686-pc-windows-msvc
name: rusolver-windows-i686.exe
path: target\i686-pc-windows-msvc\release\rusolver.exe
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: ${{ matrix.target }}
override: true
- uses: actions-rs/cargo@v1
with:
use-cross: true
command: build
args: --release --target=${{ matrix.target }}
- uses: actions/upload-artifact@v2
with:
name: ${{ matrix.name }}
path: ${{ matrix.path }}

1
.gitignore vendored
View file

@ -3,3 +3,4 @@
resolvers resolvers
.gitignore .gitignore
wordlist wordlist
/ghbinaries

2104
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
[package] [package]
name = "rusolver" name = "rusolver"
version = "0.9.1" version = "0.10.2"
authors = ["Eduard Tolosa <edu4rdshl@protonmail.com>"] authors = ["Eduard Tolosa <edu4rdshl@protonmail.com>"]
edition = "2018" edition = "2021"
description = "Fast DNS resolver written in Rust." description = "Fast DNS resolver written in Rust."
repository = "https://github.com/edu4rdshl/rusolver" repository = "https://github.com/edu4rdshl/rusolver"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
@ -12,20 +12,14 @@ readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
tokio = { version = "1.16.1", features = ["full"] } tokio = { version = "1.47.0", features = ["full"] }
futures = "0.3.21" futures = "0.3.31"
trust-dns-resolver = "0.20.4" hickory-resolver = "0.25.2"
# trust-dns-resolver = { git = "https://github.com/Edu4rdSHL/trust-dns", branch = "main" } clap = { version = "4.5.41", features = ["derive"] }
clap = "2.33.4" rand = "0.9.2"
rand = "0.8.4"
# https://github.com/bluejekyll/trust-dns/pull/1632
[patch.crates-io]
trust-dns-resolver = { git = "https://github.com/Findomain/trust-dns", package = "trust-dns-resolver", branch = "custombranch" }
[profile.release] [profile.release]
lto = true lto = true
codegen-units = 1 codegen-units = 1
panic = 'abort' panic = 'abort'
incremental = false strip = true
opt-level = "s"

View file

@ -1,5 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Rusolver releaser
NAME="rusolver" NAME="rusolver"
LINUX_TARGET="x86_64-unknown-linux-musl" LINUX_TARGET="x86_64-unknown-linux-musl"
@ -7,16 +6,21 @@ LINUX_X86_TARGET="i686-unknown-linux-musl"
WIN_TARGET="x86_64-pc-windows-gnu" WIN_TARGET="x86_64-pc-windows-gnu"
ARMV7_TARGET="armv7-unknown-linux-gnueabihf" ARMV7_TARGET="armv7-unknown-linux-gnueabihf"
AARCH_TARGET="aarch64-unknown-linux-gnu" AARCH_TARGET="aarch64-unknown-linux-gnu"
OSX_TARGET="x86_64-apple-darwin" # OSX_TARGET="x86_64-apple-darwin"
MANPAGE_DIR="./$NAME.1" MANPAGE_DIR="./$NAME.1"
BIN_OUTPUT_DIR="./ghbinaries"
rm -rf "$BIN_OUTPUT_DIR"
mkdir -p "$BIN_OUTPUT_DIR"
# Linux build # Linux build
echo "Building Linux artifact." echo "Building Linux artifact."
if cargo build -q --release --target="$LINUX_TARGET"; then if cross build -q --release --target="$LINUX_TARGET"; then
echo "Linux artifact build: SUCCESS" echo "Linux artifact build: SUCCESS"
cp "target/$LINUX_TARGET/release/$NAME" "target/$LINUX_TARGET/release/$NAME-linux" cp "target/$LINUX_TARGET/release/$NAME" "target/$LINUX_TARGET/release/$NAME-linux"
strip "target/$LINUX_TARGET/release/$NAME-linux" strip "target/$LINUX_TARGET/release/$NAME-linux"
sha512sum "target/$LINUX_TARGET/release/$NAME-linux" >"target/$LINUX_TARGET/release/$NAME-linux.sha512" sha512sum "target/$LINUX_TARGET/release/$NAME-linux" >"$BIN_OUTPUT_DIR/$NAME-linux.sha512"
zip -q -j "$BIN_OUTPUT_DIR/$NAME-linux-x64.zip" "target/$LINUX_TARGET/release/$NAME-linux"
else else
echo "Linux artifact build: FAILED" echo "Linux artifact build: FAILED"
fi fi
@ -27,7 +31,8 @@ if cross build -q --release --target="$LINUX_X86_TARGET"; then
echo "Linux x86 artifact build: SUCCESS" echo "Linux x86 artifact build: SUCCESS"
cp "target/$LINUX_X86_TARGET/release/$NAME" "target/$LINUX_X86_TARGET/release/$NAME-linux-i386" cp "target/$LINUX_X86_TARGET/release/$NAME" "target/$LINUX_X86_TARGET/release/$NAME-linux-i386"
strip "target/$LINUX_X86_TARGET/release/$NAME-linux-i386" strip "target/$LINUX_X86_TARGET/release/$NAME-linux-i386"
sha512sum "target/$LINUX_X86_TARGET/release/$NAME-linux-i386" >"target/$LINUX_X86_TARGET/release/$NAME-linux-i386.sha512" sha512sum "target/$LINUX_X86_TARGET/release/$NAME-linux-i386" >"$BIN_OUTPUT_DIR/$NAME-linux-i386.sha512"
zip -q -j "$BIN_OUTPUT_DIR/$NAME-linux-i386.zip" "target/$LINUX_X86_TARGET/release/$NAME-linux-i386"
else else
echo "Linux x86 artifact build: FAILED" echo "Linux x86 artifact build: FAILED"
fi fi
@ -38,7 +43,8 @@ if cross build -q --release --target="$WIN_TARGET"; then
echo "Windows artifact build: SUCCESS" echo "Windows artifact build: SUCCESS"
cp "target/$WIN_TARGET/release/$NAME.exe" "target/$WIN_TARGET/release/$NAME-windows.exe" cp "target/$WIN_TARGET/release/$NAME.exe" "target/$WIN_TARGET/release/$NAME-windows.exe"
strip "target/$WIN_TARGET/release/$NAME-windows.exe" strip "target/$WIN_TARGET/release/$NAME-windows.exe"
sha512sum "target/$WIN_TARGET/release/$NAME-windows.exe" >"target/$WIN_TARGET/release/$NAME-windows.exe.sha512" sha512sum "target/$WIN_TARGET/release/$NAME-windows.exe" >"$BIN_OUTPUT_DIR/$NAME-windows.exe.sha512"
zip -q -j "$BIN_OUTPUT_DIR/$NAME-windows.zip" "target/$WIN_TARGET/release/$NAME-windows.exe"
else else
echo "Windows artifact build: FAILED" echo "Windows artifact build: FAILED"
fi fi
@ -49,7 +55,8 @@ if cross build -q --release --target="$ARMV7_TARGET"; then
echo "ARMv7 artifact build: SUCCESS" echo "ARMv7 artifact build: SUCCESS"
cp "target/$ARMV7_TARGET/release/$NAME" "target/$ARMV7_TARGET/release/$NAME-armv7" cp "target/$ARMV7_TARGET/release/$NAME" "target/$ARMV7_TARGET/release/$NAME-armv7"
strip "target/$ARMV7_TARGET/release/$NAME-armv7" strip "target/$ARMV7_TARGET/release/$NAME-armv7"
sha512sum "target/$ARMV7_TARGET/release/$NAME-armv7" >"target/$ARMV7_TARGET/release/$NAME-armv7.sha512" sha512sum "target/$ARMV7_TARGET/release/$NAME-armv7" >"$BIN_OUTPUT_DIR/$NAME-armv7.sha512"
zip -q -j "$BIN_OUTPUT_DIR/$NAME-armv7.zip" "target/$ARMV7_TARGET/release/$NAME-armv7"
else else
echo "ARMv7 artifact build: FAILED" echo "ARMv7 artifact build: FAILED"
fi fi
@ -60,21 +67,23 @@ if cross build -q --release --target="$AARCH_TARGET"; then
echo "Aarch64 artifact build: SUCCESS" echo "Aarch64 artifact build: SUCCESS"
cp "target/$AARCH_TARGET/release/$NAME" "target/$AARCH_TARGET/release/$NAME-aarch64" cp "target/$AARCH_TARGET/release/$NAME" "target/$AARCH_TARGET/release/$NAME-aarch64"
strip "target/$AARCH_TARGET/release/$NAME-aarch64" strip "target/$AARCH_TARGET/release/$NAME-aarch64"
sha512sum "target/$AARCH_TARGET/release/$NAME-aarch64" >"target/$AARCH_TARGET/release/$NAME-aarch64.sha512" sha512sum "target/$AARCH_TARGET/release/$NAME-aarch64" >"$BIN_OUTPUT_DIR/$NAME-aarch64.sha512"
zip -q -j "$BIN_OUTPUT_DIR/$NAME-aarch64.zip" "target/$AARCH_TARGET/release/$NAME-aarch64"
else else
echo "Aarch64 artifact build: FAILED" echo "Aarch64 artifact build: FAILED"
fi fi
# Mac OS build # # Mac OS build
echo "Building OSX artifact." # echo "Building OSX artifact."
if CC=o64-clang CXX=o64-clang++ LIBZ_SYS_STATIC=1 cargo build -q --release --target="$OSX_TARGET"; then # if CC=o64-clang CXX=o64-clang++ LIBZ_SYS_STATIC=1 cargo build -q --release --target="$OSX_TARGET"; then
echo "OSX artifact build: SUCCESS" # echo "OSX artifact build: SUCCESS"
cp "target/$OSX_TARGET/release/$NAME" "target/$OSX_TARGET/release/$NAME-osx" # cp "target/$OSX_TARGET/release/$NAME" "target/$OSX_TARGET/release/$NAME-osx"
strip "target/$OSX_TARGET/release/$NAME-osx" # strip "target/$OSX_TARGET/release/$NAME-osx"
sha512sum "target/$OSX_TARGET/release/$NAME-osx" >"target/$OSX_TARGET/release/$NAME-osx.sha512" # sha512sum "target/$OSX_TARGET/release/$NAME-osx" >"target/$OSX_TARGET/release/$NAME-osx.sha512"
else # zip -q "$BIN_OUTPUT_DIR/$NAME-osx.zip" "target/$OSX_TARGET/release/$NAME-osx"
echo "OSX artifact build: FAILED" # else
fi # echo "OSX artifact build: FAILED"
# fi
echo "Creating manpage..." echo "Creating manpage..."
if command -v help2man >/dev/null; then if command -v help2man >/dev/null; then
@ -86,19 +95,3 @@ if command -v help2man >/dev/null; then
else else
echo "Please install the help2man package." echo "Please install the help2man package."
fi fi
#if command -v git >/dev/null; then
# git add .
# git commit -m "Bump version."
# git push
#fi
#echo "Uploading crate to crates.io..."
#if cargo publish --no-verify > /dev/null; then
# echo "Crate uploaded."
#else
# echo "An error has occurred while uploading the crate to crates.io."
# exit
#fi
echo "All builds have passed!"

View file

@ -1,56 +1,52 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.48.5. .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
.TH RUSOLVER "1" "February 2022" "Rusolver 0.9.1" "User Commands" .TH RUSOLVER "1" "August 2025" "rusolver 0.10.2" "User Commands"
.SH NAME .SH NAME
Rusolver \- manual page for Rusolver 0.9.1 rusolver \- manual page for rusolver 0.10.2
.SH SYNOPSIS
.B rusolver
[\fI\,OPTIONS\/\fR]
.SH DESCRIPTION .SH DESCRIPTION
Rusolver 0.9.1
Eduard Tolosa <edu4rdshl@protonmail.com>
Fast DNS resolver written in Rust. Fast DNS resolver written in Rust.
.SS "USAGE:" .SH OPTIONS
.IP
rusolver [FLAGS] [OPTIONS]
.SS "FLAGS:"
.TP .TP
\fB\-h\fR, \fB\-\-help\fR \fB\-t\fR, \fB\-\-threads\fR <THREADS>
Prints help information Number of threads. Default: 100 [default: 100]
.TP .TP
\fB\-i\fR, \fB\-\-ip\fR \fB\-\-retries\fR <RETRIES>
Show the discovered IP addresses. Default: false Number of retries after lookup failure before giving up. Defaults to 0 [default: 0]
.TP .TP
\fB\-\-no\-verify\fR \fB\-d\fR, \fB\-\-domain\fR <DOMAIN>
Disables the double verification algorithm for valid subdomains \fB\-NOT\fR RECOMMENDED\-. Default: false Target domain. When it's specified, a wordlist can be used from stdin for bruteforcing.
.TP .TP
\fB\-q\fR, \fB\-\-quiet\fR \fB\-r\fR, \fB\-\-resolvers\fR <RESOLVERS>
Enables quiet mode.
.TP
\fB\-V\fR, \fB\-\-version\fR
Prints version information
.SS "OPTIONS:"
.TP
\fB\-d\fR, \fB\-\-domain\fR <domain>
Target domain. When it's specified, a wordlist can be used from stdin for
bruteforcing.
.TP
\fB\-r\fR, \fB\-\-resolvers\fR <resolvers>
File with DNS ips. File with DNS ips.
.TP .TP
\fB\-\-retries\fR <retries> \fB\-\-timeout\fR <TIMEOUT>
Number of retries after lookup failure before giving up. Defaults to 0 Timeout in seconds. Default: 3 [default: 3]
.TP .TP
\fB\-t\fR, \fB\-\-threads\fR <threads> \fB\-i\fR, \fB\-\-ip\fR
Number of threads. Default: 100 Display the record data.
.TP .TP
\fB\-\-timeout\fR <timeout> \fB\-e\fR, \fB\-\-enable\-double\-check\fR
Timeout in seconds. Default: 3 Enable the double verification algorithm for subdomains. Default: false
.TP
\fB\-q\fR, \fB\-\-quiet\-flag\fR
Quiet mode, no output except errors.
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help
.TP
\fB\-V\fR, \fB\-\-version\fR
Print version
.SH "SEE ALSO" .SH "SEE ALSO"
The full documentation for The full documentation for
.B Rusolver .B rusolver
is maintained as a Texinfo manual. If the is maintained as a Texinfo manual. If the
.B info .B info
and and
.B Rusolver .B rusolver
programs are properly installed at your site, the command programs are properly installed at your site, the command
.IP .IP
.B info Rusolver .B info rusolver
.PP .PP
should give you access to the complete manual. should give you access to the complete manual.

View file

@ -0,0 +1,46 @@
use clap::{arg, Parser};
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Args {
#[arg(
short,
long,
default_value_t = 100,
help = "Number of threads. Default: 100"
)]
pub threads: usize,
#[arg(
long,
default_value_t = 0,
help = "Number of retries after lookup failure before giving up. Defaults to 0"
)]
pub retries: usize,
#[arg(
short,
long,
help = "Target domain. When it's specified, a wordlist can be used from stdin for bruteforcing."
)]
pub domain: Option<String>,
#[arg(short, long, help = "File with DNS ips.")]
pub resolvers: Option<String>,
#[arg(long, default_value_t = 3, help = "Timeout in seconds. Default: 3")]
pub timeout: u64,
#[arg(short, long, help = "Display the record data.")]
pub ip: bool,
#[arg(
short,
long,
help = "Enable the double verification algorithm for subdomains. Default: false"
)]
pub enable_double_check: bool,
#[arg(short, long, help = "Quiet mode, no output except errors.")]
pub quiet_flag: bool,
}

View file

@ -1,65 +1,109 @@
use { use {
crate::structs::DomainData, crate::{
structs::{DomainData, LibOptions},
utils::print_domain_data,
},
futures::stream::{self, StreamExt}, futures::stream::{self, StreamExt},
std::collections::{HashMap, HashSet}, hickory_resolver::{
trust_dns_resolver::config::ResolverOpts, config::{NameServerConfig, NameServerConfigGroup, ResolverConfig, ResolverOpts},
}; name_server::TokioConnectionProvider,
proto::{rr::RecordType, xfer::Protocol},
use { TokioResolver,
std::net::SocketAddr, },
trust_dns_resolver::{ std::{
config::{NameServerConfig, NameServerConfigGroup, Protocol, ResolverConfig}, collections::{HashMap, HashSet},
name_server::{GenericConnection, GenericConnectionProvider, TokioRuntime}, net::SocketAddr,
AsyncResolver, TokioAsyncResolver,
}, },
}; };
pub fn return_tokio_asyncresolver( #[must_use]
nameserver_ips: HashSet<String>, pub fn return_tokio_asyncresolver<S: ::std::hash::BuildHasher>(
nameserver_ips: &HashSet<String, S>,
options: ResolverOpts, options: ResolverOpts,
) -> AsyncResolver<GenericConnection, GenericConnectionProvider<TokioRuntime>> { ) -> TokioResolver {
let mut name_servers = NameServerConfigGroup::with_capacity(nameserver_ips.len() * 2); let mut name_servers = NameServerConfigGroup::with_capacity(nameserver_ips.len());
name_servers.extend(nameserver_ips.into_iter().flat_map(|server| { name_servers.extend(nameserver_ips.iter().map(|server| {
let socket_addr = SocketAddr::V4(match server.parse() { let socket_addr = SocketAddr::V4(server.parse().unwrap_or_else(|e| {
Ok(a) => a, panic!(
Err(e) => unreachable!(
"Error parsing the server {}, only IPv4 are allowed. Error: {}", "Error parsing the server {}, only IPv4 are allowed. Error: {}",
server, e server, e
), )
}); }));
std::iter::once(NameServerConfig { NameServerConfig {
socket_addr, socket_addr,
protocol: Protocol::Udp, protocol: Protocol::Udp,
tls_dns_name: None, tls_dns_name: None,
trust_nx_responses: false, http_endpoint: None,
}) trust_negative_responses: false,
.chain(std::iter::once(NameServerConfig { bind_addr: None,
socket_addr, }
protocol: Protocol::Tcp,
tls_dns_name: None,
trust_nx_responses: false,
}))
})); }));
TokioAsyncResolver::tokio( TokioResolver::builder_with_config(
ResolverConfig::from_parts(None, vec![], name_servers), ResolverConfig::from_parts(None, vec![], name_servers),
options, TokioConnectionProvider::default(),
) )
.unwrap() .with_options(options)
.build()
} }
#[allow(clippy::too_many_arguments)] pub async fn return_hosts_data(options: &LibOptions) -> HashMap<String, DomainData> {
pub async fn return_hosts_data( let threads = if options.hosts.len() < options.threads {
hosts: HashSet<String>, options.hosts.len()
resolvers: AsyncResolver<GenericConnection, GenericConnectionProvider<TokioRuntime>>, } else {
trustable_resolver: AsyncResolver<GenericConnection, GenericConnectionProvider<TokioRuntime>>, options.threads
wildcard_ips: HashSet<String>, };
stream::iter(options.hosts.clone().into_iter().map(|host| {
let lookup_host = format!("{}.", host.trim_end_matches('.'));
let wildcard_ips = options.wildcard_ips.clone();
let mut domain_data = DomainData::default();
async move {
let ip_lookup = if options.enable_double_check {
match (
options.resolvers.ipv4_lookup(lookup_host.clone()).await,
options.trustable_resolvers.ipv4_lookup(lookup_host).await,
) {
(Ok(_), Ok(ip)) => Some(ip),
_ => None,
}
} else {
options.resolvers.ipv4_lookup(lookup_host).await.ok()
};
if let Some(ip_lookup) = ip_lookup {
for ip in ip_lookup.iter() {
domain_data.ipv4_addresses.insert(ip.to_string());
}
}
domain_data.is_wildcard = domain_data
.ipv4_addresses
.iter()
.all(|ip| wildcard_ips.contains(ip));
if !options.quiet_flag {
print_domain_data(&host, &domain_data, options);
}
(host, domain_data)
}
}))
.buffer_unordered(threads)
.collect::<HashMap<String, DomainData>>()
.await
}
// Used internally for now
pub async fn return_cname_data<S: ::std::hash::BuildHasher>(
hosts: HashSet<String, S>,
resolver: TokioResolver,
trustable_resolver: TokioResolver,
disable_double_check: bool, disable_double_check: bool,
mut threads: usize, mut threads: usize,
show_ip_addresses: bool,
quiet_flag: bool,
) -> HashMap<String, DomainData> { ) -> HashMap<String, DomainData> {
if hosts.len() < threads { if hosts.len() < threads {
threads = hosts.len(); threads = hosts.len();
@ -67,37 +111,33 @@ pub async fn return_hosts_data(
stream::iter(hosts) stream::iter(hosts)
.map(|host| { .map(|host| {
let resolver_fut = resolvers.ipv4_lookup(host.trim_end_matches('.').to_owned() + "."); let host = host.trim_end_matches('.').to_owned();
let trustable_resolver_fut = let fqdn = format!("{host}.");
trustable_resolver.ipv4_lookup(host.trim_end_matches('.').to_owned() + ".");
let wildcard_ips = wildcard_ips.clone();
let mut domain_data = DomainData::default(); let resolver = resolver.clone();
let trustable_resolver = trustable_resolver.clone();
async move { async move {
if let Ok(ip) = resolver_fut.await { let cname_lookup = if disable_double_check {
if disable_double_check { resolver.lookup(fqdn.clone(), RecordType::CNAME).await.ok()
domain_data.ipv4_addresses = ip } else {
.into_iter() match (
.map(|x| x.to_string()) resolver.lookup(fqdn.clone(), RecordType::CNAME).await,
.collect::<HashSet<String>>(); trustable_resolver.lookup(fqdn, RecordType::CNAME).await,
} else if let Ok(ip) = trustable_resolver_fut.await { ) {
domain_data.ipv4_addresses = ip (Ok(_), Ok(lookup)) => Some(lookup),
.into_iter() _ => None,
.map(|x| x.to_string())
.collect::<HashSet<String>>();
} }
} };
domain_data.is_wildcard = domain_data
.ipv4_addresses
.iter()
.all(|ip| wildcard_ips.contains(ip));
if !quiet_flag { let mut domain_data = DomainData::default();
if show_ip_addresses && !domain_data.is_wildcard {
println!("{};{:?}", host, domain_data.ipv4_addresses); if let Some(lookup) = cname_lookup {
} else if !domain_data.is_wildcard { for record in lookup.iter() {
println!("{}", host) if let Some(cname) = record.as_cname() {
domain_data.cname = cname.to_string();
break;
}
} }
} }

View file

@ -1,3 +1,4 @@
pub mod args;
pub mod dnslib; pub mod dnslib;
pub mod structs; pub mod structs;
pub mod utils; pub mod utils;

View file

@ -1,98 +1,24 @@
use { use {
clap::{value_t, App, Arg}, clap::Parser,
rusolver::{dnslib, utils}, rusolver::{args::Args, dnslib, structs, utils},
std::collections::HashSet, std::collections::HashSet,
tokio::{ tokio::{
self, self,
io::{self, AsyncReadExt}, io::{self, AsyncReadExt},
}, },
trust_dns_resolver::config::{LookupIpStrategy, ResolverOpts},
}; };
// Please add support for AAAA, TXT, SRV, NAPTR, PTR, CNAME, DNAME, MX, NS, SOA, LOC, SVCB, HTTPS, SPF, CAA and AVC resource records. // WIP: add support for AAAA, TXT, SRV, NAPTR, PTR, CNAME, DNAME, MX, NS, SOA, LOC, SVCB, HTTPS, SPF, CAA and AVC resource records.
// This could use a new command line option such as -t, e.g. echo www.example.com | rusolver -i -t AAAA. It might also make sense // This could use a new command line option such as -t, e.g. echo www.example.com | rusolver -i -t AAAA. It might also make sense
// to change -i/--ip to -d/--data with the text Display the record data. // to change -i/--ip to -d/--data with the text Display the record data.
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Eval args // Eval args
let matches = App::new("Rusolver") let args = Args::parse();
.version(clap::crate_version!())
.author("Eduard Tolosa <edu4rdshl@protonmail.com>")
.about("Fast DNS resolver written in Rust.")
.arg(
Arg::with_name("threads")
.short("t")
.long("threads")
.takes_value(true)
.help("Number of threads. Default: 100"),
)
.arg(
Arg::with_name("retries")
.long("retries")
.takes_value(true)
.help("Number of retries after lookup failure before giving up. Defaults to 0"),
)
.arg(
Arg::with_name("domain")
.short("d")
.long("domain")
.takes_value(true)
.help("Target domain. When it's specified, a wordlist can be used from stdin for bruteforcing."),
)
.arg(
Arg::with_name("resolvers")
.short("r")
.long("resolvers")
.takes_value(true)
.help("File with DNS ips."),
)
.arg(
Arg::with_name("timeout")
.long("timeout")
.takes_value(true)
.help("Timeout in seconds. Default: 3"),
)
.arg(
Arg::with_name("ip")
.short("i")
.long("ip")
.takes_value(false)
.help("Show the discovered IP addresses. Default: false"),
)
.arg(
Arg::with_name("no-verify")
.long("no-verify")
.takes_value(false)
.help("Disables the double verification algorithm for valid subdomains -NOT RECOMMENDED-. Default: false"),
)
.arg(
Arg::with_name("quiet")
.short("q")
.long("quiet")
.takes_value(false)
.help("Enables quiet mode."),
)
.get_matches();
// Assign values or use defaults
let show_ip_adress = matches.is_present("ip");
let threads = value_t!(matches.value_of("threads"), usize).unwrap_or_else(|_| 100);
let timeout = value_t!(matches.value_of("timeout"), u64).unwrap_or_else(|_| 3);
let retries = value_t!(matches.value_of("retries"), usize).unwrap_or_else(|_| 0);
let quiet_flag = matches.is_present("quiet");
let custom_resolvers = matches.is_present("resolvers");
let disable_double_check = matches.is_present("no-verify") && custom_resolvers;
// Resolver opts // Resolver opts
let options = ResolverOpts { let options = utils::return_resolver_opts(args.timeout, args.retries);
timeout: std::time::Duration::from_secs(timeout),
attempts: retries,
ip_strategy: LookupIpStrategy::Ipv4Only,
num_concurrent_reqs: 1,
shuffle_dns_servers: true,
..Default::default()
};
let built_in_nameservers: HashSet<String> = vec![ let built_in_nameservers: HashSet<String> = vec![
// Cloudflare // Cloudflare
@ -110,30 +36,23 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Verisign // Verisign
"64.6.64.6:53", "64.6.64.6:53",
"64.6.65.6:53", "64.6.65.6:53",
// UncensoredDNS
"91.239.100.100:53",
"89.233.43.71:53",
// dns.watch
"84.200.69.80:53",
"84.200.70.40:53",
] ]
.iter() .iter()
.map(|x| x.to_string()) .map(ToString::to_string)
.collect(); .collect();
// Create resolvers // Create resolvers
let mut nameserver_ips; let mut nameserver_ips;
if custom_resolvers { if args.resolvers.is_some() {
nameserver_ips = nameserver_ips = utils::return_file_lines(&args.resolvers.unwrap()).await;
utils::return_file_lines(value_t!(matches.value_of("resolvers"), String).unwrap())
.await;
nameserver_ips.retain(|ip| !ip.is_empty()); nameserver_ips.retain(|ip| !ip.is_empty());
} else { } else {
nameserver_ips = built_in_nameservers.clone(); nameserver_ips = built_in_nameservers.clone();
} }
let resolvers = dnslib::return_tokio_asyncresolver(nameserver_ips, options);
let trustable_resolver = dnslib::return_tokio_asyncresolver(built_in_nameservers, options); let resolvers = dnslib::return_tokio_asyncresolver(&nameserver_ips, options.clone());
let trustable_resolvers = dnslib::return_tokio_asyncresolver(&built_in_nameservers, options);
let mut wildcard_ips = HashSet::new(); let mut wildcard_ips = HashSet::new();
// Read stdin // Read stdin
@ -141,28 +60,30 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut stdin = io::stdin(); let mut stdin = io::stdin();
stdin.read_to_string(&mut buffer).await?; stdin.read_to_string(&mut buffer).await?;
let hosts: HashSet<String> = if matches.is_present("domain") { let hosts: HashSet<String> = if args.domain.is_some() {
let domain = value_t!(matches, "domain", String).unwrap(); let domain = args.domain.unwrap();
wildcard_ips = utils::detect_wildcards(&domain, &trustable_resolver, quiet_flag).await; wildcard_ips =
utils::detect_wildcards(&domain, &trustable_resolvers, args.quiet_flag).await;
buffer buffer
.lines() .lines()
.map(|word| format!("{}.{}", word, domain)) .map(|word| format!("{word}.{domain}"))
.collect() .collect()
} else { } else {
buffer.lines().map(str::to_owned).collect() buffer.lines().map(str::to_owned).collect()
}; };
dnslib::return_hosts_data( let options = structs::LibOptions {
hosts, hosts,
resolvers, resolvers,
trustable_resolver, trustable_resolvers,
wildcard_ips, wildcard_ips,
disable_double_check, enable_double_check: args.enable_double_check,
threads, threads: args.threads,
show_ip_adress, show_ip_address: args.ip,
false, quiet_flag: args.quiet_flag,
) };
.await;
dnslib::return_hosts_data(&options).await;
Ok(()) Ok(())
} }

View file

@ -1,6 +1,6 @@
use std::collections::HashSet; use {hickory_resolver::TokioResolver, std::collections::HashSet};
#[derive(Clone, Debug)] #[derive(Clone, Debug, Default)]
pub struct DomainData { pub struct DomainData {
pub ipv4_addresses: HashSet<String>, pub ipv4_addresses: HashSet<String>,
pub ipv6_addresses: HashSet<String>, pub ipv6_addresses: HashSet<String>,
@ -8,13 +8,14 @@ pub struct DomainData {
pub is_wildcard: bool, pub is_wildcard: bool,
} }
impl Default for DomainData { #[derive(Clone, Debug)]
fn default() -> Self { pub struct LibOptions {
DomainData { pub hosts: HashSet<String>,
ipv4_addresses: HashSet::new(), pub resolvers: TokioResolver,
ipv6_addresses: HashSet::new(), pub trustable_resolvers: TokioResolver,
cname: String::from(""), pub wildcard_ips: HashSet<String>,
is_wildcard: false, pub enable_double_check: bool,
} pub threads: usize,
} pub show_ip_address: bool,
pub quiet_flag: bool,
} }

View file

@ -1,81 +1,109 @@
use { use {
crate::structs::{DomainData, LibOptions},
futures::stream::{self, StreamExt}, futures::stream::{self, StreamExt},
rand::{distributions::Alphanumeric, thread_rng as rng, Rng}, hickory_resolver::{
std::{collections::HashSet, net::Ipv4Addr}, config::{LookupIpStrategy, ResolverOpts, ServerOrderingStrategy},
tokio::{self, fs::File, io::AsyncReadExt}, TokioResolver,
trust_dns_resolver::{
name_server::{GenericConnection, GenericConnectionProvider, TokioRuntime},
AsyncResolver,
}, },
rand::{distr::Alphanumeric, rng, Rng},
std::{collections::HashSet, net::Ipv4Addr},
tokio::{fs::File, io::AsyncReadExt},
}; };
// In the future I may need to implement error propagation, but for now it's fine pub async fn return_file_lines(file: &str) -> HashSet<String> {
// to deal with matches let mut f = match File::open(file).await {
pub async fn return_file_lines(file: String) -> HashSet<String> {
let mut f = match File::open(&file).await {
Ok(file) => file, Ok(file) => file,
Err(e) => { Err(e) => {
eprintln!("Error opening resolvers file. Error: {}", e); eprintln!("Error opening resolvers file. Error: {e}");
std::process::exit(1) std::process::exit(1)
} }
}; };
let mut buffer = String::new(); let mut buffer = String::new();
match f.read_to_string(&mut buffer).await { (f.read_to_string(&mut buffer).await)
Ok(a) => a, .unwrap_or_else(|_| unreachable!("Error reading to string."));
_ => unreachable!("Error reading to string."),
}; let estimated_lines = buffer.matches('\n').count() + 1;
buffer.lines().map(|f| format!("{}:53", f)).collect() let mut result = HashSet::with_capacity(estimated_lines);
for line in buffer.lines() {
if !line.is_empty() {
result.insert(format!("{line}:53"));
}
}
result
} }
pub async fn detect_wildcards( pub async fn detect_wildcards(
target: &str, target: &str,
resolvers: &AsyncResolver<GenericConnection, GenericConnectionProvider<TokioRuntime>>, resolvers: &TokioResolver,
quiet_flag: bool, quiet_flag: bool,
) -> HashSet<String> { ) -> HashSet<String> {
if !quiet_flag { if !quiet_flag {
println!("Running wildcards detection for {}...\n", target) println!("Running wildcards detection for {target}...\n");
} }
let mut generated_wilcards: HashSet<String> = HashSet::new();
let mut generated_wildcards = HashSet::with_capacity(19);
// Generate random subdomains for wildcard detection
for _ in 1..20 { for _ in 1..20 {
generated_wilcards.insert(format!( let random_subdomain: String = rng()
"{}.{}.", .sample_iter(Alphanumeric)
rng() .take(15)
.sample_iter(Alphanumeric) .map(char::from)
.take(15) .collect();
.map(char::from) generated_wildcards.insert(format!("{random_subdomain}.{target}."));
.collect::<String>(),
target
));
} }
generated_wilcards = stream::iter(generated_wilcards.clone().into_iter().map( let wildcard_ips: HashSet<String> = stream::iter(generated_wildcards.into_iter())
|host| async move { .map(|host| async move {
if let Ok(ips) = resolvers.ipv4_lookup(host.clone()).await { resolvers.ipv4_lookup(host).await.map_or_else(
ips.into_iter() |_| Vec::new(),
.map(|x| x.to_string()) |ips| {
.collect::<Vec<String>>() ips.into_iter()
} else { .filter_map(|ip| {
Vec::new() let ip_str = ip.to_string();
} if ip_str.parse::<Ipv4Addr>().is_ok() {
}, Some(ip_str)
)) } else {
.buffer_unordered(10) None
.map(stream::iter) }
.flatten() })
.collect() .collect::<Vec<String>>()
.await; },
)
})
.buffer_unordered(10)
.map(stream::iter)
.flatten()
.collect()
.await;
generated_wilcards.retain(|ip| ip.parse::<Ipv4Addr>().is_ok()); if !wildcard_ips.is_empty() && !quiet_flag {
println!("Wildcards detected for {target} and wildcard's IP saved for further work.");
if !generated_wilcards.is_empty() && !quiet_flag { println!("Wildcard IPs: {wildcard_ips:?}\n");
println!(
"Wilcards detected for {} and wildcard's IP saved for furter work.",
target
);
println!("Wilcard IPs: {:?}\n", generated_wilcards)
} else if !quiet_flag { } else if !quiet_flag {
println!("No wilcards detected for {}, nice!\n", target) println!("No wildcards detected for {target}, nice!\n");
} }
generated_wilcards wildcard_ips
}
pub fn print_domain_data(host: &str, domain_data: &DomainData, options: &LibOptions) {
if !domain_data.is_wildcard && !domain_data.ipv4_addresses.is_empty() {
if options.show_ip_address {
println!("{}:{:?}", host, domain_data.ipv4_addresses);
} else {
println!("{}", host);
}
}
}
pub fn return_resolver_opts(timeout: u64, retries: usize) -> ResolverOpts {
let mut options = ResolverOpts::default();
options.timeout = std::time::Duration::from_secs(timeout);
options.attempts = retries;
options.ip_strategy = LookupIpStrategy::Ipv4Only;
options.num_concurrent_reqs = 1;
options.server_ordering_strategy = ServerOrderingStrategy::RoundRobin;
options
} }