First full working version.
This commit is contained in:
commit
a543458658
23
.githooks/install-hooks.sh
Executable file
23
.githooks/install-hooks.sh
Executable file
@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to install git hooks
|
||||
|
||||
HOOK_DIR="$(git rev-parse --git-dir)/hooks"
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
|
||||
echo "Installing git hooks..."
|
||||
|
||||
# Copy pre-commit hook
|
||||
if [ -f "$SCRIPT_DIR/pre-commit" ]; then
|
||||
cp "$SCRIPT_DIR/pre-commit" "$HOOK_DIR/pre-commit"
|
||||
chmod +x "$HOOK_DIR/pre-commit"
|
||||
echo "✓ Installed pre-commit hook"
|
||||
else
|
||||
echo "✗ pre-commit hook not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Git hooks installed successfully!"
|
||||
echo
|
||||
echo "To bypass hooks for a commit, use: git commit --no-verify"
|
||||
echo "To uninstall hooks, delete files in: $HOOK_DIR"
|
||||
94
.githooks/pre-commit
Executable file
94
.githooks/pre-commit
Executable file
@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Running pre-commit checks..."
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
}
|
||||
|
||||
# Check if cargo is available
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
print_error "Cargo is not installed or not in PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for staged Rust files
|
||||
staged_rust_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(rs)$' || true)
|
||||
|
||||
if [ -z "$staged_rust_files" ]; then
|
||||
print_warning "No Rust files staged for commit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Staged Rust files:"
|
||||
echo "$staged_rust_files"
|
||||
echo
|
||||
|
||||
# 1. Check formatting
|
||||
echo "📝 Checking code formatting..."
|
||||
if ! cargo fmt -- --check; then
|
||||
print_error "Code formatting check failed!"
|
||||
echo "Run 'cargo fmt' to fix formatting issues"
|
||||
exit 1
|
||||
fi
|
||||
print_status "Code formatting is correct"
|
||||
|
||||
# 2. Run Clippy
|
||||
echo "📎 Running Clippy linter..."
|
||||
if ! cargo clippy --all-features --all-targets -- -D warnings; then
|
||||
print_error "Clippy found issues!"
|
||||
echo "Fix the issues above or run 'cargo clippy --fix' for automatic fixes"
|
||||
exit 1
|
||||
fi
|
||||
print_status "Clippy checks passed"
|
||||
|
||||
# 3. Run tests
|
||||
echo "🧪 Running tests..."
|
||||
if ! cargo test --all-features; then
|
||||
print_error "Tests failed!"
|
||||
echo "Fix failing tests before committing"
|
||||
exit 1
|
||||
fi
|
||||
print_status "All tests passed"
|
||||
|
||||
# 4. Check for security vulnerabilities (optional - only if cargo-audit is installed)
|
||||
if command -v cargo-audit &> /dev/null; then
|
||||
echo "🔒 Running security audit..."
|
||||
if ! cargo audit; then
|
||||
print_error "Security vulnerabilities found!"
|
||||
echo "Review and fix security issues before committing"
|
||||
exit 1
|
||||
fi
|
||||
print_status "Security audit passed"
|
||||
else
|
||||
print_warning "cargo-audit not installed. Run 'cargo install cargo-audit' to enable security checks"
|
||||
fi
|
||||
|
||||
# 5. Check that the project builds
|
||||
echo "🔨 Building project..."
|
||||
if ! cargo build --all-features; then
|
||||
print_error "Build failed!"
|
||||
exit 1
|
||||
fi
|
||||
print_status "Build successful"
|
||||
|
||||
echo
|
||||
echo -e "${GREEN}🎉 All pre-commit checks passed!${NC}"
|
||||
echo "Proceeding with commit..."
|
||||
80
.github/workflows/ci.yml
vendored
Normal file
80
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Run tests
|
||||
run: cargo test --all-features
|
||||
|
||||
fmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
- name: Check formatting
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
name: Clippy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Run clippy
|
||||
run: cargo clippy --all-features --all-targets -- -D warnings
|
||||
|
||||
security_audit:
|
||||
name: Security Audit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: rustsec/audit-check@v1.4.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
coverage:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: llvm-tools-preview
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- name: Generate coverage
|
||||
run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: lcov.info
|
||||
fail_ci_if_error: true
|
||||
62
.github/workflows/release.yml
vendored
Normal file
62
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
artifact_name: nettest
|
||||
asset_name: nettest-linux-x86_64
|
||||
- os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
artifact_name: nettest.exe
|
||||
asset_name: nettest-windows-x86_64.exe
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
artifact_name: nettest
|
||||
asset_name: nettest-macos-x86_64
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Build
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.asset_name }}
|
||||
path: target/${{ matrix.target }}/release/${{ matrix.artifact_name }}
|
||||
|
||||
release:
|
||||
name: Release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
nettest-linux-x86_64/nettest
|
||||
nettest-windows-x86_64.exe/nettest.exe
|
||||
nettest-macos-x86_64/nettest
|
||||
body_path: CHANGELOG.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
71
.gitignore
vendored
Normal file
71
.gitignore
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
# Rust artifacts
|
||||
/target/
|
||||
Cargo.lock
|
||||
|
||||
# IDE and editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# OS generated files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Flamegraph output
|
||||
flamegraph.svg
|
||||
perf.data*
|
||||
|
||||
# MacOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
Icon
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
*.backup
|
||||
|
||||
# Test artifacts
|
||||
test-results/
|
||||
test-output/
|
||||
|
||||
# Benchmarks
|
||||
criterion/
|
||||
39
.pre-commit-config.yaml
Normal file
39
.pre-commit-config.yaml
Normal file
@ -0,0 +1,39 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-toml
|
||||
- id: check-json
|
||||
- id: check-merge-conflict
|
||||
- id: check-case-conflict
|
||||
- id: check-added-large-files
|
||||
args: ['--maxkb=1000']
|
||||
- id: detect-private-key
|
||||
|
||||
- repo: https://github.com/doublify/pre-commit-rust
|
||||
rev: v1.0
|
||||
hooks:
|
||||
- id: fmt
|
||||
args: ['--verbose', '--']
|
||||
- id: cargo-check
|
||||
args: ['--all-features']
|
||||
- id: clippy
|
||||
args: ['--all-features', '--all-targets', '--', '-D', 'warnings']
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: cargo-test
|
||||
name: cargo test
|
||||
entry: cargo test --all-features
|
||||
language: system
|
||||
types: [rust]
|
||||
pass_filenames: false
|
||||
- id: cargo-audit
|
||||
name: cargo audit
|
||||
entry: bash -c 'if command -v cargo-audit &> /dev/null; then cargo audit; else echo "cargo-audit not installed, skipping security audit"; fi'
|
||||
language: system
|
||||
types: [rust]
|
||||
pass_filenames: false
|
||||
89
Cargo.toml
Normal file
89
Cargo.toml
Normal file
@ -0,0 +1,89 @@
|
||||
[package]
|
||||
name = "nettest"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
authors = ["Network Test Tool"]
|
||||
description = "A comprehensive network connectivity and DNS testing CLI tool"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/example/nettest"
|
||||
keywords = ["network", "dns", "testing", "connectivity", "cli"]
|
||||
categories = ["command-line-utilities", "network-programming"]
|
||||
|
||||
[[bin]]
|
||||
name = "nettest"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
trust-dns-resolver = "0.23"
|
||||
trust-dns-client = "0.23"
|
||||
socket2 = "0.5"
|
||||
pnet = "0.34"
|
||||
anyhow = "1.0"
|
||||
thiserror = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
colored = "2.0"
|
||||
indicatif = "0.17"
|
||||
rand = "0.8"
|
||||
futures = "0.3"
|
||||
libc = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
criterion = "0.5"
|
||||
proptest = "1.0"
|
||||
pretty_assertions = "1.0"
|
||||
|
||||
[lints.clippy]
|
||||
all = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
# Allow some pedantic lints that can be overly strict
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
module_name_repetitions = "allow"
|
||||
must_use_candidate = "allow"
|
||||
return_self_not_must_use = "allow"
|
||||
# Allow multiple crate versions (common in dependency trees)
|
||||
multiple_crate_versions = "allow"
|
||||
# Allow some formatting preferences
|
||||
uninlined_format_args = "allow"
|
||||
# Allow some code style preferences
|
||||
use_self = "allow"
|
||||
wildcard_imports = "allow"
|
||||
# Allow missing const for builder patterns
|
||||
missing_const_for_fn = "allow"
|
||||
# Allow casting precision loss for statistical calculations
|
||||
cast_precision_loss = "allow"
|
||||
# Allow redundant closures for clarity
|
||||
redundant_closure = "allow"
|
||||
# Allow inefficient to_string for readability
|
||||
inefficient_to_string = "allow"
|
||||
# Allow ignored unit patterns
|
||||
ignored_unit_patterns = "allow"
|
||||
# Allow needless borrows for readability
|
||||
needless_borrows_for_generic_args = "allow"
|
||||
# Allow match same arms for clear documentation
|
||||
match_same_arms = "allow"
|
||||
# Allow redundant closures for method calls
|
||||
redundant_closure_for_method_calls = "allow"
|
||||
# Allow functions with many lines for complex logic
|
||||
too_many_lines = "allow"
|
||||
# Allow single match else patterns
|
||||
single_match_else = "allow"
|
||||
# Allow redundant pattern matching for clarity
|
||||
redundant_pattern_matching = "allow"
|
||||
# Allow unused self in methods that may need it later
|
||||
unused_self = "allow"
|
||||
# Allow single component path imports for clarity
|
||||
single_component_path_imports = "allow"
|
||||
# Allow complex boolean expressions in tests
|
||||
overly_complex_bool_expr = "allow"
|
||||
# Allow map_unwrap_or patterns
|
||||
map_unwrap_or = "allow"
|
||||
# Allow collapsible else if patterns
|
||||
collapsible_else_if = "allow"
|
||||
214
README.md
Normal file
214
README.md
Normal file
@ -0,0 +1,214 @@
|
||||
# NetTest - Network Connectivity Testing Tool
|
||||
|
||||
A comprehensive command-line tool written in Rust for testing network connectivity and DNS resolution across various dimensions.
|
||||
|
||||
## Features
|
||||
|
||||
### Network Testing
|
||||
- **IPv4 and IPv6 support** - Test connectivity using both IP versions
|
||||
- **Multiple protocols** - Support for TCP, UDP, and ICMP
|
||||
- **Port testing** - Test common ports and custom port ranges
|
||||
- **Timeout configuration** - Configurable timeouts for all tests
|
||||
|
||||
### MTU Discovery
|
||||
- **Binary search MTU discovery** - Efficiently find the maximum MTU size
|
||||
- **Common MTU testing** - Test standard MTU sizes (68, 576, 1280, 1500, 4464, 9000)
|
||||
- **Custom range testing** - Test specific MTU ranges
|
||||
- **IPv4 and IPv6 support** - MTU discovery for both IP versions
|
||||
|
||||
### DNS Testing
|
||||
- **Comprehensive record types** - A, AAAA, MX, NS, TXT, CNAME, SOA, PTR, and more
|
||||
- **Multiple DNS servers** - Test against Google, Cloudflare, Quad9, OpenDNS, and others
|
||||
- **TCP and UDP queries** - Support for both DNS transport protocols
|
||||
- **Large query testing** - Test handling of large DNS responses
|
||||
- **International domains** - Support for IDN (Internationalized Domain Names)
|
||||
|
||||
### Domain Category Testing
|
||||
- **Normal websites** - Test legitimate, commonly used sites
|
||||
- **Ad networks** - Test advertising and tracking domains
|
||||
- **Spam domains** - Test temporary email and spam-associated domains
|
||||
- **Adult content** - Test adult content sites (often filtered)
|
||||
- **Malicious domains** - Test known malicious/phishing domains
|
||||
- **Social media** - Test major social media platforms
|
||||
- **Streaming services** - Test video and music streaming sites
|
||||
- **Gaming platforms** - Test gaming services and platforms
|
||||
- **News websites** - Test major news and media sites
|
||||
|
||||
### DNS Filtering Analysis
|
||||
- **Filter effectiveness** - Analyze how well DNS filtering is working
|
||||
- **Category-based analysis** - See which categories are being blocked
|
||||
- **Detailed reporting** - Get statistics on resolution success rates
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <repository-url>
|
||||
cd nettest
|
||||
|
||||
# Build the project
|
||||
cargo build --release
|
||||
|
||||
# Install globally (optional)
|
||||
cargo install --path .
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Commands
|
||||
|
||||
```bash
|
||||
# Run comprehensive tests on a target
|
||||
nettest full google.com
|
||||
|
||||
# Test TCP connectivity
|
||||
nettest network tcp google.com --port 80
|
||||
|
||||
# Test UDP connectivity
|
||||
nettest network udp 8.8.8.8 --port 53
|
||||
|
||||
# Ping test
|
||||
nettest network ping google.com --count 4
|
||||
|
||||
# Test common ports
|
||||
nettest network ports google.com --protocol tcp
|
||||
|
||||
# DNS query
|
||||
nettest dns query google.com --record-type a
|
||||
|
||||
# Test DNS servers
|
||||
nettest dns servers google.com
|
||||
|
||||
# Test domain categories
|
||||
nettest dns categories --category normal
|
||||
|
||||
# MTU discovery
|
||||
nettest mtu discover google.com
|
||||
|
||||
# Test common MTU sizes
|
||||
nettest mtu common google.com
|
||||
```
|
||||
|
||||
### Advanced Options
|
||||
|
||||
```bash
|
||||
# Specify IP version
|
||||
nettest network tcp google.com --ip-version v4
|
||||
nettest network tcp google.com --ip-version v6
|
||||
nettest network tcp google.com --ip-version both
|
||||
|
||||
# Custom timeout
|
||||
nettest --timeout 10 network tcp google.com
|
||||
|
||||
# JSON output
|
||||
nettest --json dns query google.com
|
||||
|
||||
# Verbose logging
|
||||
nettest --verbose full google.com
|
||||
|
||||
# DNS query with specific server
|
||||
nettest dns query google.com --server 8.8.8.8:53 --tcp
|
||||
|
||||
# Custom MTU range
|
||||
nettest mtu range google.com --min 1000 --max 1500
|
||||
```
|
||||
|
||||
### Domain Category Testing
|
||||
|
||||
Test different categories of domains to analyze DNS filtering:
|
||||
|
||||
```bash
|
||||
# Test normal websites
|
||||
nettest dns categories --category normal
|
||||
|
||||
# Test ad networks
|
||||
nettest dns categories --category ads
|
||||
|
||||
# Test all categories
|
||||
nettest dns categories --category all
|
||||
|
||||
# DNS filtering effectiveness
|
||||
nettest dns filtering
|
||||
```
|
||||
|
||||
### Comprehensive Testing
|
||||
|
||||
The `full` command runs a comprehensive suite of tests:
|
||||
|
||||
```bash
|
||||
# Full test suite for a domain
|
||||
nettest full example.com
|
||||
|
||||
# Full test with specific IP version
|
||||
nettest full example.com --ip-version v4
|
||||
```
|
||||
|
||||
This includes:
|
||||
- TCP and UDP connectivity tests
|
||||
- ICMP ping tests
|
||||
- MTU discovery
|
||||
- DNS resolution tests
|
||||
- DNS server tests
|
||||
|
||||
## Output Formats
|
||||
|
||||
### Human-readable (default)
|
||||
Colored, formatted output suitable for terminal viewing.
|
||||
|
||||
### JSON
|
||||
Machine-readable JSON output for integration with other tools:
|
||||
|
||||
```bash
|
||||
nettest --json dns query google.com
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Rust 1.70 or later
|
||||
- Root/administrator privileges may be required for:
|
||||
- ICMP ping tests
|
||||
- Raw socket operations
|
||||
- MTU discovery
|
||||
|
||||
## Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
cargo test
|
||||
|
||||
# Integration tests
|
||||
cargo test --test integration_tests
|
||||
|
||||
# All tests with verbose output
|
||||
cargo test -- --nocapture
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The tool is organized into several modules:
|
||||
|
||||
- **cli** - Command-line argument parsing and interface
|
||||
- **network** - TCP, UDP, and ICMP connectivity testing
|
||||
- **dns** - DNS resolution and query testing
|
||||
- **mtu** - MTU discovery and testing
|
||||
- **utils** - Common utilities and error handling
|
||||
|
||||
## Security Considerations
|
||||
|
||||
This tool is designed for defensive security testing and network diagnostics. It:
|
||||
|
||||
- Tests legitimate connectivity to verify network functionality
|
||||
- Analyzes DNS filtering effectiveness
|
||||
- Discovers network path characteristics
|
||||
- Does not attempt to exploit or attack systems
|
||||
- Respects rate limits and timeouts
|
||||
|
||||
## License
|
||||
|
||||
[Add your license here]
|
||||
|
||||
## Contributing
|
||||
|
||||
[Add contribution guidelines here]
|
||||
14
rustfmt.toml
Normal file
14
rustfmt.toml
Normal file
@ -0,0 +1,14 @@
|
||||
# Stable Rust rustfmt configuration
|
||||
max_width = 100
|
||||
hard_tabs = false
|
||||
tab_spaces = 4
|
||||
newline_style = "Unix"
|
||||
use_small_heuristics = "Default"
|
||||
edition = "2021"
|
||||
merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
force_explicit_abi = true
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
||||
remove_nested_parens = true
|
||||
236
src/cli/mod.rs
Normal file
236
src/cli/mod.rs
Normal file
@ -0,0 +1,236 @@
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "nettest")]
|
||||
#[command(about = "A comprehensive network connectivity and DNS testing CLI tool")]
|
||||
#[command(version)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
|
||||
#[arg(short, long, global = true)]
|
||||
pub verbose: bool,
|
||||
|
||||
#[arg(short, long, global = true, default_value = "5")]
|
||||
pub timeout: u64,
|
||||
|
||||
#[arg(long, global = true)]
|
||||
pub json: bool,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
#[command(about = "Test network connectivity")]
|
||||
Network {
|
||||
#[command(subcommand)]
|
||||
command: NetworkCommands,
|
||||
},
|
||||
#[command(about = "Test DNS resolution")]
|
||||
Dns {
|
||||
#[command(subcommand)]
|
||||
command: DnsCommands,
|
||||
},
|
||||
#[command(about = "Discover MTU sizes")]
|
||||
Mtu {
|
||||
#[command(subcommand)]
|
||||
command: MtuCommands,
|
||||
},
|
||||
#[command(about = "Run comprehensive tests")]
|
||||
Full {
|
||||
target: String,
|
||||
#[arg(short, long, value_enum, default_value = "both")]
|
||||
ip_version: IpVersionArg,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum NetworkCommands {
|
||||
#[command(about = "Test TCP connectivity")]
|
||||
Tcp {
|
||||
target: String,
|
||||
#[arg(short, long, default_value = "80")]
|
||||
port: u16,
|
||||
#[arg(short, long, value_enum, default_value = "both")]
|
||||
ip_version: IpVersionArg,
|
||||
},
|
||||
#[command(about = "Test UDP connectivity")]
|
||||
Udp {
|
||||
target: String,
|
||||
#[arg(short, long, default_value = "53")]
|
||||
port: u16,
|
||||
#[arg(short, long, value_enum, default_value = "both")]
|
||||
ip_version: IpVersionArg,
|
||||
},
|
||||
#[command(about = "Test ICMP ping")]
|
||||
Ping {
|
||||
target: String,
|
||||
#[arg(short, long, default_value = "4")]
|
||||
count: u32,
|
||||
#[arg(short, long, value_enum, default_value = "both")]
|
||||
ip_version: IpVersionArg,
|
||||
},
|
||||
#[command(about = "Test common ports")]
|
||||
Ports {
|
||||
target: String,
|
||||
#[arg(short, long, value_enum, default_value = "tcp")]
|
||||
protocol: ProtocolArg,
|
||||
#[arg(short, long, value_enum, default_value = "both")]
|
||||
ip_version: IpVersionArg,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum DnsCommands {
|
||||
#[command(about = "Query specific DNS record")]
|
||||
Query {
|
||||
domain: String,
|
||||
#[arg(short, long, value_enum, default_value = "a")]
|
||||
record_type: RecordTypeArg,
|
||||
#[arg(short, long)]
|
||||
server: Option<SocketAddr>,
|
||||
#[arg(long)]
|
||||
tcp: bool,
|
||||
},
|
||||
#[command(about = "Test DNS servers")]
|
||||
Servers {
|
||||
domain: String,
|
||||
#[arg(short, long, value_enum, default_value = "a")]
|
||||
record_type: RecordTypeArg,
|
||||
},
|
||||
#[command(about = "Test domain categories")]
|
||||
Categories {
|
||||
#[arg(short, long, value_enum)]
|
||||
category: Option<CategoryArg>,
|
||||
#[arg(short, long, value_enum, default_value = "a")]
|
||||
record_type: RecordTypeArg,
|
||||
},
|
||||
#[command(about = "Test DNS filtering effectiveness")]
|
||||
Filtering,
|
||||
#[command(about = "Show system DNS configuration")]
|
||||
Debug,
|
||||
#[command(about = "Comprehensive DNS tests")]
|
||||
Comprehensive { domain: String },
|
||||
#[command(about = "Test large DNS queries")]
|
||||
Large { domain: String },
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum MtuCommands {
|
||||
#[command(about = "Discover MTU for target")]
|
||||
Discover {
|
||||
target: String,
|
||||
#[arg(short, long, value_enum, default_value = "both")]
|
||||
ip_version: IpVersionArg,
|
||||
},
|
||||
#[command(about = "Test common MTU sizes")]
|
||||
Common {
|
||||
target: String,
|
||||
#[arg(short, long, value_enum, default_value = "both")]
|
||||
ip_version: IpVersionArg,
|
||||
},
|
||||
#[command(about = "Test custom MTU range")]
|
||||
Range {
|
||||
target: String,
|
||||
#[arg(short, long, default_value = "68")]
|
||||
min: u16,
|
||||
#[arg(short, long, default_value = "1500")]
|
||||
max: u16,
|
||||
#[arg(short, long, value_enum, default_value = "both")]
|
||||
ip_version: IpVersionArg,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, ValueEnum)]
|
||||
pub enum IpVersionArg {
|
||||
V4,
|
||||
V6,
|
||||
Both,
|
||||
}
|
||||
|
||||
impl IpVersionArg {
|
||||
pub fn to_versions(&self) -> Vec<crate::network::IpVersion> {
|
||||
match self {
|
||||
IpVersionArg::V4 => vec![crate::network::IpVersion::V4],
|
||||
IpVersionArg::V6 => vec![crate::network::IpVersion::V6],
|
||||
IpVersionArg::Both => {
|
||||
vec![crate::network::IpVersion::V4, crate::network::IpVersion::V6]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, ValueEnum)]
|
||||
pub enum ProtocolArg {
|
||||
Tcp,
|
||||
Udp,
|
||||
Both,
|
||||
}
|
||||
|
||||
#[derive(Clone, ValueEnum)]
|
||||
pub enum RecordTypeArg {
|
||||
A,
|
||||
AAAA,
|
||||
MX,
|
||||
NS,
|
||||
TXT,
|
||||
CNAME,
|
||||
SOA,
|
||||
PTR,
|
||||
All,
|
||||
}
|
||||
|
||||
impl RecordTypeArg {
|
||||
pub fn to_record_type(&self) -> Vec<trust_dns_client::rr::RecordType> {
|
||||
match self {
|
||||
RecordTypeArg::A => vec![trust_dns_client::rr::RecordType::A],
|
||||
RecordTypeArg::AAAA => vec![trust_dns_client::rr::RecordType::AAAA],
|
||||
RecordTypeArg::MX => vec![trust_dns_client::rr::RecordType::MX],
|
||||
RecordTypeArg::NS => vec![trust_dns_client::rr::RecordType::NS],
|
||||
RecordTypeArg::TXT => vec![trust_dns_client::rr::RecordType::TXT],
|
||||
RecordTypeArg::CNAME => vec![trust_dns_client::rr::RecordType::CNAME],
|
||||
RecordTypeArg::SOA => vec![trust_dns_client::rr::RecordType::SOA],
|
||||
RecordTypeArg::PTR => vec![trust_dns_client::rr::RecordType::PTR],
|
||||
RecordTypeArg::All => vec![
|
||||
trust_dns_client::rr::RecordType::A,
|
||||
trust_dns_client::rr::RecordType::AAAA,
|
||||
trust_dns_client::rr::RecordType::MX,
|
||||
trust_dns_client::rr::RecordType::NS,
|
||||
trust_dns_client::rr::RecordType::TXT,
|
||||
trust_dns_client::rr::RecordType::CNAME,
|
||||
trust_dns_client::rr::RecordType::SOA,
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, ValueEnum)]
|
||||
pub enum CategoryArg {
|
||||
Normal,
|
||||
Ads,
|
||||
Spam,
|
||||
Adult,
|
||||
Malicious,
|
||||
Social,
|
||||
Streaming,
|
||||
Gaming,
|
||||
News,
|
||||
All,
|
||||
}
|
||||
|
||||
impl CategoryArg {
|
||||
pub fn to_categories(&self) -> Vec<&'static crate::dns::categories::DomainCategory> {
|
||||
match self {
|
||||
CategoryArg::Normal => vec![&crate::dns::categories::NORMAL_SITES],
|
||||
CategoryArg::Ads => vec![&crate::dns::categories::AD_SITES],
|
||||
CategoryArg::Spam => vec![&crate::dns::categories::SPAM_SITES],
|
||||
CategoryArg::Adult => vec![&crate::dns::categories::ADULT_SITES],
|
||||
CategoryArg::Malicious => vec![&crate::dns::categories::MALICIOUS_SITES],
|
||||
CategoryArg::Social => vec![&crate::dns::categories::SOCIAL_MEDIA],
|
||||
CategoryArg::Streaming => vec![&crate::dns::categories::STREAMING_SITES],
|
||||
CategoryArg::Gaming => vec![&crate::dns::categories::GAMING_SITES],
|
||||
CategoryArg::News => vec![&crate::dns::categories::NEWS_SITES],
|
||||
CategoryArg::All => crate::dns::categories::ALL_CATEGORIES.iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
378
src/dns/categories.rs
Normal file
378
src/dns/categories.rs
Normal file
@ -0,0 +1,378 @@
|
||||
use super::DnsTest;
|
||||
use crate::utils::TestResult;
|
||||
use trust_dns_client::rr::RecordType;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DomainCategory {
|
||||
pub name: &'static str,
|
||||
pub domains: &'static [&'static str],
|
||||
pub description: &'static str,
|
||||
}
|
||||
|
||||
pub const NORMAL_SITES: DomainCategory = DomainCategory {
|
||||
name: "Normal Sites",
|
||||
domains: &[
|
||||
"google.com",
|
||||
"github.com",
|
||||
"stackoverflow.com",
|
||||
"wikipedia.org",
|
||||
"microsoft.com",
|
||||
"apple.com",
|
||||
"amazon.com",
|
||||
"cloudflare.com",
|
||||
"mozilla.org",
|
||||
"rust-lang.org",
|
||||
],
|
||||
description: "Legitimate, commonly used websites",
|
||||
};
|
||||
|
||||
pub const AD_SITES: DomainCategory = DomainCategory {
|
||||
name: "Ad Networks",
|
||||
domains: &[
|
||||
"doubleclick.net",
|
||||
"googlesyndication.com",
|
||||
"googleadservices.com",
|
||||
"facebook.com",
|
||||
"googletagmanager.com",
|
||||
"amazon-adsystem.com",
|
||||
"adsystem.amazon.com",
|
||||
"outbrain.com",
|
||||
"taboola.com",
|
||||
"criteo.com",
|
||||
],
|
||||
description: "Advertising networks and tracking domains",
|
||||
};
|
||||
|
||||
pub const SPAM_SITES: DomainCategory = DomainCategory {
|
||||
name: "Known Spam Domains",
|
||||
domains: &[
|
||||
// Using domains from known spam lists (these should be blocked by many DNS filters)
|
||||
"guerrillamail.com",
|
||||
"10minutemail.com",
|
||||
"tempmail.org",
|
||||
"mailinator.com",
|
||||
"spam4.me",
|
||||
"trashmail.com",
|
||||
"yopmail.com",
|
||||
"tempinbox.com",
|
||||
"throwaway.email",
|
||||
"temp-mail.org",
|
||||
],
|
||||
description: "Temporary email services often associated with spam",
|
||||
};
|
||||
|
||||
pub const ADULT_SITES: DomainCategory = DomainCategory {
|
||||
name: "Adult Content",
|
||||
domains: &[
|
||||
// Using well-known adult sites that are often blocked by family filters
|
||||
// These are legitimate businesses but often filtered
|
||||
"pornhub.com",
|
||||
"xvideos.com",
|
||||
"xnxx.com",
|
||||
"redtube.com",
|
||||
"youporn.com",
|
||||
"tube8.com",
|
||||
"xtube.com",
|
||||
"spankbang.com",
|
||||
"xhamster.com",
|
||||
"beeg.com",
|
||||
],
|
||||
description: "Adult content websites often blocked by family filters",
|
||||
};
|
||||
|
||||
pub const MALICIOUS_SITES: DomainCategory = DomainCategory {
|
||||
name: "Known Malicious Domains",
|
||||
domains: &[
|
||||
// Using domains from threat intelligence feeds (these should be blocked)
|
||||
// Note: These might not resolve or might be sinkholed
|
||||
"malware.testcategory.com",
|
||||
"phishing.testcategory.com",
|
||||
"badware.com",
|
||||
"example-malware.com",
|
||||
"test-phishing.com",
|
||||
"fake-bank-site.com",
|
||||
"malicious-download.com",
|
||||
"virus-test.com",
|
||||
"trojan-test.com",
|
||||
"ransomware-test.com",
|
||||
],
|
||||
description: "Test domains for malicious content detection",
|
||||
};
|
||||
|
||||
pub const SOCIAL_MEDIA: DomainCategory = DomainCategory {
|
||||
name: "Social Media",
|
||||
domains: &[
|
||||
"facebook.com",
|
||||
"twitter.com",
|
||||
"instagram.com",
|
||||
"linkedin.com",
|
||||
"youtube.com",
|
||||
"tiktok.com",
|
||||
"snapchat.com",
|
||||
"pinterest.com",
|
||||
"reddit.com",
|
||||
"discord.com",
|
||||
],
|
||||
description: "Social media platforms",
|
||||
};
|
||||
|
||||
pub const STREAMING_SITES: DomainCategory = DomainCategory {
|
||||
name: "Streaming Services",
|
||||
domains: &[
|
||||
"netflix.com",
|
||||
"hulu.com",
|
||||
"disney.com",
|
||||
"primevideo.com",
|
||||
"spotify.com",
|
||||
"twitch.tv",
|
||||
"youtube.com",
|
||||
"crunchyroll.com",
|
||||
"funimation.com",
|
||||
"hbomax.com",
|
||||
],
|
||||
description: "Video and music streaming services",
|
||||
};
|
||||
|
||||
pub const GAMING_SITES: DomainCategory = DomainCategory {
|
||||
name: "Gaming Platforms",
|
||||
domains: &[
|
||||
"steam.com",
|
||||
"epicgames.com",
|
||||
"battle.net",
|
||||
"origin.com",
|
||||
"uplay.com",
|
||||
"roblox.com",
|
||||
"minecraft.net",
|
||||
"ea.com",
|
||||
"activision.com",
|
||||
"nintendo.com",
|
||||
],
|
||||
description: "Gaming platforms and services",
|
||||
};
|
||||
|
||||
pub const NEWS_SITES: DomainCategory = DomainCategory {
|
||||
name: "News Websites",
|
||||
domains: &[
|
||||
"cnn.com",
|
||||
"bbc.com",
|
||||
"reuters.com",
|
||||
"nytimes.com",
|
||||
"washingtonpost.com",
|
||||
"theguardian.com",
|
||||
"npr.org",
|
||||
"ap.org",
|
||||
"bloomberg.com",
|
||||
"wsj.com",
|
||||
],
|
||||
description: "News and media websites",
|
||||
};
|
||||
|
||||
pub const ALL_CATEGORIES: &[DomainCategory] = &[
|
||||
NORMAL_SITES,
|
||||
AD_SITES,
|
||||
SPAM_SITES,
|
||||
ADULT_SITES,
|
||||
MALICIOUS_SITES,
|
||||
SOCIAL_MEDIA,
|
||||
STREAMING_SITES,
|
||||
GAMING_SITES,
|
||||
NEWS_SITES,
|
||||
];
|
||||
|
||||
pub async fn test_domain_category(
|
||||
category: &DomainCategory,
|
||||
record_type: RecordType,
|
||||
) -> Vec<TestResult> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
for &domain in category.domains {
|
||||
let test = DnsTest::new(domain.to_string(), record_type);
|
||||
|
||||
// Use appropriate testing method based on category type
|
||||
let mut test_result = match category.name {
|
||||
"Known Malicious Domains" => test.run_security_test().await,
|
||||
"Ad Networks" | "Known Spam Domains" | "Adult Content" => {
|
||||
test.run_filtering_test().await
|
||||
}
|
||||
_ => test.run().await,
|
||||
};
|
||||
|
||||
test_result.test_name = format!("{} - {} ({:?})", category.name, domain, record_type);
|
||||
results.push(test_result);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn test_all_categories(record_type: RecordType) -> Vec<TestResult> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
for category in ALL_CATEGORIES {
|
||||
let category_results = test_domain_category(category, record_type).await;
|
||||
results.extend(category_results);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn comprehensive_category_test() -> Vec<TestResult> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
// Test A records for all categories
|
||||
results.extend(test_all_categories(RecordType::A).await);
|
||||
|
||||
// Test AAAA records for normal sites only (to avoid too many tests)
|
||||
results.extend(test_domain_category(&NORMAL_SITES, RecordType::AAAA).await);
|
||||
|
||||
// Test MX records for normal sites
|
||||
results.extend(test_domain_category(&NORMAL_SITES, RecordType::MX).await);
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn test_dns_filtering_effectiveness() -> Vec<TestResult> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
// Test if DNS filtering is working by checking resolution of different categories
|
||||
let filter_test_categories = [
|
||||
(&AD_SITES, "Ad blocking test"),
|
||||
(&SPAM_SITES, "Spam filtering test"),
|
||||
(&ADULT_SITES, "Adult content filtering test"),
|
||||
(&MALICIOUS_SITES, "Malware filtering test"),
|
||||
];
|
||||
|
||||
for (category, test_name) in &filter_test_categories {
|
||||
let category_results = test_domain_category(category, RecordType::A).await;
|
||||
|
||||
// Analyze results based on category type
|
||||
let total_domains = category_results.len();
|
||||
let (blocked_domains, resolved_domains, concerning_domains) = if category.name
|
||||
== "Known Malicious Domains"
|
||||
{
|
||||
// For malicious domains, categorize the results
|
||||
let dns_blocked = category_results
|
||||
.iter()
|
||||
.filter(|result| result.details.contains("🛡️ BLOCKED"))
|
||||
.count();
|
||||
let sinkholed = category_results
|
||||
.iter()
|
||||
.filter(|result| result.details.contains("🕳️ SINKHOLED"))
|
||||
.count();
|
||||
let total_blocked = dns_blocked + sinkholed;
|
||||
let concerning = category_results
|
||||
.iter()
|
||||
.filter(|result| {
|
||||
result.details.contains("⚠️ RESOLVED") || result.details.contains("⚠️ MIXED")
|
||||
})
|
||||
.count();
|
||||
let other_resolved = total_domains - total_blocked - concerning;
|
||||
(total_blocked, other_resolved, concerning)
|
||||
} else if matches!(
|
||||
category.name,
|
||||
"Ad Networks" | "Known Spam Domains" | "Adult Content"
|
||||
) {
|
||||
// For filtering categories, count based on filtering results
|
||||
let dns_filtered = category_results
|
||||
.iter()
|
||||
.filter(|result| result.details.contains("🚫 FILTERED"))
|
||||
.count();
|
||||
let sinkholed = category_results
|
||||
.iter()
|
||||
.filter(|result| result.details.contains("🕳️ SINKHOLED"))
|
||||
.count();
|
||||
let total_filtered = dns_filtered + sinkholed;
|
||||
let accessible = category_results
|
||||
.iter()
|
||||
.filter(|result| result.details.contains("📡 ACCESSIBLE"))
|
||||
.count();
|
||||
(total_filtered, accessible, 0)
|
||||
} else {
|
||||
// For other categories, traditional success/failure counting
|
||||
let blocked = category_results
|
||||
.iter()
|
||||
.filter(|result| !result.success)
|
||||
.count();
|
||||
let resolved = total_domains - blocked;
|
||||
(blocked, resolved, 0)
|
||||
};
|
||||
|
||||
let summary_result = if category.name == "Known Malicious Domains" {
|
||||
let security_status = if concerning_domains > 0 {
|
||||
format!(
|
||||
"⚠️ SECURITY CONCERN: {} potentially malicious domains resolved successfully",
|
||||
concerning_domains
|
||||
)
|
||||
} else if blocked_domains > resolved_domains {
|
||||
format!(
|
||||
"🛡️ GOOD SECURITY: {:.1}% of malicious domains blocked",
|
||||
(blocked_domains as f64 / total_domains as f64) * 100.0
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"⚠️ WEAK FILTERING: Only {:.1}% of malicious domains blocked",
|
||||
(blocked_domains as f64 / total_domains as f64) * 100.0
|
||||
)
|
||||
};
|
||||
|
||||
TestResult::new(format!(
|
||||
"Security Analysis: {} blocked, {} resolved, {} concerning",
|
||||
blocked_domains, resolved_domains, concerning_domains
|
||||
))
|
||||
.success(std::time::Duration::from_millis(0), security_status)
|
||||
} else {
|
||||
TestResult::new(format!(
|
||||
"{}: {} resolved, {} blocked",
|
||||
test_name, resolved_domains, blocked_domains
|
||||
))
|
||||
.success(
|
||||
std::time::Duration::from_millis(0),
|
||||
format!(
|
||||
"Blocking rate: {:.1}%",
|
||||
(blocked_domains as f64 / total_domains as f64) * 100.0
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
results.push(summary_result);
|
||||
results.extend(category_results);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Provide explanation of what DNS filtering results mean
|
||||
pub fn explain_dns_filtering_results() -> String {
|
||||
r#"
|
||||
DNS FILTERING ANALYSIS EXPLANATION:
|
||||
|
||||
🛡️ BLOCKED results for malicious domains = SECURITY SUCCESS
|
||||
- Domain was blocked at DNS level (good!)
|
||||
- Possible blocking levels: Router, ISP, DNS service (Cloudflare, Quad9), OS
|
||||
|
||||
🕳️ SINKHOLED results = ADVANCED FILTERING SUCCESS
|
||||
- Domain redirected to harmless "sinkhole" IP addresses
|
||||
- Common sinkholes: 0.0.0.0, 127.0.0.1, router IPs
|
||||
- More sophisticated than simple blocking
|
||||
|
||||
⚠️ RESOLVED results for malicious domains = POTENTIAL SECURITY CONCERN
|
||||
- Domain resolved to real IP addresses
|
||||
- Could indicate: No filtering, outdated blocklists, or domain not yet flagged
|
||||
|
||||
⚡ PARTIAL SINKHOLE = MIXED FILTERING
|
||||
- Some IPs sinkholed, others real (inconsistent filtering)
|
||||
|
||||
For other categories (Adult, Ads, etc.):
|
||||
📡 ACCESSIBLE = Normal (expected behavior without filtering)
|
||||
🚫 FILTERED = Content filtering active (family filters, ad blockers, etc.)
|
||||
🕳️ SINKHOLED = Advanced filtering (DNS redirect to safe IPs)
|
||||
|
||||
Common sinkhole IP addresses:
|
||||
• 0.0.0.0 - Universal "null route"
|
||||
• 127.0.0.1 - Localhost redirect
|
||||
• 192.168.1.1 - Router redirect
|
||||
• 146.112.61.104/105 - OpenDNS sinkholes
|
||||
• 198.105.232.6/7 - Quad9 sinkholes
|
||||
• 185.228.168.10 - CleanBrowsing sinkholes
|
||||
"#
|
||||
.to_string()
|
||||
}
|
||||
689
src/dns/mod.rs
Normal file
689
src/dns/mod.rs
Normal file
@ -0,0 +1,689 @@
|
||||
use crate::utils::{measure_time, NetworkError, Result, TestResult};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
use trust_dns_client::rr::{Name, RData, RecordData, RecordType};
|
||||
use trust_dns_resolver::config::*;
|
||||
use trust_dns_resolver::system_conf;
|
||||
use trust_dns_resolver::TokioAsyncResolver;
|
||||
|
||||
pub mod categories;
|
||||
pub mod queries;
|
||||
|
||||
pub use categories::*;
|
||||
pub use queries::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ConnectivityStatus {
|
||||
Reachable,
|
||||
DnsOnlyNetworkBlocked,
|
||||
PartiallyReachable,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DnsTest {
|
||||
pub domain: String,
|
||||
pub record_type: RecordType,
|
||||
pub server: Option<SocketAddr>,
|
||||
pub timeout: Duration,
|
||||
pub use_tcp: bool,
|
||||
}
|
||||
|
||||
impl DnsTest {
|
||||
pub fn new(domain: String, record_type: RecordType) -> Self {
|
||||
Self {
|
||||
domain,
|
||||
record_type,
|
||||
server: None,
|
||||
timeout: Duration::from_secs(5),
|
||||
use_tcp: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_server(mut self, server: SocketAddr) -> Self {
|
||||
self.server = Some(server);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_tcp(mut self, use_tcp: bool) -> Self {
|
||||
self.use_tcp = use_tcp;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> TestResult {
|
||||
let protocol = if self.use_tcp { "TCP" } else { "UDP" };
|
||||
let server_info = self
|
||||
.server
|
||||
.map(|s| format!(" via {}", s))
|
||||
.unwrap_or_default();
|
||||
|
||||
let test_name = format!(
|
||||
"DNS {:?} query for {} ({}{})",
|
||||
self.record_type, self.domain, protocol, server_info
|
||||
);
|
||||
|
||||
let (duration, result) = measure_time(|| async {
|
||||
if let Some(server) = self.server {
|
||||
self.query_specific_server(server).await
|
||||
} else {
|
||||
self.query_system_resolver().await
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(details) => TestResult::new(test_name).success(duration, details),
|
||||
Err(error) => TestResult::new(test_name).failure(duration, error),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_security_test(&self) -> TestResult {
|
||||
let protocol = if self.use_tcp { "TCP" } else { "UDP" };
|
||||
let server_info = self
|
||||
.server
|
||||
.map(|s| format!(" via {}", s))
|
||||
.unwrap_or_default();
|
||||
|
||||
let test_name = format!(
|
||||
"DNS {:?} query for {} ({}{})",
|
||||
self.record_type, self.domain, protocol, server_info
|
||||
);
|
||||
|
||||
let (duration, result) = measure_time(|| async {
|
||||
if let Some(server) = self.server {
|
||||
self.query_specific_server(server).await
|
||||
} else {
|
||||
self.query_system_resolver().await
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(details) => {
|
||||
// Check if the resolved IPs are sinkholed
|
||||
let ips = self.extract_ips_from_dns_details(&details);
|
||||
let sinkhole_analysis = analyze_sinkhole_ips(&ips);
|
||||
|
||||
match sinkhole_analysis {
|
||||
SinkholeAnalysis::FullySinkholed(sinkhole_ips) => {
|
||||
let sinkhole_list: Vec<String> =
|
||||
sinkhole_ips.iter().map(|ip| ip.to_string()).collect();
|
||||
TestResult::new(test_name).success(
|
||||
duration,
|
||||
format!(
|
||||
"🕳️ SINKHOLED (security success): Redirected to sinkhole IPs: {}",
|
||||
sinkhole_list.join(", ")
|
||||
),
|
||||
)
|
||||
}
|
||||
SinkholeAnalysis::PartiallySinkholed {
|
||||
sinkhole_ips,
|
||||
legitimate_ips,
|
||||
} => {
|
||||
let sinkhole_list: Vec<String> =
|
||||
sinkhole_ips.iter().map(|ip| ip.to_string()).collect();
|
||||
let legit_list: Vec<String> =
|
||||
legitimate_ips.iter().map(|ip| ip.to_string()).collect();
|
||||
TestResult::new(test_name).success(
|
||||
duration,
|
||||
format!("⚠️ MIXED RESOLUTION: Sinkholed: {} | Real IPs: {} (partial security concern)",
|
||||
sinkhole_list.join(", "), legit_list.join(", "))
|
||||
)
|
||||
}
|
||||
SinkholeAnalysis::NotSinkholed(_) => TestResult::new(test_name).success(
|
||||
duration,
|
||||
format!("⚠️ RESOLVED (potential security concern): {}", details),
|
||||
),
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
// For security tests, DNS resolution failures are considered successes
|
||||
match error {
|
||||
NetworkError::DnsResolution(err_msg) => {
|
||||
let blocking_explanation = analyze_dns_blocking(&err_msg);
|
||||
TestResult::new(test_name).success(
|
||||
duration,
|
||||
format!("🛡️ BLOCKED (security success): {}", blocking_explanation),
|
||||
)
|
||||
}
|
||||
_ => TestResult::new(test_name).failure(duration, error),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_filtering_test(&self) -> TestResult {
|
||||
let protocol = if self.use_tcp { "TCP" } else { "UDP" };
|
||||
let server_info = self
|
||||
.server
|
||||
.map(|s| format!(" via {}", s))
|
||||
.unwrap_or_default();
|
||||
|
||||
let test_name = format!(
|
||||
"DNS {:?} query for {} ({}{})",
|
||||
self.record_type, self.domain, protocol, server_info
|
||||
);
|
||||
|
||||
let (duration, result) = measure_time(|| async {
|
||||
if let Some(server) = self.server {
|
||||
self.query_specific_server(server).await
|
||||
} else {
|
||||
self.query_system_resolver().await
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(details) => {
|
||||
// Check if the resolved IPs are sinkholed
|
||||
let ips = self.extract_ips_from_dns_details(&details);
|
||||
let sinkhole_analysis = analyze_sinkhole_ips(&ips);
|
||||
|
||||
match sinkhole_analysis {
|
||||
SinkholeAnalysis::FullySinkholed(sinkhole_ips) => {
|
||||
let sinkhole_list: Vec<String> =
|
||||
sinkhole_ips.iter().map(|ip| ip.to_string()).collect();
|
||||
TestResult::new(test_name).success(
|
||||
duration,
|
||||
format!("🕳️ SINKHOLED: Redirected to sinkhole IPs: {} (filtered via DNS redirect)", sinkhole_list.join(", "))
|
||||
)
|
||||
}
|
||||
SinkholeAnalysis::PartiallySinkholed {
|
||||
sinkhole_ips,
|
||||
legitimate_ips,
|
||||
} => {
|
||||
let sinkhole_list: Vec<String> =
|
||||
sinkhole_ips.iter().map(|ip| ip.to_string()).collect();
|
||||
let legit_list: Vec<String> =
|
||||
legitimate_ips.iter().map(|ip| ip.to_string()).collect();
|
||||
TestResult::new(test_name).success(
|
||||
duration,
|
||||
format!("⚡ PARTIAL SINKHOLE: Sinkholed: {} | Real IPs: {} (partial filtering)",
|
||||
sinkhole_list.join(", "), legit_list.join(", "))
|
||||
)
|
||||
}
|
||||
SinkholeAnalysis::NotSinkholed(_) => TestResult::new(test_name)
|
||||
.success(duration, format!("📡 ACCESSIBLE: {}", details)),
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
// DNS resolution failed - for filtering tests, this is good (blocked)
|
||||
match error {
|
||||
NetworkError::DnsResolution(err_msg) => {
|
||||
let blocking_explanation = analyze_dns_blocking(&err_msg);
|
||||
TestResult::new(test_name)
|
||||
.success(duration, format!("🚫 FILTERED: {}", blocking_explanation))
|
||||
}
|
||||
_ => TestResult::new(test_name).failure(duration, error),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_comprehensive_test(&self) -> TestResult {
|
||||
let protocol = if self.use_tcp { "TCP" } else { "UDP" };
|
||||
let server_info = self
|
||||
.server
|
||||
.map(|s| format!(" via {}", s))
|
||||
.unwrap_or_default();
|
||||
|
||||
let test_name = format!(
|
||||
"DNS {:?} query for {} ({}{})",
|
||||
self.record_type, self.domain, protocol, server_info
|
||||
);
|
||||
|
||||
let (dns_duration, dns_result) = measure_time(|| async {
|
||||
if let Some(server) = self.server {
|
||||
self.query_specific_server(server).await
|
||||
} else {
|
||||
self.query_system_resolver().await
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
match dns_result {
|
||||
Ok(dns_details) => {
|
||||
// DNS resolved, now check actual connectivity
|
||||
let connectivity_result =
|
||||
self.check_connectivity_to_resolved_ips(&dns_details).await;
|
||||
let total_duration = dns_duration + Duration::from_millis(50); // Approximate connectivity check time
|
||||
|
||||
match connectivity_result {
|
||||
ConnectivityStatus::Reachable => {
|
||||
TestResult::new(test_name).success(
|
||||
total_duration,
|
||||
format!("✅ FULLY ACCESSIBLE: {} | Connectivity: Reachable", dns_details)
|
||||
)
|
||||
}
|
||||
ConnectivityStatus::DnsOnlyNetworkBlocked => {
|
||||
TestResult::new(test_name).success(
|
||||
total_duration,
|
||||
format!("🌐 DNS RESOLVES, NETWORK BLOCKED: {} | Traffic blocked at ISP/router level", dns_details)
|
||||
)
|
||||
}
|
||||
ConnectivityStatus::PartiallyReachable => {
|
||||
TestResult::new(test_name).success(
|
||||
total_duration,
|
||||
format!("⚡ PARTIALLY REACHABLE: {} | Some ports blocked", dns_details)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => match error {
|
||||
NetworkError::DnsResolution(err_msg) => {
|
||||
let blocking_explanation = analyze_dns_blocking(&err_msg);
|
||||
TestResult::new(test_name).success(
|
||||
dns_duration,
|
||||
format!("🛡️ DNS BLOCKED: {}", blocking_explanation),
|
||||
)
|
||||
}
|
||||
_ => TestResult::new(test_name).failure(dns_duration, error),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn query_system_resolver(&self) -> Result<String> {
|
||||
// Try to use system DNS configuration first, fall back to default if that fails
|
||||
let (config, opts) = match system_conf::read_system_conf() {
|
||||
Ok((config, opts)) => (config, opts),
|
||||
Err(_) => {
|
||||
// Fallback to default config if system config cannot be read
|
||||
eprintln!("Warning: Could not read system DNS config, using default");
|
||||
(ResolverConfig::default(), ResolverOpts::default())
|
||||
}
|
||||
};
|
||||
|
||||
let resolver = TokioAsyncResolver::tokio(config, opts);
|
||||
|
||||
let name = Name::from_str(&self.domain)
|
||||
.map_err(|e| NetworkError::DnsResolution(format!("Invalid domain: {}", e)))?;
|
||||
|
||||
let response = timeout(self.timeout, async {
|
||||
match self.record_type {
|
||||
RecordType::A => {
|
||||
let lookup = resolver
|
||||
.ipv4_lookup(name.clone())
|
||||
.await
|
||||
.map_err(|e| format!("A lookup failed: {}", e))?;
|
||||
let ips: Vec<String> = lookup.iter().map(|ip| ip.to_string()).collect();
|
||||
Ok(format!("A records: {}", ips.join(", ")))
|
||||
}
|
||||
RecordType::AAAA => {
|
||||
let lookup = resolver
|
||||
.ipv6_lookup(name.clone())
|
||||
.await
|
||||
.map_err(|e| format!("AAAA lookup failed: {}", e))?;
|
||||
let ips: Vec<String> = lookup.iter().map(|ip| ip.to_string()).collect();
|
||||
Ok(format!("AAAA records: {}", ips.join(", ")))
|
||||
}
|
||||
RecordType::MX => {
|
||||
let lookup = resolver
|
||||
.mx_lookup(name.clone())
|
||||
.await
|
||||
.map_err(|e| format!("MX lookup failed: {}", e))?;
|
||||
let records: Vec<String> = lookup
|
||||
.iter()
|
||||
.map(|mx| format!("{} {}", mx.preference(), mx.exchange()))
|
||||
.collect();
|
||||
Ok(format!("MX records: {}", records.join(", ")))
|
||||
}
|
||||
RecordType::TXT => {
|
||||
let lookup = resolver
|
||||
.txt_lookup(name.clone())
|
||||
.await
|
||||
.map_err(|e| format!("TXT lookup failed: {}", e))?;
|
||||
let records: Vec<String> = lookup.iter().map(|txt| txt.to_string()).collect();
|
||||
Ok(format!("TXT records: {}", records.join(", ")))
|
||||
}
|
||||
RecordType::NS => {
|
||||
let lookup = resolver
|
||||
.ns_lookup(name.clone())
|
||||
.await
|
||||
.map_err(|e| format!("NS lookup failed: {}", e))?;
|
||||
let records: Vec<String> = lookup.iter().map(|ns| ns.to_string()).collect();
|
||||
Ok(format!("NS records: {}", records.join(", ")))
|
||||
}
|
||||
RecordType::CNAME => {
|
||||
let lookup = resolver
|
||||
.lookup(name.clone(), self.record_type)
|
||||
.await
|
||||
.map_err(|e| format!("CNAME lookup failed: {}", e))?;
|
||||
let records: Vec<String> = lookup
|
||||
.iter()
|
||||
.filter_map(|record| {
|
||||
if let RData::CNAME(cname) = record.clone().into_rdata() {
|
||||
Some(cname.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(format!("CNAME records: {}", records.join(", ")))
|
||||
}
|
||||
RecordType::SOA => {
|
||||
let lookup = resolver
|
||||
.soa_lookup(name.clone())
|
||||
.await
|
||||
.map_err(|e| format!("SOA lookup failed: {}", e))?;
|
||||
let records: Vec<String> = lookup
|
||||
.iter()
|
||||
.map(|soa| {
|
||||
format!(
|
||||
"{} {} {} {} {} {} {}",
|
||||
soa.mname(),
|
||||
soa.rname(),
|
||||
soa.serial(),
|
||||
soa.refresh(),
|
||||
soa.retry(),
|
||||
soa.expire(),
|
||||
soa.minimum()
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Ok(format!("SOA records: {}", records.join(", ")))
|
||||
}
|
||||
RecordType::PTR => {
|
||||
let lookup = resolver
|
||||
.lookup(name.clone(), self.record_type)
|
||||
.await
|
||||
.map_err(|e| format!("PTR lookup failed: {}", e))?;
|
||||
let records: Vec<String> = lookup
|
||||
.iter()
|
||||
.filter_map(|record| {
|
||||
if let RData::PTR(ptr) = record.clone().into_rdata() {
|
||||
Some(ptr.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(format!("PTR records: {}", records.join(", ")))
|
||||
}
|
||||
_ => {
|
||||
let lookup = resolver
|
||||
.lookup(name.clone(), self.record_type)
|
||||
.await
|
||||
.map_err(|e| format!("Lookup failed: {}", e))?;
|
||||
let count = lookup.iter().count();
|
||||
Ok(format!("{:?} records: {} found", self.record_type, count))
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| NetworkError::Timeout)?
|
||||
.map_err(|_: String| NetworkError::DnsResolution("System resolver failed".to_string()))?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn query_specific_server(&self, server: SocketAddr) -> Result<String> {
|
||||
// Create a resolver configuration that uses the specific server
|
||||
let mut config = ResolverConfig::new();
|
||||
config.add_name_server(trust_dns_resolver::config::NameServerConfig::new(
|
||||
server,
|
||||
trust_dns_resolver::config::Protocol::Udp,
|
||||
));
|
||||
let opts = ResolverOpts::default();
|
||||
|
||||
let resolver = TokioAsyncResolver::tokio(config, opts);
|
||||
|
||||
let name = Name::from_str(&self.domain)
|
||||
.map_err(|e| NetworkError::DnsResolution(format!("Invalid domain: {}", e)))?;
|
||||
|
||||
let response = timeout(self.timeout, async {
|
||||
match self.record_type {
|
||||
RecordType::A => {
|
||||
let lookup = resolver
|
||||
.ipv4_lookup(name.clone())
|
||||
.await
|
||||
.map_err(|e| format!("A lookup failed: {}", e))?;
|
||||
let ips: Vec<String> = lookup.iter().map(|ip| ip.to_string()).collect();
|
||||
Ok(format!("A records: {}", ips.join(", ")))
|
||||
}
|
||||
RecordType::AAAA => {
|
||||
let lookup = resolver
|
||||
.ipv6_lookup(name.clone())
|
||||
.await
|
||||
.map_err(|e| format!("AAAA lookup failed: {}", e))?;
|
||||
let ips: Vec<String> = lookup.iter().map(|ip| ip.to_string()).collect();
|
||||
Ok(format!("AAAA records: {}", ips.join(", ")))
|
||||
}
|
||||
_ => {
|
||||
let lookup = resolver
|
||||
.lookup(name.clone(), self.record_type)
|
||||
.await
|
||||
.map_err(|e| format!("Lookup failed: {}", e))?;
|
||||
let count = lookup.iter().count();
|
||||
Ok(format!("{:?} records: {} found", self.record_type, count))
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| NetworkError::Timeout)?
|
||||
.map_err(|e| NetworkError::DnsResolution(e))?;
|
||||
|
||||
Ok(format!("{} (via {})", response, server))
|
||||
}
|
||||
|
||||
async fn check_connectivity_to_resolved_ips(&self, dns_details: &str) -> ConnectivityStatus {
|
||||
// Extract IP addresses from DNS details string
|
||||
let ips = self.extract_ips_from_dns_details(dns_details);
|
||||
|
||||
if ips.is_empty() {
|
||||
return ConnectivityStatus::DnsOnlyNetworkBlocked;
|
||||
}
|
||||
|
||||
let mut reachable_count = 0;
|
||||
let test_ports = [80, 443, 8080]; // Common HTTP/HTTPS ports
|
||||
|
||||
for ip in ips.iter().take(2) {
|
||||
// Test first 2 IPs to avoid too many connections
|
||||
for &port in &test_ports {
|
||||
let addr = SocketAddr::new(*ip, port);
|
||||
|
||||
if let Ok(_) = timeout(Duration::from_millis(1000), TcpStream::connect(addr)).await
|
||||
{
|
||||
reachable_count += 1;
|
||||
break; // If any port works, IP is reachable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if reachable_count > 0 {
|
||||
if reachable_count == ips.len().min(2) {
|
||||
ConnectivityStatus::Reachable
|
||||
} else {
|
||||
ConnectivityStatus::PartiallyReachable
|
||||
}
|
||||
} else {
|
||||
ConnectivityStatus::DnsOnlyNetworkBlocked
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_ips_from_dns_details(&self, dns_details: &str) -> Vec<IpAddr> {
|
||||
let mut ips = Vec::new();
|
||||
|
||||
// Look for patterns like "A records: 1.2.3.4, 5.6.7.8"
|
||||
if let Some(records_part) = dns_details.split("records: ").nth(1) {
|
||||
for ip_str in records_part.split(", ") {
|
||||
if let Ok(ip) = ip_str.trim().parse::<IpAddr>() {
|
||||
ips.push(ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ips
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn test_common_dns_servers(domain: &str, record_type: RecordType) -> Vec<TestResult> {
|
||||
let servers = [
|
||||
"8.8.8.8:53", // Google
|
||||
"8.8.4.4:53", // Google
|
||||
"1.1.1.1:53", // Cloudflare
|
||||
"1.0.0.1:53", // Cloudflare
|
||||
"9.9.9.9:53", // Quad9
|
||||
"208.67.222.222:53", // OpenDNS
|
||||
"208.67.220.220:53", // OpenDNS
|
||||
];
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
for server_str in &servers {
|
||||
if let Ok(server) = server_str.parse::<SocketAddr>() {
|
||||
let test = DnsTest::new(domain.to_string(), record_type).with_server(server);
|
||||
results.push(test.run().await);
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn test_dns_over_tcp_udp(
|
||||
domain: &str,
|
||||
record_type: RecordType,
|
||||
server: SocketAddr,
|
||||
) -> Vec<TestResult> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
// UDP test
|
||||
let udp_test = DnsTest::new(domain.to_string(), record_type)
|
||||
.with_server(server)
|
||||
.with_tcp(false);
|
||||
results.push(udp_test.run().await);
|
||||
|
||||
// TCP test
|
||||
let tcp_test = DnsTest::new(domain.to_string(), record_type)
|
||||
.with_server(server)
|
||||
.with_tcp(true);
|
||||
results.push(tcp_test.run().await);
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Analyze DNS blocking to provide detailed explanation of what's happening
|
||||
fn analyze_dns_blocking(error_msg: &str) -> String {
|
||||
let error_lower = error_msg.to_lowercase();
|
||||
|
||||
if error_lower.contains("nxdomain") || error_lower.contains("name not found") {
|
||||
"DNS server returned NXDOMAIN (domain doesn't exist or is blocked at DNS level)".to_string()
|
||||
} else if error_lower.contains("servfail") || error_lower.contains("server failure") {
|
||||
"DNS server returned SERVFAIL (possibly blocked by DNS filtering service)".to_string()
|
||||
} else if error_lower.contains("refused") {
|
||||
"DNS query refused (likely blocked by DNS server policy)".to_string()
|
||||
} else if error_lower.contains("timeout") {
|
||||
"DNS query timeout (domain may be sinkholed or filtered)".to_string()
|
||||
} else if error_lower.contains("connection refused") {
|
||||
"Connection refused to DNS server (network-level blocking)".to_string()
|
||||
} else if error_lower.contains("system resolver failed") {
|
||||
"System DNS resolver blocked the query (OS or network-level filtering)".to_string()
|
||||
} else {
|
||||
format!("DNS resolution failed ({})", error_msg)
|
||||
}
|
||||
}
|
||||
|
||||
/// Analyze IP addresses to detect DNS sinkholing
|
||||
fn analyze_sinkhole_ips(ips: &[IpAddr]) -> SinkholeAnalysis {
|
||||
let mut sinkhole_ips = Vec::new();
|
||||
let mut legitimate_ips = Vec::new();
|
||||
|
||||
for &ip in ips {
|
||||
if is_sinkhole_ip(ip) {
|
||||
sinkhole_ips.push(ip);
|
||||
} else {
|
||||
legitimate_ips.push(ip);
|
||||
}
|
||||
}
|
||||
|
||||
if !sinkhole_ips.is_empty() && legitimate_ips.is_empty() {
|
||||
SinkholeAnalysis::FullySinkholed(sinkhole_ips)
|
||||
} else if !sinkhole_ips.is_empty() {
|
||||
SinkholeAnalysis::PartiallySinkholed {
|
||||
sinkhole_ips,
|
||||
legitimate_ips,
|
||||
}
|
||||
} else {
|
||||
SinkholeAnalysis::NotSinkholed(legitimate_ips)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an IP address is a known sinkhole address
|
||||
fn is_sinkhole_ip(ip: IpAddr) -> bool {
|
||||
match ip {
|
||||
IpAddr::V4(ipv4) => {
|
||||
let octets = ipv4.octets();
|
||||
|
||||
// Common sinkhole addresses
|
||||
match octets {
|
||||
[0, 0, 0, 0] => true, // 0.0.0.0 - most common
|
||||
[127, 0, 0, 1] => true, // 127.0.0.1 - localhost redirect
|
||||
[127, 0, 0, 2..=255] => true, // 127.x.x.x range
|
||||
[10, 0, 0, 1] => true, // 10.0.0.1 - router sinkhole
|
||||
[192, 168, 1, 1] => true, // 192.168.1.1 - router sinkhole
|
||||
[192, 168, 0, 1] => true, // 192.168.0.1 - router sinkhole
|
||||
[146, 112, 61, 104] => true, // OpenDNS sinkhole
|
||||
[146, 112, 61, 105] => true, // OpenDNS sinkhole
|
||||
[199, 85, 126, 10] => true, // Norton DNS sinkhole
|
||||
[199, 85, 127, 10] => true, // Norton DNS sinkhole
|
||||
[208, 69, 38, 170] => true, // OpenDNS phishing block page
|
||||
[208, 69, 39, 170] => true, // OpenDNS phishing block page
|
||||
[198, 105, 232, 6] => true, // Quad9 sinkhole
|
||||
[198, 105, 232, 7] => true, // Quad9 sinkhole
|
||||
[185, 228, 168, 10] => true, // CleanBrowsing sinkhole
|
||||
[185, 228, 169, 11] => true, // CleanBrowsing sinkhole
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
IpAddr::V6(ipv6) => {
|
||||
// IPv6 sinkhole addresses
|
||||
let segments = ipv6.segments();
|
||||
match segments {
|
||||
[0, 0, 0, 0, 0, 0, 0, 0] => true, // :: (IPv6 equivalent of 0.0.0.0)
|
||||
[0, 0, 0, 0, 0, 0, 0, 1] => true, // ::1 (IPv6 localhost)
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SinkholeAnalysis {
|
||||
NotSinkholed(Vec<IpAddr>),
|
||||
FullySinkholed(Vec<IpAddr>),
|
||||
PartiallySinkholed {
|
||||
sinkhole_ips: Vec<IpAddr>,
|
||||
legitimate_ips: Vec<IpAddr>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Debug function to show current DNS configuration
|
||||
pub fn debug_dns_config() -> String {
|
||||
match system_conf::read_system_conf() {
|
||||
Ok((config, _opts)) => {
|
||||
let mut debug_info = vec!["System DNS Configuration:".to_string()];
|
||||
|
||||
for name_server in config.name_servers() {
|
||||
debug_info.push(format!(
|
||||
" 📡 DNS Server: {} ({})",
|
||||
name_server.socket_addr,
|
||||
match name_server.protocol {
|
||||
Protocol::Udp => "UDP",
|
||||
Protocol::Tcp => "TCP",
|
||||
_ => "Other",
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
debug_info.push(format!(" 🔍 Search domains: {:?}", config.search()));
|
||||
debug_info.join("\n")
|
||||
}
|
||||
Err(e) => format!("❌ Could not read system DNS config: {}", e),
|
||||
}
|
||||
}
|
||||
147
src/dns/queries.rs
Normal file
147
src/dns/queries.rs
Normal file
@ -0,0 +1,147 @@
|
||||
use super::DnsTest;
|
||||
use crate::utils::TestResult;
|
||||
use trust_dns_client::rr::RecordType;
|
||||
|
||||
pub async fn comprehensive_dns_test(domain: &str) -> Vec<TestResult> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
let record_types = [
|
||||
RecordType::A,
|
||||
RecordType::AAAA,
|
||||
RecordType::MX,
|
||||
RecordType::NS,
|
||||
RecordType::TXT,
|
||||
RecordType::CNAME,
|
||||
RecordType::SOA,
|
||||
];
|
||||
|
||||
for record_type in &record_types {
|
||||
let test = DnsTest::new(domain.to_string(), *record_type);
|
||||
results.push(test.run().await);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn test_large_dns_queries(domain: &str) -> Vec<TestResult> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
// Test with a domain that has large TXT records
|
||||
let large_txt_domains = [
|
||||
"_dmarc.google.com",
|
||||
"google.com", // Often has large TXT records for verification
|
||||
"_domainkey.google.com",
|
||||
];
|
||||
|
||||
for test_domain in &large_txt_domains {
|
||||
let test = DnsTest::new(test_domain.to_string(), RecordType::TXT);
|
||||
results.push(test.run().await);
|
||||
}
|
||||
|
||||
// Test DNSSEC-related records if available
|
||||
let dnssec_types = [
|
||||
RecordType::DS,
|
||||
RecordType::RRSIG,
|
||||
RecordType::DNSKEY,
|
||||
RecordType::NSEC,
|
||||
RecordType::NSEC3,
|
||||
];
|
||||
|
||||
for record_type in &dnssec_types {
|
||||
let test = DnsTest::new(domain.to_string(), *record_type);
|
||||
results.push(test.run().await);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn test_dns_amplification_domains() -> Vec<TestResult> {
|
||||
// Test domains that might be used in DNS amplification attacks
|
||||
// These are legitimate tests to check if the resolver handles them properly
|
||||
let test_domains = [
|
||||
"isc.org", // Often has large responses
|
||||
"ripe.net", // Registry with comprehensive records
|
||||
"version.bind", // Special query
|
||||
"hostname.bind", // Special query
|
||||
];
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
for domain in &test_domains {
|
||||
let test = DnsTest::new(domain.to_string(), RecordType::TXT);
|
||||
results.push(test.run().await);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn test_reverse_dns_lookups() -> Vec<TestResult> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
let test_ips = [
|
||||
"8.8.8.8", // Google DNS
|
||||
"1.1.1.1", // Cloudflare DNS
|
||||
"208.67.222.222", // OpenDNS
|
||||
];
|
||||
|
||||
for ip in &test_ips {
|
||||
// Convert IP to reverse DNS format
|
||||
let reverse_domain = if let Ok(addr) = ip.parse::<std::net::Ipv4Addr>() {
|
||||
let octets = addr.octets();
|
||||
format!(
|
||||
"{}.{}.{}.{}.in-addr.arpa",
|
||||
octets[3], octets[2], octets[1], octets[0]
|
||||
)
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let test = DnsTest::new(reverse_domain, RecordType::PTR);
|
||||
results.push(test.run().await);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn test_international_domains() -> Vec<TestResult> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
// Test internationalized domain names
|
||||
let international_domains = [
|
||||
"xn--n3h.com", // ☃.com (snowman emoji)
|
||||
"xn--e1afmkfd.xn--p1ai", // пример.рф (example.rf in Russian)
|
||||
"xn--fsq.xn--0zwm56d", // 测试.测试 (test.test in Chinese)
|
||||
];
|
||||
|
||||
for domain in &international_domains {
|
||||
let test = DnsTest::new(domain.to_string(), RecordType::A);
|
||||
results.push(test.run().await);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn test_dns_query_sizes() -> Vec<TestResult> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
// Test queries that might produce different response sizes
|
||||
let size_test_domains = [
|
||||
("short.example", "Short domain name"),
|
||||
(
|
||||
"very-long-subdomain-name-that-tests-dns-limits.example.com",
|
||||
"Long domain name",
|
||||
),
|
||||
(
|
||||
"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.example.com",
|
||||
"Deep subdomain",
|
||||
),
|
||||
];
|
||||
|
||||
for (domain, description) in &size_test_domains {
|
||||
let mut test_result = DnsTest::new(domain.to_string(), RecordType::A).run().await;
|
||||
test_result.test_name = format!("DNS query size test: {}", description);
|
||||
results.push(test_result);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
11
src/lib.rs
Normal file
11
src/lib.rs
Normal file
@ -0,0 +1,11 @@
|
||||
pub mod cli;
|
||||
pub mod dns;
|
||||
pub mod mtu;
|
||||
pub mod network;
|
||||
pub mod utils;
|
||||
|
||||
pub use cli::*;
|
||||
pub use dns::*;
|
||||
pub use mtu::*;
|
||||
pub use network::*;
|
||||
pub use utils::*;
|
||||
370
src/main.rs
Normal file
370
src/main.rs
Normal file
@ -0,0 +1,370 @@
|
||||
use clap::Parser;
|
||||
use colored::*;
|
||||
use env_logger;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use nettest::*;
|
||||
use serde_json;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let cli = cli::Cli::parse();
|
||||
|
||||
env_logger::Builder::from_default_env()
|
||||
.filter_level(if cli.verbose {
|
||||
log::LevelFilter::Info
|
||||
} else {
|
||||
log::LevelFilter::Warn
|
||||
})
|
||||
.init();
|
||||
|
||||
let timeout = Duration::from_secs(cli.timeout);
|
||||
|
||||
let results = match cli.command {
|
||||
cli::Commands::Network { command } => handle_network_command(command, timeout).await,
|
||||
cli::Commands::Dns { command } => handle_dns_command(command, timeout).await,
|
||||
cli::Commands::Mtu { command } => handle_mtu_command(command, timeout).await,
|
||||
cli::Commands::Full { target, ip_version } => {
|
||||
handle_full_test(target, ip_version, timeout).await
|
||||
}
|
||||
};
|
||||
|
||||
if cli.json {
|
||||
print_results_json(&results);
|
||||
} else {
|
||||
print_results_human(&results);
|
||||
}
|
||||
|
||||
let failed_tests = results.iter().filter(|r| !r.success).count();
|
||||
if failed_tests > 0 {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_network_command(
|
||||
command: cli::NetworkCommands,
|
||||
timeout: Duration,
|
||||
) -> Vec<TestResult> {
|
||||
match command {
|
||||
cli::NetworkCommands::Tcp {
|
||||
target,
|
||||
port,
|
||||
ip_version,
|
||||
} => {
|
||||
let mut results = Vec::new();
|
||||
for version in ip_version.to_versions() {
|
||||
let test = network::NetworkTest::new(
|
||||
target.clone(),
|
||||
version,
|
||||
network::NetworkProtocol::Tcp,
|
||||
)
|
||||
.with_port(port)
|
||||
.with_timeout(timeout);
|
||||
results.push(test.run().await);
|
||||
}
|
||||
results
|
||||
}
|
||||
cli::NetworkCommands::Udp {
|
||||
target,
|
||||
port,
|
||||
ip_version,
|
||||
} => {
|
||||
let mut results = Vec::new();
|
||||
for version in ip_version.to_versions() {
|
||||
let test = network::NetworkTest::new(
|
||||
target.clone(),
|
||||
version,
|
||||
network::NetworkProtocol::Udp,
|
||||
)
|
||||
.with_port(port)
|
||||
.with_timeout(timeout);
|
||||
results.push(test.run().await);
|
||||
}
|
||||
results
|
||||
}
|
||||
cli::NetworkCommands::Ping {
|
||||
target,
|
||||
count,
|
||||
ip_version,
|
||||
} => {
|
||||
let mut results = Vec::new();
|
||||
for version in ip_version.to_versions() {
|
||||
let ping_results = network::ping_test(&target, version, count).await;
|
||||
results.extend(ping_results);
|
||||
}
|
||||
results
|
||||
}
|
||||
cli::NetworkCommands::Ports {
|
||||
target,
|
||||
protocol,
|
||||
ip_version,
|
||||
} => {
|
||||
let mut results = Vec::new();
|
||||
for version in ip_version.to_versions() {
|
||||
match protocol {
|
||||
cli::ProtocolArg::Tcp => {
|
||||
let tcp_results = network::test_tcp_common_ports(&target, version).await;
|
||||
results.extend(tcp_results);
|
||||
}
|
||||
cli::ProtocolArg::Udp => {
|
||||
let udp_results = network::test_udp_common_ports(&target, version).await;
|
||||
results.extend(udp_results);
|
||||
}
|
||||
cli::ProtocolArg::Both => {
|
||||
let tcp_results = network::test_tcp_common_ports(&target, version).await;
|
||||
let udp_results = network::test_udp_common_ports(&target, version).await;
|
||||
results.extend(tcp_results);
|
||||
results.extend(udp_results);
|
||||
}
|
||||
}
|
||||
}
|
||||
results
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_dns_command(command: cli::DnsCommands, timeout: Duration) -> Vec<TestResult> {
|
||||
match command {
|
||||
cli::DnsCommands::Query {
|
||||
domain,
|
||||
record_type,
|
||||
server,
|
||||
tcp,
|
||||
} => {
|
||||
let mut results = Vec::new();
|
||||
for rt in record_type.to_record_type() {
|
||||
let mut test = dns::DnsTest::new(domain.clone(), rt)
|
||||
.with_timeout(timeout)
|
||||
.with_tcp(tcp);
|
||||
|
||||
if let Some(server_addr) = server {
|
||||
test = test.with_server(server_addr);
|
||||
}
|
||||
|
||||
results.push(test.run().await);
|
||||
}
|
||||
results
|
||||
}
|
||||
cli::DnsCommands::Servers {
|
||||
domain,
|
||||
record_type,
|
||||
} => {
|
||||
let mut results = Vec::new();
|
||||
for rt in record_type.to_record_type() {
|
||||
let server_results = dns::test_common_dns_servers(&domain, rt).await;
|
||||
results.extend(server_results);
|
||||
}
|
||||
results
|
||||
}
|
||||
cli::DnsCommands::Categories {
|
||||
category,
|
||||
record_type,
|
||||
} => {
|
||||
let mut results = Vec::new();
|
||||
let categories = category
|
||||
.map(|c| c.to_categories())
|
||||
.unwrap_or_else(|| dns::categories::ALL_CATEGORIES.iter().collect());
|
||||
|
||||
for rt in record_type.to_record_type() {
|
||||
for cat in &categories {
|
||||
let cat_results = dns::categories::test_domain_category(cat, rt).await;
|
||||
results.extend(cat_results);
|
||||
}
|
||||
}
|
||||
results
|
||||
}
|
||||
cli::DnsCommands::Filtering => dns::categories::test_dns_filtering_effectiveness().await,
|
||||
cli::DnsCommands::Debug => {
|
||||
// Create a debug result showing DNS configuration
|
||||
let debug_info = dns::debug_dns_config();
|
||||
vec![TestResult::new("DNS Configuration Debug".to_string())
|
||||
.success(Duration::from_millis(0), debug_info)]
|
||||
}
|
||||
cli::DnsCommands::Comprehensive { domain } => {
|
||||
dns::queries::comprehensive_dns_test(&domain).await
|
||||
}
|
||||
cli::DnsCommands::Large { domain } => dns::queries::test_large_dns_queries(&domain).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_mtu_command(command: cli::MtuCommands, _timeout: Duration) -> Vec<TestResult> {
|
||||
match command {
|
||||
cli::MtuCommands::Discover { target, ip_version } => {
|
||||
let mut results = Vec::new();
|
||||
for version in ip_version.to_versions() {
|
||||
let result = mtu::full_mtu_discovery(&target, version).await;
|
||||
results.push(result);
|
||||
}
|
||||
results
|
||||
}
|
||||
cli::MtuCommands::Common { target, ip_version } => {
|
||||
let mut results = Vec::new();
|
||||
for version in ip_version.to_versions() {
|
||||
let common_results = mtu::test_common_mtu_sizes(&target, version).await;
|
||||
results.extend(common_results);
|
||||
}
|
||||
results
|
||||
}
|
||||
cli::MtuCommands::Range {
|
||||
target,
|
||||
min,
|
||||
max,
|
||||
ip_version,
|
||||
} => {
|
||||
let mut results = Vec::new();
|
||||
for version in ip_version.to_versions() {
|
||||
let discovery =
|
||||
mtu::MtuDiscovery::new(target.clone(), version).with_range(min, max);
|
||||
results.push(discovery.discover().await);
|
||||
}
|
||||
results
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_full_test(
|
||||
target: String,
|
||||
ip_version: cli::IpVersionArg,
|
||||
timeout: Duration,
|
||||
) -> Vec<TestResult> {
|
||||
let versions = ip_version.to_versions();
|
||||
let total_tests = versions.len() * 10; // Rough estimate
|
||||
|
||||
let pb = ProgressBar::new(total_tests as u64);
|
||||
pb.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template(
|
||||
"{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} {msg}",
|
||||
)
|
||||
.unwrap()
|
||||
.progress_chars("█▉▊▋▌▍▎▏ "),
|
||||
);
|
||||
|
||||
let mut all_results = Vec::new();
|
||||
|
||||
for version in versions {
|
||||
pb.set_message(format!("Testing {:?} connectivity...", version));
|
||||
|
||||
// Network tests
|
||||
let tcp_test =
|
||||
network::NetworkTest::new(target.clone(), version, network::NetworkProtocol::Tcp)
|
||||
.with_port(80)
|
||||
.with_timeout(timeout);
|
||||
all_results.push(tcp_test.run().await);
|
||||
pb.inc(1);
|
||||
|
||||
let udp_test =
|
||||
network::NetworkTest::new(target.clone(), version, network::NetworkProtocol::Udp)
|
||||
.with_port(53)
|
||||
.with_timeout(timeout);
|
||||
all_results.push(udp_test.run().await);
|
||||
pb.inc(1);
|
||||
|
||||
// ICMP test
|
||||
let ping_results = network::ping_test(&target, version, 3).await;
|
||||
all_results.extend(ping_results);
|
||||
pb.inc(3);
|
||||
|
||||
// MTU discovery
|
||||
pb.set_message(format!("Discovering MTU for {:?}...", version));
|
||||
let mtu_result = mtu::full_mtu_discovery(&target, version).await;
|
||||
all_results.push(mtu_result);
|
||||
pb.inc(1);
|
||||
|
||||
// Common MTU sizes
|
||||
let mtu_common = mtu::test_common_mtu_sizes(&target, version).await;
|
||||
all_results.extend(mtu_common);
|
||||
pb.inc(1);
|
||||
}
|
||||
|
||||
// DNS tests
|
||||
pb.set_message("Testing DNS resolution...");
|
||||
let dns_results = dns::queries::comprehensive_dns_test(&target).await;
|
||||
all_results.extend(dns_results);
|
||||
pb.inc(1);
|
||||
|
||||
// DNS servers test
|
||||
let dns_servers =
|
||||
dns::test_common_dns_servers(&target, trust_dns_client::rr::RecordType::A).await;
|
||||
all_results.extend(dns_servers);
|
||||
pb.inc(1);
|
||||
|
||||
pb.finish_with_message("Testing complete!");
|
||||
all_results
|
||||
}
|
||||
|
||||
fn print_results_human(results: &[TestResult]) {
|
||||
println!("\n{}", "=".repeat(80).blue());
|
||||
println!("{}", "Network Test Results".bold().blue());
|
||||
println!("{}", "=".repeat(80).blue());
|
||||
|
||||
let mut success_count = 0;
|
||||
let mut failure_count = 0;
|
||||
|
||||
for result in results {
|
||||
let status = if result.success {
|
||||
success_count += 1;
|
||||
"PASS".green().bold()
|
||||
} else {
|
||||
failure_count += 1;
|
||||
"FAIL".red().bold()
|
||||
};
|
||||
|
||||
let duration_str = utils::format_duration(result.duration);
|
||||
|
||||
println!("{} {} ({})", status, result.test_name, duration_str.cyan());
|
||||
|
||||
if result.success {
|
||||
if !result.details.is_empty() {
|
||||
println!(" ✓ {}", result.details.green());
|
||||
}
|
||||
} else {
|
||||
if let Some(ref error) = result.error {
|
||||
println!(" ✗ {}", error.to_string().red());
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
println!("{}", "-".repeat(80).blue());
|
||||
println!(
|
||||
"Summary: {} passed, {} failed, {} total",
|
||||
success_count.to_string().green().bold(),
|
||||
failure_count.to_string().red().bold(),
|
||||
results.len().to_string().blue().bold()
|
||||
);
|
||||
|
||||
if failure_count > 0 {
|
||||
println!("{}", "Some tests failed!".red().bold());
|
||||
} else {
|
||||
println!("{}", "All tests passed!".green().bold());
|
||||
}
|
||||
}
|
||||
|
||||
fn print_results_json(results: &[TestResult]) {
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonResult {
|
||||
test_name: String,
|
||||
success: bool,
|
||||
duration_ms: u128,
|
||||
details: Option<String>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
let json_results: Vec<JsonResult> = results
|
||||
.iter()
|
||||
.map(|r| JsonResult {
|
||||
test_name: r.test_name.clone(),
|
||||
success: r.success,
|
||||
duration_ms: r.duration.as_millis(),
|
||||
details: if r.success && !r.details.is_empty() {
|
||||
Some(r.details.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
error: r.error.as_ref().map(|e| e.to_string()),
|
||||
})
|
||||
.collect();
|
||||
|
||||
println!("{}", serde_json::to_string_pretty(&json_results).unwrap());
|
||||
}
|
||||
132
src/mtu/mod.rs
Normal file
132
src/mtu/mod.rs
Normal file
@ -0,0 +1,132 @@
|
||||
use crate::network::IpVersion;
|
||||
use crate::utils::{measure_time, NetworkError, Result, TestResult};
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct MtuDiscovery {
|
||||
pub target: String,
|
||||
pub ip_version: IpVersion,
|
||||
pub timeout: Duration,
|
||||
pub max_mtu: u16,
|
||||
pub min_mtu: u16,
|
||||
}
|
||||
|
||||
impl Default for MtuDiscovery {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
target: String::new(),
|
||||
ip_version: IpVersion::V4,
|
||||
timeout: Duration::from_secs(5),
|
||||
max_mtu: 1500,
|
||||
min_mtu: 68,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MtuDiscovery {
|
||||
pub fn new(target: String, ip_version: IpVersion) -> Self {
|
||||
Self {
|
||||
target,
|
||||
ip_version,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_range(mut self, min_mtu: u16, max_mtu: u16) -> Self {
|
||||
self.min_mtu = min_mtu;
|
||||
self.max_mtu = max_mtu;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn discover(&self) -> TestResult {
|
||||
let test_name = format!("MTU discovery for {} ({:?})", self.target, self.ip_version);
|
||||
|
||||
let (duration, result) = measure_time(|| async { self.binary_search_mtu().await }).await;
|
||||
|
||||
match result {
|
||||
Ok(mtu) => TestResult::new(test_name)
|
||||
.success(duration, format!("Discovered MTU: {} bytes", mtu)),
|
||||
Err(error) => TestResult::new(test_name).failure(duration, error),
|
||||
}
|
||||
}
|
||||
|
||||
async fn binary_search_mtu(&self) -> Result<u16> {
|
||||
let mut low = self.min_mtu;
|
||||
let mut high = self.max_mtu;
|
||||
let mut best_mtu = low;
|
||||
|
||||
while low <= high {
|
||||
let mid = (low + high) / 2;
|
||||
|
||||
match self.test_mtu_size(mid).await {
|
||||
Ok(_) => {
|
||||
best_mtu = mid;
|
||||
low = mid + 1;
|
||||
}
|
||||
Err(_) => {
|
||||
high = mid - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(best_mtu)
|
||||
}
|
||||
|
||||
async fn test_mtu_size(&self, mtu_size: u16) -> Result<()> {
|
||||
// Use system ping with packet size for MTU testing
|
||||
let ping_cmd = match self.ip_version {
|
||||
IpVersion::V4 => "ping",
|
||||
IpVersion::V6 => "ping6",
|
||||
};
|
||||
|
||||
let payload_size = match self.ip_version {
|
||||
IpVersion::V4 => mtu_size.saturating_sub(28), // IP header 20 + ICMP header 8
|
||||
IpVersion::V6 => mtu_size.saturating_sub(48), // IPv6 header 40 + ICMP header 8
|
||||
};
|
||||
|
||||
if payload_size < 8 {
|
||||
return Err(NetworkError::InvalidMtu(mtu_size));
|
||||
}
|
||||
|
||||
let output = tokio::process::Command::new(ping_cmd)
|
||||
.args(&[
|
||||
"-c",
|
||||
"1",
|
||||
"-W",
|
||||
"5000",
|
||||
"-M",
|
||||
"do", // Don't fragment
|
||||
"-s",
|
||||
&payload_size.to_string(),
|
||||
&self.target,
|
||||
])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| NetworkError::Io(e))?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(NetworkError::Other("MTU test failed".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn test_common_mtu_sizes(target: &str, ip_version: IpVersion) -> Vec<TestResult> {
|
||||
let common_sizes = [68, 576, 1280, 1500, 4464, 9000];
|
||||
let mut results = Vec::new();
|
||||
|
||||
for &size in &common_sizes {
|
||||
let discovery = MtuDiscovery::new(target.to_string(), ip_version).with_range(size, size);
|
||||
|
||||
let mut result = discovery.discover().await;
|
||||
result.test_name = format!("MTU test {} bytes for {} ({:?})", size, target, ip_version);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn full_mtu_discovery(target: &str, ip_version: IpVersion) -> TestResult {
|
||||
let discovery = MtuDiscovery::new(target.to_string(), ip_version);
|
||||
discovery.discover().await
|
||||
}
|
||||
51
src/network/icmp.rs
Normal file
51
src/network/icmp.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use super::{IpVersion, NetworkTest};
|
||||
use crate::utils::{NetworkError, Result, TestResult};
|
||||
use tokio::time::Duration;
|
||||
|
||||
impl NetworkTest {
|
||||
pub async fn test_icmp(&self) -> Result<String> {
|
||||
// Use system ping command for simplicity and compatibility
|
||||
let target = &self.target;
|
||||
|
||||
let ping_cmd = match self.ip_version {
|
||||
IpVersion::V4 => "ping",
|
||||
IpVersion::V6 => "ping6",
|
||||
};
|
||||
|
||||
let output = tokio::process::Command::new(ping_cmd)
|
||||
.args(&["-c", "1", "-W", "5000", target]) // 1 ping, 5 second timeout
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| NetworkError::Io(e))?;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if let Some(line) = stdout.lines().find(|line| line.contains("time=")) {
|
||||
Ok(format!("ICMP ping successful: {}", line.trim()))
|
||||
} else {
|
||||
Ok("ICMP ping successful".to_string())
|
||||
}
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
Err(NetworkError::Other(format!("Ping failed: {}", stderr)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn ping_test(target: &str, ip_version: IpVersion, count: u32) -> Vec<TestResult> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
for i in 0..count {
|
||||
let test = NetworkTest::new(target.to_string(), ip_version, super::NetworkProtocol::Icmp);
|
||||
|
||||
let mut result = test.run().await;
|
||||
result.test_name = format!("ICMP ping #{} to {} ({:?})", i + 1, target, ip_version);
|
||||
results.push(result);
|
||||
|
||||
if i < count - 1 {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
106
src/network/mod.rs
Normal file
106
src/network/mod.rs
Normal file
@ -0,0 +1,106 @@
|
||||
use crate::utils::{measure_time, NetworkError, Result, TestResult};
|
||||
use std::net::IpAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod icmp;
|
||||
pub mod tcp;
|
||||
pub mod udp;
|
||||
|
||||
pub use icmp::*;
|
||||
pub use tcp::*;
|
||||
pub use udp::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum IpVersion {
|
||||
V4,
|
||||
V6,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum NetworkProtocol {
|
||||
Tcp,
|
||||
Udp,
|
||||
Icmp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkTest {
|
||||
pub target: String,
|
||||
pub ip_version: IpVersion,
|
||||
pub protocol: NetworkProtocol,
|
||||
pub port: Option<u16>,
|
||||
pub timeout: Duration,
|
||||
}
|
||||
|
||||
impl NetworkTest {
|
||||
pub fn new(target: String, ip_version: IpVersion, protocol: NetworkProtocol) -> Self {
|
||||
Self {
|
||||
target,
|
||||
ip_version,
|
||||
protocol,
|
||||
port: None,
|
||||
timeout: Duration::from_secs(5),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_port(mut self, port: u16) -> Self {
|
||||
self.port = Some(port);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> TestResult {
|
||||
let test_name = format!(
|
||||
"{:?} test to {} {:?} {}",
|
||||
self.protocol,
|
||||
self.target,
|
||||
self.ip_version,
|
||||
self.port.map(|p| format!(":{}", p)).unwrap_or_default()
|
||||
);
|
||||
|
||||
let (duration, result) = measure_time(|| async {
|
||||
match self.protocol {
|
||||
NetworkProtocol::Tcp => self.test_tcp().await,
|
||||
NetworkProtocol::Udp => self.test_udp().await,
|
||||
NetworkProtocol::Icmp => self.test_icmp().await,
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(details) => TestResult::new(test_name).success(duration, details),
|
||||
Err(error) => TestResult::new(test_name).failure(duration, error),
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_target(&self) -> Result<IpAddr> {
|
||||
use trust_dns_resolver::config::*;
|
||||
use trust_dns_resolver::TokioAsyncResolver;
|
||||
|
||||
let resolver =
|
||||
TokioAsyncResolver::tokio(ResolverConfig::default(), ResolverOpts::default());
|
||||
|
||||
let lookup = match self.ip_version {
|
||||
IpVersion::V4 => {
|
||||
let response = resolver
|
||||
.ipv4_lookup(&self.target)
|
||||
.await
|
||||
.map_err(|e| NetworkError::DnsResolution(e.to_string()))?;
|
||||
response.iter().next().map(|ip| IpAddr::V4(**ip))
|
||||
}
|
||||
IpVersion::V6 => {
|
||||
let response = resolver
|
||||
.ipv6_lookup(&self.target)
|
||||
.await
|
||||
.map_err(|e| NetworkError::DnsResolution(e.to_string()))?;
|
||||
response.iter().next().map(|ip| IpAddr::V6(**ip))
|
||||
}
|
||||
};
|
||||
|
||||
lookup.ok_or_else(|| NetworkError::DnsResolution("No IP found".to_string()))
|
||||
}
|
||||
}
|
||||
43
src/network/tcp.rs
Normal file
43
src/network/tcp.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use super::{IpVersion, NetworkTest};
|
||||
use crate::utils::{NetworkError, Result, TestResult};
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
|
||||
impl NetworkTest {
|
||||
pub async fn test_tcp(&self) -> Result<String> {
|
||||
let ip = self.resolve_target().await?;
|
||||
let port = self.port.unwrap_or(80);
|
||||
let addr = SocketAddr::new(ip, port);
|
||||
|
||||
let stream = timeout(self.timeout, TcpStream::connect(addr))
|
||||
.await
|
||||
.map_err(|_| NetworkError::Timeout)?
|
||||
.map_err(|e| NetworkError::Io(e))?;
|
||||
|
||||
let local_addr = stream.local_addr().map_err(NetworkError::Io)?;
|
||||
let peer_addr = stream.peer_addr().map_err(NetworkError::Io)?;
|
||||
|
||||
Ok(format!(
|
||||
"TCP connection successful: {} -> {}",
|
||||
local_addr, peer_addr
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn test_tcp_ports(target: &str, ports: &[u16], ip_version: IpVersion) -> Vec<TestResult> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
for &port in ports {
|
||||
let test = NetworkTest::new(target.to_string(), ip_version, super::NetworkProtocol::Tcp)
|
||||
.with_port(port);
|
||||
results.push(test.run().await);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn test_tcp_common_ports(target: &str, ip_version: IpVersion) -> Vec<TestResult> {
|
||||
let common_ports = [22, 25, 53, 80, 110, 143, 443, 993, 995, 8080, 8443];
|
||||
test_tcp_ports(target, &common_ports, ip_version).await
|
||||
}
|
||||
63
src/network/udp.rs
Normal file
63
src/network/udp.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use super::{IpVersion, NetworkTest};
|
||||
use crate::utils::{NetworkError, Result, TestResult};
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::time::timeout;
|
||||
|
||||
impl NetworkTest {
|
||||
pub async fn test_udp(&self) -> Result<String> {
|
||||
let ip = self.resolve_target().await?;
|
||||
let port = self.port.unwrap_or(53);
|
||||
let addr = SocketAddr::new(ip, port);
|
||||
|
||||
let bind_addr = match self.ip_version {
|
||||
IpVersion::V4 => "0.0.0.0:0",
|
||||
IpVersion::V6 => "[::]:0",
|
||||
};
|
||||
|
||||
let socket = UdpSocket::bind(bind_addr).await.map_err(NetworkError::Io)?;
|
||||
|
||||
socket.connect(addr).await.map_err(NetworkError::Io)?;
|
||||
|
||||
let test_data = b"test";
|
||||
|
||||
let send_result = timeout(self.timeout, socket.send(test_data))
|
||||
.await
|
||||
.map_err(|_| NetworkError::Timeout)?
|
||||
.map_err(NetworkError::Io)?;
|
||||
|
||||
let mut buf = [0; 1024];
|
||||
let recv_result =
|
||||
timeout(std::time::Duration::from_millis(100), socket.recv(&mut buf)).await;
|
||||
|
||||
let details = match recv_result {
|
||||
Ok(Ok(bytes)) => format!(
|
||||
"UDP test successful: sent {} bytes, received {} bytes to {}",
|
||||
send_result, bytes, addr
|
||||
),
|
||||
_ => format!(
|
||||
"UDP test (send only): sent {} bytes to {} (no response expected for basic connectivity test)",
|
||||
send_result, addr
|
||||
),
|
||||
};
|
||||
|
||||
Ok(details)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn test_udp_ports(target: &str, ports: &[u16], ip_version: IpVersion) -> Vec<TestResult> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
for &port in ports {
|
||||
let test = NetworkTest::new(target.to_string(), ip_version, super::NetworkProtocol::Udp)
|
||||
.with_port(port);
|
||||
results.push(test.run().await);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn test_udp_common_ports(target: &str, ip_version: IpVersion) -> Vec<TestResult> {
|
||||
let common_ports = [53, 67, 68, 123, 161, 162, 514, 1194, 5353];
|
||||
test_udp_ports(target, &common_ports, ip_version).await
|
||||
}
|
||||
83
src/utils/mod.rs
Normal file
83
src/utils/mod.rs
Normal file
@ -0,0 +1,83 @@
|
||||
use std::time::{Duration, Instant};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NetworkError {
|
||||
#[error("Connection timeout")]
|
||||
Timeout,
|
||||
#[error("DNS resolution failed: {0}")]
|
||||
DnsResolution(String),
|
||||
#[error("Socket creation failed: {0}")]
|
||||
SocketCreation(String),
|
||||
#[error("Network unreachable")]
|
||||
NetworkUnreachable,
|
||||
#[error("Host unreachable")]
|
||||
HostUnreachable,
|
||||
#[error("Permission denied")]
|
||||
PermissionDenied,
|
||||
#[error("Invalid MTU size: {0}")]
|
||||
InvalidMtu(u16),
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, NetworkError>;
|
||||
|
||||
#[must_use]
|
||||
pub struct TestResult {
|
||||
pub test_name: String,
|
||||
pub success: bool,
|
||||
pub duration: Duration,
|
||||
pub details: String,
|
||||
pub error: Option<NetworkError>,
|
||||
}
|
||||
|
||||
impl TestResult {
|
||||
pub const fn new(test_name: String) -> Self {
|
||||
Self {
|
||||
test_name,
|
||||
success: false,
|
||||
duration: Duration::ZERO,
|
||||
details: String::new(),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn success(mut self, duration: Duration, details: String) -> Self {
|
||||
self.success = true;
|
||||
self.duration = duration;
|
||||
self.details = details;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn failure(mut self, duration: Duration, error: NetworkError) -> Self {
|
||||
self.success = false;
|
||||
self.duration = duration;
|
||||
self.error = Some(error);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_duration(duration: Duration) -> String {
|
||||
let ms = duration.as_millis();
|
||||
if ms < 1000 {
|
||||
format!("{ms}ms")
|
||||
} else {
|
||||
format!("{:.2}s", duration.as_secs_f32())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn measure_time<F, Fut, T>(f: F) -> (Duration, T)
|
||||
where
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: std::future::Future<Output = T>,
|
||||
{
|
||||
let start = Instant::now();
|
||||
let result = f().await;
|
||||
let duration = start.elapsed();
|
||||
(duration, result)
|
||||
}
|
||||
|
||||
mod tests;
|
||||
66
src/utils/tests.rs
Normal file
66
src/utils/tests.rs
Normal file
@ -0,0 +1,66 @@
|
||||
#[cfg(test)]
|
||||
mod unit_tests {
|
||||
use crate::utils::{format_duration, measure_time, NetworkError, TestResult};
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_format_duration_milliseconds() {
|
||||
let duration = Duration::from_millis(500);
|
||||
assert_eq!(format_duration(duration), "500ms");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_duration_seconds() {
|
||||
let duration = Duration::from_millis(1500);
|
||||
assert_eq!(format_duration(duration), "1.50s");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_result_new() {
|
||||
let result = TestResult::new("test_name".to_string());
|
||||
assert_eq!(result.test_name, "test_name");
|
||||
assert!(!result.success);
|
||||
assert_eq!(result.duration, Duration::ZERO);
|
||||
assert!(result.details.is_empty());
|
||||
assert!(result.error.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_result_success() {
|
||||
let duration = Duration::from_millis(100);
|
||||
let details = "Success details".to_string();
|
||||
let result = TestResult::new("test".to_string()).success(duration, details.clone());
|
||||
|
||||
assert!(result.success);
|
||||
assert_eq!(result.duration, duration);
|
||||
assert_eq!(result.details, details);
|
||||
assert!(result.error.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_result_failure() {
|
||||
let duration = Duration::from_millis(200);
|
||||
let error = NetworkError::Timeout;
|
||||
let result = TestResult::new("test".to_string()).failure(duration, error);
|
||||
|
||||
assert!(!result.success);
|
||||
assert_eq!(result.duration, duration);
|
||||
assert!(result.details.is_empty());
|
||||
assert!(result.error.is_some());
|
||||
|
||||
assert!(matches!(result.error, Some(NetworkError::Timeout)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_measure_time() {
|
||||
let (duration, result) = measure_time(|| async {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
"test_result"
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(duration >= Duration::from_millis(90)); // Allow some margin
|
||||
assert!(duration <= Duration::from_millis(200)); // Upper bound
|
||||
assert_eq!(result, "test_result");
|
||||
}
|
||||
}
|
||||
177
tests/integration_tests.rs
Normal file
177
tests/integration_tests.rs
Normal file
@ -0,0 +1,177 @@
|
||||
use nettest::*;
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_network_tcp_connectivity() {
|
||||
let test = network::NetworkTest::new(
|
||||
"google.com".to_string(),
|
||||
network::IpVersion::V4,
|
||||
network::NetworkProtocol::Tcp,
|
||||
)
|
||||
.with_port(80)
|
||||
.with_timeout(Duration::from_secs(10));
|
||||
|
||||
let result = test.run().await;
|
||||
assert!(result.success, "TCP test to google.com should succeed");
|
||||
assert!(result.duration > Duration::ZERO);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_network_udp_connectivity() {
|
||||
let test = network::NetworkTest::new(
|
||||
"8.8.8.8".to_string(),
|
||||
network::IpVersion::V4,
|
||||
network::NetworkProtocol::Udp,
|
||||
)
|
||||
.with_port(53)
|
||||
.with_timeout(Duration::from_secs(10));
|
||||
|
||||
let result = test.run().await;
|
||||
assert!(result.success || !result.success); // UDP might not always respond, so we test both cases
|
||||
assert!(result.duration > Duration::ZERO);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dns_query() {
|
||||
let test = dns::DnsTest::new(
|
||||
"google.com".to_string(),
|
||||
trust_dns_client::rr::RecordType::A,
|
||||
)
|
||||
.with_timeout(Duration::from_secs(10));
|
||||
|
||||
let result = test.run().await;
|
||||
assert!(result.success, "DNS A query for google.com should succeed");
|
||||
assert!(result.duration > Duration::ZERO);
|
||||
assert!(!result.details.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dns_servers() {
|
||||
let results =
|
||||
dns::test_common_dns_servers("google.com", trust_dns_client::rr::RecordType::A).await;
|
||||
assert!(!results.is_empty());
|
||||
|
||||
let successful_results = results.iter().filter(|r| r.success).count();
|
||||
assert!(
|
||||
successful_results > 0,
|
||||
"At least one DNS server should respond"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mtu_discovery() {
|
||||
let discovery = mtu::MtuDiscovery::new("google.com".to_string(), network::IpVersion::V4)
|
||||
.with_range(68, 576); // Test smaller range for speed
|
||||
|
||||
let result = discovery.discover().await;
|
||||
assert!(result.duration > Duration::ZERO);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_comprehensive_dns() {
|
||||
let results = dns::queries::comprehensive_dns_test("google.com").await;
|
||||
assert!(!results.is_empty());
|
||||
|
||||
let successful_results = results.iter().filter(|r| r.success).count();
|
||||
assert!(
|
||||
successful_results > 0,
|
||||
"At least some DNS queries should succeed"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_domain_categories() {
|
||||
let results = dns::categories::test_domain_category(
|
||||
&dns::categories::NORMAL_SITES,
|
||||
trust_dns_client::rr::RecordType::A,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(!results.is_empty());
|
||||
let successful_results = results.iter().filter(|r| r.success).count();
|
||||
assert!(successful_results > 0, "Normal sites should mostly resolve");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_error_handling() {
|
||||
let test = network::NetworkTest::new(
|
||||
"nonexistent.invalid".to_string(),
|
||||
network::IpVersion::V4,
|
||||
network::NetworkProtocol::Tcp,
|
||||
)
|
||||
.with_port(80)
|
||||
.with_timeout(Duration::from_millis(100));
|
||||
|
||||
let result = test.run().await;
|
||||
assert!(!result.success, "Test to nonexistent domain should fail");
|
||||
assert!(result.error.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_timeout_handling() {
|
||||
let test = network::NetworkTest::new(
|
||||
"192.0.2.1".to_string(), // Reserved test IP that shouldn't respond
|
||||
network::IpVersion::V4,
|
||||
network::NetworkProtocol::Tcp,
|
||||
)
|
||||
.with_port(80)
|
||||
.with_timeout(Duration::from_millis(100));
|
||||
|
||||
let result = test.run().await;
|
||||
assert!(!result.success, "Test to non-responsive IP should timeout");
|
||||
assert!(result.duration <= Duration::from_millis(200)); // Allow some margin
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ipv6_support() {
|
||||
let test = network::NetworkTest::new(
|
||||
"google.com".to_string(),
|
||||
network::IpVersion::V6,
|
||||
network::NetworkProtocol::Tcp,
|
||||
)
|
||||
.with_port(80)
|
||||
.with_timeout(Duration::from_secs(10));
|
||||
|
||||
let result = test.run().await;
|
||||
// IPv6 might not be available in all test environments, so we just check it doesn't panic
|
||||
assert!(result.duration > Duration::ZERO);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_duration() {
|
||||
assert_eq!(format_duration(Duration::from_millis(500)), "500ms");
|
||||
assert_eq!(format_duration(Duration::from_secs(2)), "2.00s");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_result_creation() {
|
||||
let result = TestResult::new("test".to_string());
|
||||
assert_eq!(result.test_name, "test");
|
||||
assert!(!result.success);
|
||||
assert_eq!(result.duration, Duration::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_result_success() {
|
||||
let result = TestResult::new("test".to_string())
|
||||
.success(Duration::from_millis(100), "details".to_string());
|
||||
|
||||
assert!(result.success);
|
||||
assert_eq!(result.duration, Duration::from_millis(100));
|
||||
assert_eq!(result.details, "details");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_result_failure() {
|
||||
let error = NetworkError::Timeout;
|
||||
let result = TestResult::new("test".to_string()).failure(Duration::from_millis(100), error);
|
||||
|
||||
assert!(!result.success);
|
||||
assert_eq!(result.duration, Duration::from_millis(100));
|
||||
assert!(result.error.is_some());
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user