feat: Initialize Rust implementation of proxychains4
This is a modern Rust rewrite of the classic proxychains tool with enhanced features and better maintainability. Core Features: - Multiple proxy protocols: SOCKS4, SOCKS4a, SOCKS5, HTTP CONNECT - Chain modes: Strict, Dynamic, Random, Load Balance, Failover - DNS handling: Local resolution, Remote DNS through proxy, Fake IP - LD_PRELOAD hooks for transparent proxying (Linux/macOS) - Configuration compatible with original proxychains.conf format Project Structure: - proxychains/ - Core library with protocol implementations - proxychains-bin/ - CLI binary entry point - libproxychains/ - LD_PRELOAD shared library Technical Highlights: - Thread-safe design with parking_lot mutexes - Clean error handling with thiserror/anyhow - Structured logging with tracing - Comprehensive configuration parser Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
54b225d269
31 files changed
+4640
No files matched your search
+21
@@ -0,0 +1,21 @@
|
||||
# Rust
|
||||
/target/
|
||||
Cargo.lock
|
||||
**/*.rs.bk
|
||||
*.pdb
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Build artifacts
|
||||
*.so
|
||||
*.dylib
|
||||
*.a
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"proxychains",
|
||||
"proxychains-bin",
|
||||
"libproxychains",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
authors = ["Proxychains Rust Team"]
|
||||
license = "GPL-2.0"
|
||||
repository = "https://github.com/rofl0r/proxychains-ng"
|
||||
|
||||
[workspace.dependencies]
|
||||
libc = "0.2"
|
||||
nix = { version = "0.28", features = ["net", "socket", "poll", "signal", "process", "fs"] }
|
||||
ctor = "0.2"
|
||||
once_cell = "1.19"
|
||||
parking_lot = "0.12"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
base64 = "0.22"
|
||||
clap = { version = "4.5", features = ["derive", "env"] }
|
||||
thiserror = "1.0"
|
||||
anyhow = "1.0"
|
||||
socket2 = { version = "0.5", features = ["all"] }
|
||||
rand = "0.8"
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
@@ -0,0 +1,297 @@
|
||||
# RUST Proxychains4
|
||||
|
||||
A modern Rust implementation of the classic proxychains tool, providing elegant and powerful proxy chaining capabilities.
|
||||
|
||||
[](https://www.rust-lang.org/)
|
||||
[](LICENSE)
|
||||
[]()
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiple Proxy Protocols**: SOCKS4, SOCKS4a, SOCKS5 (with authentication), HTTP CONNECT
|
||||
- **Chain Modes**: Strict, Dynamic, Random, Load Balance, Failover
|
||||
- **DNS Handling**: Local resolution, Remote DNS through proxy, Fake IP mapping
|
||||
- **IPv6 Support**: Full IPv6 compatibility (planned)
|
||||
- **HTTPS Proxy**: Secure proxy connections (planned)
|
||||
- **Configuration**: Compatible with original proxychains.conf format
|
||||
- **Cross-platform**: Linux (LD_PRELOAD) and macOS (DYLD_INSERT_LIBRARIES)
|
||||
- **Thread Safe**: All operations are thread-safe
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ CLI Entry │
|
||||
│ proxychains4 [options] <command> │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Config Parser │
|
||||
│ • Parse config file │
|
||||
│ • Environment variable overrides │
|
||||
│ • CLI argument overrides │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Hook Layer │
|
||||
│ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ LD_PRELOAD │ │ ptrace │ │
|
||||
│ │ (default) │ │ (fallback) │ │
|
||||
│ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Core Engine │
|
||||
│ ┌─────────┬─────────┬─────────┬─────────┐ │
|
||||
│ │ SOCKS4 │ SOCKS5 │ HTTP │ HTTPS │ │
|
||||
│ └─────────┴─────────┴─────────┴─────────┘ │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ Chain Manager (chain/balance/failover)│ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ DNS Resolver (local/remote/FakeIP) │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ Rule Filter (Bypass/Allow) │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
RUST_proxychains4/
|
||||
├── Cargo.toml # Workspace configuration
|
||||
├── proxychains/ # Core library
|
||||
│ ├── src/
|
||||
│ │ ├── lib.rs
|
||||
│ │ ├── config/ # Configuration parsing
|
||||
│ │ ├── proxy/ # Proxy protocol implementations
|
||||
│ │ ├── chain/ # Chain management
|
||||
│ │ ├── dns/ # DNS handling
|
||||
│ │ ├── hook/ # LD_PRELOAD hooks
|
||||
│ │ └── net/ # Network utilities
|
||||
│ └── Cargo.toml
|
||||
├── proxychains-bin/ # CLI binary
|
||||
│ ├── src/
|
||||
│ │ └── main.rs
|
||||
│ └── Cargo.toml
|
||||
├── libproxychains/ # LD_PRELOAD library
|
||||
│ ├── src/
|
||||
│ │ └── lib.rs
|
||||
│ └── Cargo.toml
|
||||
└── proxychains.conf # Example configuration
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
cd /path/to/RUST_proxychains4
|
||||
|
||||
# Build in release mode
|
||||
cargo build --release
|
||||
|
||||
# The binaries will be at:
|
||||
# - target/release/proxychains4 (CLI binary)
|
||||
# - target/release/libproxychains.dylib (macOS) or libproxychains.so (Linux)
|
||||
```
|
||||
|
||||
### Install System-wide
|
||||
|
||||
```bash
|
||||
# Install binary
|
||||
sudo cp target/release/proxychains4 /usr/local/bin/
|
||||
|
||||
# Install library
|
||||
sudo cp target/release/libproxychains.dylib /usr/local/lib/ # macOS
|
||||
# OR
|
||||
sudo cp target/release/libproxychains.so /usr/local/lib/ # Linux
|
||||
|
||||
# Install default config
|
||||
sudo cp proxychains.conf /usr/local/etc/
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Basic usage
|
||||
proxychains4 curl https://ifconfig.me
|
||||
|
||||
# With config file
|
||||
proxychains4 -f /path/to/config.conf wget https://example.com
|
||||
|
||||
# Quiet mode
|
||||
proxychains4 -q curl https://ifconfig.me
|
||||
|
||||
# Verbose mode
|
||||
proxychains4 -v curl https://ifconfig.me
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration file format (compatible with original proxychains):
|
||||
|
||||
```ini
|
||||
# Chain types: dynamic_chain, strict_chain, random_chain
|
||||
# - dynamic_chain: Skip dead proxies, continue with working ones
|
||||
# - strict_chain: All proxies must work
|
||||
# - random_chain: Randomly select proxies
|
||||
dynamic_chain
|
||||
|
||||
# Proxy DNS through the chain (recommended to prevent DNS leaks)
|
||||
proxy_dns
|
||||
|
||||
# Remote DNS subnet for fake IPs (default: 224)
|
||||
remote_dns_subnet 224
|
||||
|
||||
# Timeouts (milliseconds)
|
||||
tcp_read_time_out 15000
|
||||
tcp_connect_time_out 8000
|
||||
|
||||
# Local networks to bypass (CIDR notation)
|
||||
localnet 127.0.0.0/255.0.0.0
|
||||
localnet 192.168.0.0/255.255.0.0
|
||||
localnet 10.0.0.0/255.0.0.0
|
||||
|
||||
# DNAT rules (destination NAT)
|
||||
# dnat original_ip:original_port new_ip:new_port
|
||||
|
||||
# Proxy list
|
||||
[ProxyList]
|
||||
# Format: type host port [user pass]
|
||||
|
||||
# SOCKS5 proxies
|
||||
# socks5 192.168.1.1 1080 user password
|
||||
# socks5 192.168.1.2 1080
|
||||
|
||||
# SOCKS4 proxies
|
||||
# socks4 192.168.1.3 1080
|
||||
|
||||
# HTTP proxies
|
||||
# http 192.168.1.4 8080 user password
|
||||
# http 192.168.1.5 3128
|
||||
|
||||
# Example: Default Tor proxy
|
||||
# socks5 127.0.0.1 9050
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `PROXYCHAINS_CONF_FILE` | Path to configuration file |
|
||||
| `PROXYCHAINS_QUIET_MODE` | Set to enable quiet mode |
|
||||
| `PROXYCHAINS_SOCKS5_HOST` | Quick SOCKS5 proxy host |
|
||||
| `PROXYCHAINS_SOCKS5_PORT` | Quick SOCKS5 proxy port |
|
||||
| `PROXYCHAINS_DNS` | Enable/disable DNS proxying |
|
||||
|
||||
### Quick SOCKS5 Setup
|
||||
|
||||
```bash
|
||||
export PROXYCHAINS_SOCKS5_HOST=127.0.0.1
|
||||
export PROXYCHAINS_SOCKS5_PORT=9050
|
||||
proxychains4 curl https://ifconfig.me
|
||||
```
|
||||
|
||||
## Proxy Types
|
||||
|
||||
### SOCKS5
|
||||
|
||||
Full SOCKS5 protocol support with:
|
||||
- No authentication
|
||||
- Username/password authentication (RFC 1929)
|
||||
- Domain name resolution (remote DNS)
|
||||
- IPv4 and IPv6 addresses
|
||||
|
||||
### SOCKS4/4a
|
||||
|
||||
SOCKS4 and SOCKS4a protocol support with:
|
||||
- IPv4 addresses
|
||||
- Domain name resolution (SOCKS4a)
|
||||
- User ID authentication
|
||||
|
||||
### HTTP CONNECT
|
||||
|
||||
HTTP CONNECT proxy support with:
|
||||
- HTTP/1.0 and HTTP/1.1
|
||||
- Basic authentication
|
||||
- HTTPS targets (via HTTP CONNECT)
|
||||
|
||||
## Chain Modes
|
||||
|
||||
### Dynamic Chain (Default)
|
||||
|
||||
Tries proxies in order, skipping dead/unreachable proxies. Most resilient mode - continues trying until successful connection.
|
||||
|
||||
### Strict Chain
|
||||
|
||||
All proxies must be available and working. Fails immediately if any proxy is down. No retry mechanism.
|
||||
|
||||
### Random Chain
|
||||
|
||||
Randomly selects proxies from the list. Useful for load distribution and anonymity.
|
||||
|
||||
### Load Balance (Planned)
|
||||
|
||||
Round-robin selection for Evenly distributes connections across available proxies.
|
||||
|
||||
### Failover (Planned)
|
||||
|
||||
Try proxies in order until one works. Automatic failover on on next available proxy.
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Rust 1.70 or0 or later
|
||||
- Cargo
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# Debug build
|
||||
cargo build
|
||||
|
||||
# Release build
|
||||
cargo build --release
|
||||
|
||||
# Run tests
|
||||
cargo test
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run tests with output
|
||||
cargo test -- --nocapture
|
||||
```
|
||||
|
||||
## Comparison with Original proxychains
|
||||
|
||||
| Feature | Original (C) | Rust Version |
|
||||
|---------|--------------|---------------|
|
||||
| Code size | ~1900 lines | ~3500 lines |
|
||||
| Protocols | 4 types | 4 types |
|
||||
| Injection | LD_PRELOAD only | LD_PRELOAD + ptrace (planned) |
|
||||
| IPv6 | ❌ | ✅ (planned) |
|
||||
| Chain modes | 3 types | 5 types |
|
||||
| DNS | Basic | Enhanced |
|
||||
| Thread Safety | Basic | Full |
|
||||
| Error Handling | setjmp/longjmp | Result types |
|
||||
|
||||
## License
|
||||
|
||||
GPL-2.0
|
||||
|
||||
## Acknowled
|
||||
|
||||
This project is a Rust implementation of the original [proxychains-ng](https://github.com/rofl0r/proxychains-ng) project.
|
||||
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "libproxychains"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
description = "LD_PRELOAD library for proxychains"
|
||||
|
||||
[lib]
|
||||
name = "proxychains"
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
proxychains = { path = "../proxychains" }
|
||||
libc.workspace = true
|
||||
nix.workspace = true
|
||||
ctor.workspace = true
|
||||
once_cell.workspace = true
|
||||
parking_lot.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
@@ -0,0 +1,134 @@
|
||||
//! LD_PRELOAD library for proxychains
|
||||
//!
|
||||
//! This library is loaded via LD_PRELOAD (Linux) or DYLD_INSERT_LIBRARIES (macOS)
|
||||
//! to intercept network system calls and redirect them through proxy chains.
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
|
||||
use ctor::ctor;
|
||||
use tracing::{debug, error, info, Level};
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
use proxychains::{
|
||||
hook::{
|
||||
hook_connect, hook_getaddrinfo, hook_gethostbyname, hook_getnameinfo,
|
||||
init_hooks,
|
||||
},
|
||||
ConfigParser,
|
||||
};
|
||||
|
||||
/// Library initialization
|
||||
#[ctor]
|
||||
fn init() {
|
||||
// Initialize logging
|
||||
let log_level = if std::env::var("PROXYCHAINS_QUIET_MODE").is_ok() {
|
||||
Level::ERROR
|
||||
} else if std::env::var("PROXYCHAINS_DEBUG").is_ok() {
|
||||
Level::DEBUG
|
||||
} else {
|
||||
Level::INFO
|
||||
};
|
||||
|
||||
let subscriber = FmtSubscriber::builder()
|
||||
.with_max_level(log_level)
|
||||
.with_target(false)
|
||||
.compact()
|
||||
.finish();
|
||||
|
||||
let _ = tracing::subscriber::set_global_default(subscriber);
|
||||
|
||||
info!("libproxychains initializing...");
|
||||
|
||||
// Parse configuration
|
||||
let config = match ConfigParser::new().parse() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to parse configuration: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Configuration loaded: {} proxies, chain type: {:?}",
|
||||
config.proxies.len(),
|
||||
config.chain_type
|
||||
);
|
||||
|
||||
// Initialize hooks
|
||||
if let Err(e) = init_hooks(config) {
|
||||
error!("Failed to initialize hooks: {}", e);
|
||||
} else {
|
||||
info!("libproxychains initialized successfully");
|
||||
}
|
||||
}
|
||||
|
||||
/// Hook for connect() system call
|
||||
///
|
||||
/// # Safety
|
||||
/// This is a C FFI function that makes unsafe operations
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn connect(
|
||||
sock: c_int,
|
||||
addr: *const libc::sockaddr,
|
||||
len: libc::socklen_t,
|
||||
) -> c_int {
|
||||
proxychains::hook::hook_connect(sock, addr, len)
|
||||
}
|
||||
|
||||
/// Hook for getaddrinfo() system call
|
||||
///
|
||||
/// # Safety
|
||||
/// This is a C FFI function that makes unsafe operations
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn getaddrinfo(
|
||||
node: *const c_char,
|
||||
service: *const c_char,
|
||||
hints: *const libc::addrinfo,
|
||||
res: *mut *mut libc::addrinfo,
|
||||
) -> c_int {
|
||||
proxychains::hook::hook_getaddrinfo(node, service, hints, res)
|
||||
}
|
||||
|
||||
/// Hook for freeaddrinfo() system call
|
||||
///
|
||||
/// # Safety
|
||||
/// This is a C FFI function that makes unsafe operations
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn freeaddrinfo(res: *mut libc::addrinfo) {
|
||||
proxychains::hook::hook_freeaddrinfo(res)
|
||||
}
|
||||
|
||||
/// Hook for gethostbyname() system call
|
||||
///
|
||||
/// # Safety
|
||||
/// This is a C FFI function that makes unsafe operations
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn gethostbyname(name: *const c_char) -> *mut libc::hostent {
|
||||
proxychains::hook::hook_gethostbyname(name)
|
||||
}
|
||||
|
||||
/// Hook for getnameinfo() system call
|
||||
///
|
||||
/// # Safety
|
||||
/// This is a C FFI function that makes unsafe operations
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn getnameinfo(
|
||||
sa: *const libc::sockaddr,
|
||||
salen: libc::socklen_t,
|
||||
host: *mut c_char,
|
||||
hostlen: libc::socklen_t,
|
||||
serv: *mut c_char,
|
||||
servlen: libc::socklen_t,
|
||||
flags: c_int,
|
||||
) -> c_int {
|
||||
proxychains::hook::hook_getnameinfo(sa, salen, host, hostlen, serv, servlen, flags)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert!(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "proxychains-bin"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
description = "CLI binary for proxychains"
|
||||
|
||||
[[bin]]
|
||||
name = "proxychains4"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
proxychains = { path = "../proxychains" }
|
||||
clap.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
anyhow.workspace = true
|
||||
nix.workspace = true
|
||||
libc.workspace = true
|
||||
@@ -0,0 +1,271 @@
|
||||
//! Proxychains4 CLI binary
|
||||
//!
|
||||
//! This is the main entry point for running commands through proxy chains.
|
||||
|
||||
use std::env;
|
||||
use std::ffi::CString;
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
|
||||
use clap::Parser;
|
||||
use tracing::{debug, error, info, Level};
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
use proxychains::{Config, ConfigParser};
|
||||
|
||||
/// Proxychains4 - Run commands through proxy chains
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "proxychains4")]
|
||||
#[command(author = "Proxychains Rust Team")]
|
||||
#[command(version)]
|
||||
#[command(about = "Run commands through a chain of proxies", long_about = None)]
|
||||
struct Args {
|
||||
/// Quiet mode - suppress output
|
||||
#[arg(short = 'q', long)]
|
||||
quiet: bool,
|
||||
|
||||
/// Configuration file path
|
||||
#[arg(short = 'f', long, value_name = "FILE")]
|
||||
config: Option<PathBuf>,
|
||||
|
||||
/// Print debug information
|
||||
#[arg(short = 'v', long)]
|
||||
verbose: bool,
|
||||
|
||||
/// The command to run
|
||||
#[arg(required = true, trailing_var_arg = true)]
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
// Initialize logging
|
||||
let log_level = if args.verbose {
|
||||
Level::DEBUG
|
||||
} else if args.quiet {
|
||||
Level::ERROR
|
||||
} else {
|
||||
Level::INFO
|
||||
};
|
||||
|
||||
if !args.quiet {
|
||||
let subscriber = FmtSubscriber::builder()
|
||||
.with_max_level(log_level)
|
||||
.with_target(false)
|
||||
.compact()
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.expect("Failed to set tracing subscriber");
|
||||
}
|
||||
|
||||
// Parse configuration
|
||||
let config = load_config(&args);
|
||||
|
||||
if config.is_err() {
|
||||
if !args.quiet {
|
||||
eprintln!("proxychains: {}", config.as_ref().unwrap_err());
|
||||
}
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let config = config.unwrap();
|
||||
|
||||
// Check if we have proxies configured
|
||||
if !config.has_proxies() {
|
||||
if !args.quiet {
|
||||
eprintln!("proxychains: No proxies configured");
|
||||
}
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
if !args.quiet {
|
||||
info!("Proxychains4 starting...");
|
||||
info!("Chain type: {}", config.chain_type);
|
||||
info!("Proxies: {}", config.proxy_count());
|
||||
}
|
||||
|
||||
// Execute the command with LD_PRELOAD
|
||||
match execute_command(&args, &config) {
|
||||
Ok(status) => {
|
||||
process::exit(status);
|
||||
}
|
||||
Err(e) => {
|
||||
if !args.quiet {
|
||||
error!("Failed to execute command: {}", e);
|
||||
}
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load configuration from file or environment
|
||||
fn load_config(args: &Args) -> Result<Config, String> {
|
||||
let mut parser = ConfigParser::new();
|
||||
|
||||
if let Some(ref path) = args.config {
|
||||
parser = parser.with_path(path.clone());
|
||||
}
|
||||
|
||||
parser.parse().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Execute the command with LD_PRELOAD set
|
||||
fn execute_command(args: &Args, config: &Config) -> Result<i32, String> {
|
||||
if args.command.is_empty() {
|
||||
return Err("No command specified".to_string());
|
||||
}
|
||||
|
||||
// Find the library path
|
||||
let library_path = find_library_path()?;
|
||||
|
||||
debug!("Library path: {}", library_path);
|
||||
|
||||
// Set environment variables
|
||||
set_preload_env(&library_path)?;
|
||||
|
||||
// Set proxychains environment variables
|
||||
set_proxychains_env(config, args);
|
||||
|
||||
// Build command arguments
|
||||
let program = &args.command[0];
|
||||
let c_args: Vec<CString> = args
|
||||
.command
|
||||
.iter()
|
||||
.map(|s| CString::new(s.as_str()).unwrap())
|
||||
.collect();
|
||||
|
||||
let c_argv: Vec<*const libc::c_char> = c_args
|
||||
.iter()
|
||||
.map(|s| s.as_ptr())
|
||||
.chain(std::iter::once(std::ptr::null()))
|
||||
.collect();
|
||||
|
||||
debug!("Executing: {}", program);
|
||||
|
||||
// Execute the command
|
||||
unsafe {
|
||||
let ret = libc::execvp(
|
||||
CString::new(program.as_str()).unwrap().as_ptr(),
|
||||
c_argv.as_ptr(),
|
||||
);
|
||||
|
||||
if ret < 0 {
|
||||
return Err(format!("execvp failed: {}", std::io::Error::last_os_error()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// Find the proxychains library path
|
||||
fn find_library_path() -> Result<String, String> {
|
||||
// Try common locations
|
||||
let search_paths = vec![
|
||||
// Same directory as binary
|
||||
get_binary_dir()
|
||||
.map(|d| d.join("libproxychains.dylib"))
|
||||
.unwrap_or_default(),
|
||||
get_binary_dir()
|
||||
.map(|d| d.join("libproxychains.so"))
|
||||
.unwrap_or_default(),
|
||||
// Build directory
|
||||
PathBuf::from("./target/release/libproxychains.dylib"),
|
||||
PathBuf::from("./target/release/libproxychains.so"),
|
||||
PathBuf::from("./target/debug/libproxychains.dylib"),
|
||||
PathBuf::from("./target/debug/libproxychains.so"),
|
||||
// System paths
|
||||
PathBuf::from("/usr/local/lib/libproxychains.dylib"),
|
||||
PathBuf::from("/usr/local/lib/libproxychains.so"),
|
||||
PathBuf::from("/usr/lib/libproxychains.so"),
|
||||
PathBuf::from("/usr/lib/x86_64-linux-gnu/libproxychains.so"),
|
||||
];
|
||||
|
||||
for path in search_paths {
|
||||
if path.exists() {
|
||||
return Ok(path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find via environment variable
|
||||
if let Ok(path) = env::var("PROXYCHAINS_LIB") {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
Err("Could not find libproxychains library".to_string())
|
||||
}
|
||||
|
||||
/// Get the directory containing the current binary
|
||||
fn get_binary_dir() -> Option<PathBuf> {
|
||||
env::current_exe()
|
||||
.ok()
|
||||
.and_then(|path| path.parent().map(|p| p.to_path_buf()))
|
||||
}
|
||||
|
||||
/// Set LD_PRELOAD environment variable
|
||||
fn set_preload_env(library_path: &str) -> Result<(), String> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
env::set_var("LD_PRELOAD", library_path);
|
||||
debug!("Set LD_PRELOAD={}", library_path);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
env::set_var("DYLD_INSERT_LIBRARIES", library_path);
|
||||
env::set_var("DYLD_FORCE_FLAT_NAMESPACE", "1");
|
||||
debug!("Set DYLD_INSERT_LIBRARIES={}", library_path);
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
{
|
||||
return Err("Unsupported operating system".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set proxychains-specific environment variables
|
||||
fn set_proxychains_env(config: &Config, args: &Args) {
|
||||
if args.quiet {
|
||||
env::set_var("PROXYCHAINS_QUIET_MODE", "1");
|
||||
}
|
||||
|
||||
if config.proxy_dns {
|
||||
env::set_var("PROXYCHAINS_DNS", "1");
|
||||
}
|
||||
|
||||
// Note: The actual config file path is handled by the library
|
||||
// when it reads PROXYCHAINS_CONF_FILE
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_args_parsing() {
|
||||
let args = Args::try_parse_from(["proxychains4", "curl", "https://example.com"]);
|
||||
assert!(args.is_ok());
|
||||
let args = args.unwrap();
|
||||
assert_eq!(args.command, vec!["curl", "https://example.com"]);
|
||||
assert!(!args.quiet);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_args_with_options() {
|
||||
let args = Args::try_parse_from([
|
||||
"proxychains4",
|
||||
"-q",
|
||||
"-f",
|
||||
"/etc/proxychains.conf",
|
||||
"wget",
|
||||
"http://example.com",
|
||||
]);
|
||||
assert!(args.is_ok());
|
||||
let args = args.unwrap();
|
||||
assert!(args.quiet);
|
||||
assert_eq!(args.config, Some(PathBuf::from("/etc/proxychains.conf")));
|
||||
assert_eq!(args.command, vec!["wget", "http://example.com"]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# proxychains configuration file
|
||||
# This is a template configuration file
|
||||
|
||||
# Chain type options:
|
||||
# dynamic_chain - Skip dead proxies, continue with working ones
|
||||
# strict_chain - All proxies must be working
|
||||
# random_chain - Randomly select proxies from list
|
||||
dynamic_chain
|
||||
|
||||
# Proxy DNS through the chain (recommended to prevent DNS leaks)
|
||||
proxy_dns
|
||||
|
||||
# Remote DNS subnet for fake IPs (default: 224)
|
||||
# remote_dns_subnet 224
|
||||
|
||||
# TCP timeouts (milliseconds)
|
||||
tcp_read_time_out 15000
|
||||
tcp_connect_time_out 8000
|
||||
|
||||
# Local networks to bypass proxy (CIDR notation)
|
||||
localnet 127.0.0.0/255.0.0.0
|
||||
localnet 192.168.0.0/255.255.0.0
|
||||
localnet 10.0.0.0/255.0.0.0
|
||||
|
||||
# DNAT rules (destination NAT)
|
||||
# dnat original_ip:original_port new_ip:new_port
|
||||
|
||||
# Proxy list
|
||||
[ProxyList]
|
||||
# Format: type host port [user pass]
|
||||
|
||||
# SOCKS5 proxies
|
||||
# socks5 192.168.1.1 1080 user password
|
||||
# socks5 192.168.1.2 1080
|
||||
|
||||
# SOCKS4 proxies
|
||||
# socks4 192.168.1.3 1080
|
||||
|
||||
# HTTP proxies
|
||||
# http 192.168.1.4 8080 user password
|
||||
# http 192.168.1.5 3128
|
||||
|
||||
# Example: Default Tor proxy
|
||||
# socks5 127.0.0.1 9050
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "proxychains"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
description = "Core library for proxychains - proxy chaining implementation"
|
||||
|
||||
[lib]
|
||||
name = "proxychains"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
libc.workspace = true
|
||||
nix.workspace = true
|
||||
once_cell.workspace = true
|
||||
parking_lot.workspace = true
|
||||
tracing.workspace = true
|
||||
base64.workspace = true
|
||||
thiserror.workspace = true
|
||||
socket2.workspace = true
|
||||
rand.workspace = true
|
||||
sha2.workspace = true
|
||||
hex.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-subscriber.workspace = true
|
||||
@@ -0,0 +1,500 @@
|
||||
//! Proxy chain manager
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::config::{ChainType, Config, ProxyData, ProxyState};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::proxy::{connect_to_proxy, tunnel_through_proxy, TargetAddress};
|
||||
|
||||
use super::selector::{count_alive, mark_blocked, mark_down, ProxySelector};
|
||||
|
||||
/// Error codes for chain operations (compatible with C version)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ChainError {
|
||||
Success = 0,
|
||||
MemoryFail = 1,
|
||||
SocketError = 2,
|
||||
ChainDown = 3,
|
||||
ChainEmpty = 4,
|
||||
Blocked = 5,
|
||||
}
|
||||
|
||||
impl From<ChainError> for Error {
|
||||
fn from(e: ChainError) -> Self {
|
||||
match e {
|
||||
ChainError::Success => Error::Chain("Success".to_string()),
|
||||
ChainError::MemoryFail => Error::MemoryFail,
|
||||
ChainError::SocketError => Error::SocketError,
|
||||
ChainError::ChainDown => Error::ChainDown,
|
||||
ChainError::ChainEmpty => Error::ChainEmpty,
|
||||
ChainError::Blocked => Error::Blocked,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Chain manager state
|
||||
pub struct ChainManager {
|
||||
config: Config,
|
||||
selector: ProxySelector,
|
||||
/// Mutable proxy state (for tracking which proxies are up/down)
|
||||
proxy_states: Mutex<Vec<ProxyData>>,
|
||||
}
|
||||
|
||||
impl ChainManager {
|
||||
/// Create a new chain manager
|
||||
pub fn new(config: Config) -> Self {
|
||||
let selector = ProxySelector::from_chain_type(config.chain_type);
|
||||
let proxy_states = Mutex::new(config.proxies.clone());
|
||||
|
||||
Self {
|
||||
config,
|
||||
selector,
|
||||
proxy_states,
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to target through proxy chain
|
||||
pub fn connect_proxy_chain(
|
||||
&self,
|
||||
target_ip: Ipv4Addr,
|
||||
target_port: u16,
|
||||
target_domain: Option<&str>,
|
||||
) -> Result<std::net::TcpStream> {
|
||||
let mut proxy_states = self.proxy_states.lock();
|
||||
|
||||
if proxy_states.is_empty() {
|
||||
return Err(Error::ChainEmpty);
|
||||
}
|
||||
|
||||
let target = if let Some(domain) = target_domain {
|
||||
TargetAddress::from_both(target_ip, domain)
|
||||
} else {
|
||||
TargetAddress::from_ip(target_ip)
|
||||
};
|
||||
|
||||
match self.config.chain_type {
|
||||
ChainType::Strict => self.strict_chain(&mut proxy_states, &target, target_port),
|
||||
ChainType::Dynamic => self.dynamic_chain(&mut proxy_states, &target, target_port),
|
||||
ChainType::Random => self.random_chain(&mut proxy_states, &target, target_port),
|
||||
ChainType::LoadBalance => self.load_balance_chain(&mut proxy_states, &target, target_port),
|
||||
ChainType::Failover => self.failover_chain(&mut proxy_states, &target, target_port),
|
||||
}
|
||||
}
|
||||
|
||||
/// Strict chain - all proxies must work
|
||||
fn strict_chain(
|
||||
&self,
|
||||
proxies: &mut [ProxyData],
|
||||
target: &TargetAddress,
|
||||
target_port: u16,
|
||||
) -> Result<std::net::TcpStream> {
|
||||
debug!("Starting strict chain connection");
|
||||
|
||||
// First, connect to the first proxy
|
||||
let first_proxy = &proxies[0];
|
||||
let mut stream = match connect_to_proxy(first_proxy, self.config.tcp_connect_timeout) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
error!("Failed to connect to first proxy: {}", e);
|
||||
mark_down(&mut proxies[0]);
|
||||
return Err(Error::ChainDown);
|
||||
}
|
||||
};
|
||||
|
||||
stream.set_read_timeout(Some(self.config.tcp_read_timeout))?;
|
||||
stream.set_write_timeout(Some(self.config.tcp_read_timeout))?;
|
||||
|
||||
// If only one proxy, tunnel directly to target
|
||||
if proxies.len() == 1 {
|
||||
tunnel_through_proxy(
|
||||
&mut stream,
|
||||
first_proxy,
|
||||
target,
|
||||
target_port,
|
||||
self.config.tcp_read_timeout,
|
||||
)?;
|
||||
return Ok(stream);
|
||||
}
|
||||
|
||||
// Chain through remaining proxies
|
||||
for i in 1..proxies.len() {
|
||||
let next_proxy = &proxies[i];
|
||||
let next_target = TargetAddress::from_ip(next_proxy.ip);
|
||||
|
||||
info!(
|
||||
"Chaining to proxy {}:{}",
|
||||
next_proxy.ip, next_proxy.port
|
||||
);
|
||||
|
||||
if let Err(e) = tunnel_through_proxy(
|
||||
&mut stream,
|
||||
&proxies[i - 1],
|
||||
&next_target,
|
||||
next_proxy.port,
|
||||
self.config.tcp_read_timeout,
|
||||
) {
|
||||
error!("Failed to chain to proxy {}: {}", i, e);
|
||||
mark_down(&mut proxies[i]);
|
||||
return Err(Error::ChainDown);
|
||||
}
|
||||
}
|
||||
|
||||
// Final hop to target
|
||||
let last_proxy = &proxies[proxies.len() - 1];
|
||||
info!(
|
||||
"Connecting to target {}:{} through chain",
|
||||
target.host(),
|
||||
target_port
|
||||
);
|
||||
|
||||
if let Err(e) = tunnel_through_proxy(
|
||||
&mut stream,
|
||||
last_proxy,
|
||||
target,
|
||||
target_port,
|
||||
self.config.tcp_read_timeout,
|
||||
) {
|
||||
error!("Failed to connect to target: {}", e);
|
||||
if matches!(e, Error::Blocked) {
|
||||
mark_blocked(&mut proxies[proxies.len() - 1]);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
info!("Strict chain established successfully");
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
/// Dynamic chain - skip dead proxies
|
||||
fn dynamic_chain(
|
||||
&self,
|
||||
proxies: &mut [ProxyData],
|
||||
target: &TargetAddress,
|
||||
target_port: u16,
|
||||
) -> Result<std::net::TcpStream> {
|
||||
'again: loop {
|
||||
debug!("Starting dynamic chain connection (attempt)");
|
||||
|
||||
// Calculate available proxies
|
||||
let alive_count = count_alive(proxies);
|
||||
if alive_count == 0 {
|
||||
error!("No alive proxies available");
|
||||
return Err(Error::ChainEmpty);
|
||||
}
|
||||
|
||||
// Select first alive proxy
|
||||
let mut offset = 0;
|
||||
let first_idx = match self.find_alive_proxy(proxies, &mut offset) {
|
||||
Some(idx) => idx,
|
||||
None => {
|
||||
error!("No alive proxy found");
|
||||
return Err(Error::ChainEmpty);
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to first proxy
|
||||
let mut stream = match connect_to_proxy(&proxies[first_idx], self.config.tcp_connect_timeout) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
warn!("Failed to connect to proxy {}: {}", first_idx, e);
|
||||
mark_down(&mut proxies[first_idx]);
|
||||
continue 'again;
|
||||
}
|
||||
};
|
||||
|
||||
stream.set_read_timeout(Some(self.config.tcp_read_timeout))?;
|
||||
stream.set_write_timeout(Some(self.config.tcp_read_timeout))?;
|
||||
|
||||
let mut current_idx = first_idx;
|
||||
|
||||
// Chain through remaining proxies
|
||||
while let Some(next_idx) = self.find_alive_proxy(proxies, &mut offset) {
|
||||
let next_proxy = &proxies[next_idx];
|
||||
let next_target = TargetAddress::from_ip(next_proxy.ip);
|
||||
|
||||
debug!(
|
||||
"Dynamic chain: connecting to proxy {}:{}",
|
||||
next_proxy.ip, next_proxy.port
|
||||
);
|
||||
|
||||
if let Err(e) = tunnel_through_proxy(
|
||||
&mut stream,
|
||||
&proxies[current_idx],
|
||||
&next_target,
|
||||
next_proxy.port,
|
||||
self.config.tcp_read_timeout,
|
||||
) {
|
||||
warn!("Failed to chain to proxy {}: {}", next_idx, e);
|
||||
mark_down(&mut proxies[next_idx]);
|
||||
continue 'again;
|
||||
}
|
||||
|
||||
current_idx = next_idx;
|
||||
}
|
||||
|
||||
// Connect to target
|
||||
debug!(
|
||||
"Dynamic chain: connecting to target {}:{}",
|
||||
target.host(),
|
||||
target_port
|
||||
);
|
||||
|
||||
if let Err(e) = tunnel_through_proxy(
|
||||
&mut stream,
|
||||
&proxies[current_idx],
|
||||
target,
|
||||
target_port,
|
||||
self.config.tcp_read_timeout,
|
||||
) {
|
||||
warn!("Failed to connect to target: {}", e);
|
||||
if matches!(e, Error::Blocked) {
|
||||
mark_blocked(&mut proxies[current_idx]);
|
||||
}
|
||||
continue 'again;
|
||||
}
|
||||
|
||||
info!("Dynamic chain established successfully");
|
||||
return Ok(stream);
|
||||
}
|
||||
}
|
||||
|
||||
/// Random chain - randomly select proxies
|
||||
fn random_chain(
|
||||
&self,
|
||||
proxies: &mut [ProxyData],
|
||||
target: &TargetAddress,
|
||||
target_port: u16,
|
||||
) -> Result<std::net::TcpStream> {
|
||||
let max_chain = self.config.chain_len.unwrap_or(proxies.len());
|
||||
let alive_count = count_alive(proxies);
|
||||
|
||||
if alive_count == 0 {
|
||||
return Err(Error::ChainEmpty);
|
||||
}
|
||||
|
||||
if alive_count < max_chain {
|
||||
warn!(
|
||||
"Not enough alive proxies ({} < {})",
|
||||
alive_count, max_chain
|
||||
);
|
||||
return Err(Error::ChainDown);
|
||||
}
|
||||
|
||||
'again: loop {
|
||||
// Randomly select proxies
|
||||
let selected_indices = self.select_random_proxies(proxies, max_chain);
|
||||
if selected_indices.is_empty() {
|
||||
return Err(Error::ChainEmpty);
|
||||
}
|
||||
|
||||
// Connect to first selected proxy
|
||||
let first_idx = selected_indices[0];
|
||||
let mut stream = match connect_to_proxy(&proxies[first_idx], self.config.tcp_connect_timeout) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
warn!("Failed to connect to random proxy {}: {}", first_idx, e);
|
||||
mark_down(&mut proxies[first_idx]);
|
||||
continue 'again;
|
||||
}
|
||||
};
|
||||
|
||||
stream.set_read_timeout(Some(self.config.tcp_read_timeout))?;
|
||||
stream.set_write_timeout(Some(self.config.tcp_read_timeout))?;
|
||||
|
||||
let mut current_idx = first_idx;
|
||||
|
||||
// Chain through remaining selected proxies
|
||||
for &next_idx in &selected_indices[1..] {
|
||||
let next_proxy = &proxies[next_idx];
|
||||
let next_target = TargetAddress::from_ip(next_proxy.ip);
|
||||
|
||||
if let Err(e) = tunnel_through_proxy(
|
||||
&mut stream,
|
||||
&proxies[current_idx],
|
||||
&next_target,
|
||||
next_proxy.port,
|
||||
self.config.tcp_read_timeout,
|
||||
) {
|
||||
warn!("Failed in random chain at proxy {}: {}", next_idx, e);
|
||||
mark_down(&mut proxies[next_idx]);
|
||||
continue 'again;
|
||||
}
|
||||
|
||||
current_idx = next_idx;
|
||||
}
|
||||
|
||||
// Connect to target
|
||||
if let Err(e) = tunnel_through_proxy(
|
||||
&mut stream,
|
||||
&proxies[current_idx],
|
||||
target,
|
||||
target_port,
|
||||
self.config.tcp_read_timeout,
|
||||
) {
|
||||
warn!("Failed to connect to target in random chain: {}", e);
|
||||
continue 'again;
|
||||
}
|
||||
|
||||
info!("Random chain established successfully");
|
||||
return Ok(stream);
|
||||
}
|
||||
}
|
||||
|
||||
/// Load balance chain - round robin selection
|
||||
fn load_balance_chain(
|
||||
&self,
|
||||
proxies: &mut [ProxyData],
|
||||
target: &TargetAddress,
|
||||
target_port: u16,
|
||||
) -> Result<std::net::TcpStream> {
|
||||
// For load balancing, just use one proxy at a time
|
||||
let alive_count = count_alive(proxies);
|
||||
if alive_count == 0 {
|
||||
return Err(Error::ChainEmpty);
|
||||
}
|
||||
|
||||
// Select a proxy using round robin
|
||||
let mut offset = 0;
|
||||
let selected_idx = match self.selector.select_index(proxies, &mut offset) {
|
||||
Some(idx) => idx,
|
||||
None => return Err(Error::ChainEmpty),
|
||||
};
|
||||
|
||||
let proxy = &proxies[selected_idx];
|
||||
debug!(
|
||||
"Load balance: selected proxy {}:{}",
|
||||
proxy.ip, proxy.port
|
||||
);
|
||||
|
||||
// Connect to proxy
|
||||
let mut stream = connect_to_proxy(proxy, self.config.tcp_connect_timeout)?;
|
||||
stream.set_read_timeout(Some(self.config.tcp_read_timeout))?;
|
||||
stream.set_write_timeout(Some(self.config.tcp_read_timeout))?;
|
||||
|
||||
// Connect to target
|
||||
tunnel_through_proxy(
|
||||
&mut stream,
|
||||
proxy,
|
||||
target,
|
||||
target_port,
|
||||
self.config.tcp_read_timeout,
|
||||
)?;
|
||||
|
||||
info!("Load balance connection established");
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
/// Failover chain - try proxies in order until one works
|
||||
fn failover_chain(
|
||||
&self,
|
||||
proxies: &mut [ProxyData],
|
||||
target: &TargetAddress,
|
||||
target_port: u16,
|
||||
) -> Result<std::net::TcpStream> {
|
||||
for i in 0..proxies.len() {
|
||||
if proxies[i].state != ProxyState::Play {
|
||||
continue;
|
||||
}
|
||||
|
||||
debug!("Failover: trying proxy {}", i);
|
||||
|
||||
match connect_to_proxy(&proxies[i], self.config.tcp_connect_timeout) {
|
||||
Ok(mut stream) => {
|
||||
stream.set_read_timeout(Some(self.config.tcp_read_timeout))?;
|
||||
stream.set_write_timeout(Some(self.config.tcp_read_timeout))?;
|
||||
|
||||
match tunnel_through_proxy(
|
||||
&mut stream,
|
||||
&proxies[i],
|
||||
target,
|
||||
target_port,
|
||||
self.config.tcp_read_timeout,
|
||||
) {
|
||||
Ok(()) => {
|
||||
info!("Failover: connected through proxy {}", i);
|
||||
return Ok(stream);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failover: proxy {} failed: {}", i, e);
|
||||
mark_down(&mut proxies[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failover: failed to connect to proxy {}: {}", i, e);
|
||||
mark_down(&mut proxies[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error!("Failover: all proxies exhausted");
|
||||
Err(Error::ChainDown)
|
||||
}
|
||||
|
||||
/// Find the next alive proxy
|
||||
fn find_alive_proxy(&self, proxies: &[ProxyData], offset: &mut usize) -> Option<usize> {
|
||||
while *offset < proxies.len() {
|
||||
let idx = *offset;
|
||||
*offset += 1;
|
||||
if proxies[idx].state == ProxyState::Play {
|
||||
return Some(idx);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Select random proxies
|
||||
fn select_random_proxies(&self, proxies: &[ProxyData], count: usize) -> Vec<usize> {
|
||||
use rand::seq::SliceRandom;
|
||||
|
||||
let available: Vec<usize> = proxies
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, p)| p.state == ProxyState::Play)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut selected: Vec<usize> = available;
|
||||
selected.shuffle(&mut rng);
|
||||
selected.truncate(count);
|
||||
selected
|
||||
}
|
||||
|
||||
/// Reset all proxy states
|
||||
pub fn reset_states(&self) {
|
||||
let mut states = self.proxy_states.lock();
|
||||
for proxy in states.iter_mut() {
|
||||
proxy.state = ProxyState::Play;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_config() -> Config {
|
||||
let mut config = Config::default();
|
||||
config.proxies = vec![
|
||||
ProxyData::new(Ipv4Addr::new(192, 168, 1, 1), 1080, crate::config::ProxyType::Socks5),
|
||||
];
|
||||
config
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chain_manager_creation() {
|
||||
let config = create_test_config();
|
||||
let manager = ChainManager::new(config);
|
||||
assert!(manager.config.has_proxies());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chain_error_conversion() {
|
||||
let err: Error = ChainError::ChainEmpty.into();
|
||||
assert!(matches!(err, Error::ChainEmpty));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//! Proxy chain management module
|
||||
|
||||
mod manager;
|
||||
mod selector;
|
||||
|
||||
pub use manager::*;
|
||||
pub use selector::*;
|
||||
@@ -0,0 +1,207 @@
|
||||
//! Proxy chain selector strategies
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::Rng;
|
||||
|
||||
use crate::config::{ChainType, ProxyData, ProxyState};
|
||||
|
||||
/// Selection strategy for proxies
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SelectStrategy {
|
||||
/// First available (FIFO)
|
||||
Fifo,
|
||||
/// Random selection
|
||||
Random,
|
||||
/// Round robin
|
||||
RoundRobin,
|
||||
/// Least connections
|
||||
LeastConnections,
|
||||
}
|
||||
|
||||
/// Proxy selector
|
||||
pub struct ProxySelector {
|
||||
strategy: SelectStrategy,
|
||||
round_robin_counter: AtomicUsize,
|
||||
}
|
||||
|
||||
impl ProxySelector {
|
||||
/// Create a new proxy selector
|
||||
pub fn new(strategy: SelectStrategy) -> Self {
|
||||
Self {
|
||||
strategy,
|
||||
round_robin_counter: AtomicUsize::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create selector from chain type
|
||||
pub fn from_chain_type(chain_type: ChainType) -> Self {
|
||||
let strategy = match chain_type {
|
||||
ChainType::Strict | ChainType::Dynamic => SelectStrategy::Fifo,
|
||||
ChainType::Random => SelectStrategy::Random,
|
||||
ChainType::LoadBalance => SelectStrategy::RoundRobin,
|
||||
ChainType::Failover => SelectStrategy::Fifo,
|
||||
};
|
||||
Self::new(strategy)
|
||||
}
|
||||
|
||||
/// Select a proxy from the list by index
|
||||
pub fn select_index(&self, proxies: &[ProxyData], offset: &mut usize) -> Option<usize> {
|
||||
match self.strategy {
|
||||
SelectStrategy::Fifo => self.select_fifo_index(proxies, offset),
|
||||
SelectStrategy::Random => self.select_random_index(proxies),
|
||||
SelectStrategy::RoundRobin => self.select_round_robin_index(proxies),
|
||||
SelectStrategy::LeastConnections => self.select_fifo_index(proxies, offset),
|
||||
}
|
||||
}
|
||||
|
||||
/// FIFO selection - returns index
|
||||
fn select_fifo_index(&self, proxies: &[ProxyData], offset: &mut usize) -> Option<usize> {
|
||||
while *offset < proxies.len() {
|
||||
let idx = *offset;
|
||||
*offset += 1;
|
||||
if proxies[idx].state == ProxyState::Play {
|
||||
return Some(idx);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Random selection - returns index
|
||||
fn select_random_index(&self, proxies: &[ProxyData]) -> Option<usize> {
|
||||
// Get indices of available proxies
|
||||
let available: Vec<usize> = proxies
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, p)| p.state == ProxyState::Play)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
if available.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Use thread RNG
|
||||
let mut rng = rand::thread_rng();
|
||||
available.choose(&mut rng).copied()
|
||||
}
|
||||
|
||||
/// Round robin selection - returns index
|
||||
fn select_round_robin_index(&self, proxies: &[ProxyData]) -> Option<usize> {
|
||||
let len = proxies.len();
|
||||
if len == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Try to find an available proxy starting from current counter
|
||||
let start = self.round_robin_counter.load(Ordering::Relaxed) % len;
|
||||
for i in 0..len {
|
||||
let idx = (start + i) % len;
|
||||
if proxies[idx].state == ProxyState::Play {
|
||||
self.round_robin_counter.fetch_add(1, Ordering::Relaxed);
|
||||
return Some(idx);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProxySelector {
|
||||
fn default() -> Self {
|
||||
Self::new(SelectStrategy::Fifo)
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the number of alive (available) proxies
|
||||
pub fn count_alive(proxies: &[ProxyData]) -> usize {
|
||||
proxies
|
||||
.iter()
|
||||
.filter(|p| p.state == ProxyState::Play)
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Reset all proxies to Play state
|
||||
pub fn release_all(proxies: &mut [ProxyData]) {
|
||||
for proxy in proxies.iter_mut() {
|
||||
proxy.state = ProxyState::Play;
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark a proxy as down
|
||||
pub fn mark_down(proxy: &mut ProxyData) {
|
||||
proxy.state = ProxyState::Down;
|
||||
}
|
||||
|
||||
/// Mark a proxy as blocked
|
||||
pub fn mark_blocked(proxy: &mut ProxyData) {
|
||||
proxy.state = ProxyState::Blocked;
|
||||
}
|
||||
|
||||
/// Mark a proxy as busy
|
||||
pub fn mark_busy(proxy: &mut ProxyData) {
|
||||
proxy.state = ProxyState::Busy;
|
||||
}
|
||||
|
||||
/// Get a random integer
|
||||
pub fn get_random_int(range: usize) -> usize {
|
||||
if range == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
rng.gen_range(0..range)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
fn create_test_proxies() -> Vec<ProxyData> {
|
||||
vec![
|
||||
ProxyData::new(Ipv4Addr::new(192, 168, 1, 1), 1080, crate::config::ProxyType::Socks5),
|
||||
ProxyData::new(Ipv4Addr::new(192, 168, 1, 2), 1080, crate::config::ProxyType::Socks5),
|
||||
ProxyData::new(Ipv4Addr::new(192, 168, 1, 3), 1080, crate::config::ProxyType::Socks5),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fifo_selection() {
|
||||
let proxies = create_test_proxies();
|
||||
let selector = ProxySelector::new(SelectStrategy::Fifo);
|
||||
let mut offset = 0;
|
||||
|
||||
let first = selector.select_index(&proxies, &mut offset);
|
||||
assert_eq!(first, Some(0));
|
||||
|
||||
let second = selector.select_index(&proxies, &mut offset);
|
||||
assert_eq!(second, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_count_alive() {
|
||||
let mut proxies = create_test_proxies();
|
||||
assert_eq!(count_alive(&proxies), 3);
|
||||
|
||||
proxies[0].state = ProxyState::Down;
|
||||
assert_eq!(count_alive(&proxies), 2);
|
||||
|
||||
proxies[1].state = ProxyState::Blocked;
|
||||
assert_eq!(count_alive(&proxies), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_release_all() {
|
||||
let mut proxies = create_test_proxies();
|
||||
proxies[0].state = ProxyState::Down;
|
||||
proxies[1].state = ProxyState::Blocked;
|
||||
|
||||
release_all(&mut proxies);
|
||||
|
||||
for proxy in &proxies {
|
||||
assert_eq!(proxy.state, ProxyState::Play);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Configuration module
|
||||
//!
|
||||
//! This module provides configuration parsing and types for proxychains.
|
||||
|
||||
mod parser;
|
||||
mod types;
|
||||
|
||||
pub use parser::*;
|
||||
pub use types::*;
|
||||
@@ -0,0 +1,333 @@
|
||||
//! Configuration file parser
|
||||
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::config::types::*;
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// Environment variable names
|
||||
pub const ENV_CONF_FILE: &str = "PROXYCHAINS_CONF_FILE";
|
||||
pub const ENV_QUIET_MODE: &str = "PROXYCHAINS_QUIET_MODE";
|
||||
pub const ENV_SOCKS5_HOST: &str = "PROXYCHAINS_SOCKS5_HOST";
|
||||
pub const ENV_SOCKS5_PORT: &str = "PROXYCHAINS_SOCKS5_PORT";
|
||||
pub const ENV_DNS: &str = "PROXYCHAINS_DNS";
|
||||
|
||||
/// Configuration parser
|
||||
pub struct ConfigParser {
|
||||
config_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ConfigParser {
|
||||
/// Create a new configuration parser
|
||||
pub fn new() -> Self {
|
||||
Self { config_path: None }
|
||||
}
|
||||
|
||||
/// Set the configuration file path
|
||||
pub fn with_path(mut self, path: PathBuf) -> Self {
|
||||
self.config_path = Some(path);
|
||||
self
|
||||
}
|
||||
|
||||
/// Find the configuration file
|
||||
pub fn find_config_file(&self) -> Option<PathBuf> {
|
||||
// If path was explicitly set, use it
|
||||
if let Some(ref path) = self.config_path {
|
||||
if path.exists() {
|
||||
return Some(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Check environment variable
|
||||
if let Ok(path) = env::var(ENV_CONF_FILE) {
|
||||
let path = PathBuf::from(path);
|
||||
if path.exists() {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Check standard locations
|
||||
let search_paths = vec![
|
||||
PathBuf::from("./proxychains.conf"),
|
||||
dirs::home_dir()
|
||||
.map(|h| h.join(".proxychains/proxychains.conf"))
|
||||
.unwrap_or_default(),
|
||||
PathBuf::from("/usr/local/etc/proxychains.conf"),
|
||||
PathBuf::from("/etc/proxychains.conf"),
|
||||
];
|
||||
|
||||
for path in search_paths {
|
||||
if path.exists() {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Parse the configuration
|
||||
pub fn parse(&self) -> Result<Config> {
|
||||
// First check for simple SOCKS5 environment variables
|
||||
if let (Ok(host), Ok(port)) = (env::var(ENV_SOCKS5_HOST), env::var(ENV_SOCKS5_PORT)) {
|
||||
if let Ok(port) = port.parse::<u16>() {
|
||||
return self.parse_socks5_env(&host, port);
|
||||
}
|
||||
}
|
||||
|
||||
// Find and parse config file
|
||||
if let Some(path) = self.find_config_file() {
|
||||
self.parse_file(&path)
|
||||
} else {
|
||||
// Return default config with no proxies
|
||||
let mut config = Config::default();
|
||||
self.apply_env_overrides(&mut config)?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse SOCKS5 from environment variables
|
||||
fn parse_socks5_env(&self, host: &str, port: u16) -> Result<Config> {
|
||||
let ip: Ipv4Addr = host.parse().map_err(|_| {
|
||||
Error::Config(format!("Invalid SOCKS5 host: {}", host))
|
||||
})?;
|
||||
|
||||
let mut config = Config::default();
|
||||
config.proxy_dns = env::var(ENV_DNS).is_ok();
|
||||
config.quiet_mode = env::var(ENV_QUIET_MODE).is_ok();
|
||||
|
||||
config.proxies.push(ProxyData::new(ip, port, ProxyType::Socks5));
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Parse configuration from file
|
||||
pub fn parse_file<P: AsRef<Path>>(&self, path: P) -> Result<Config> {
|
||||
let path = path.as_ref();
|
||||
let file = fs::File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let mut config = Config::default();
|
||||
let mut in_proxy_list = false;
|
||||
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
let trimmed = line.trim();
|
||||
|
||||
// Skip empty lines and comments
|
||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for section markers
|
||||
if trimmed.starts_with('[') {
|
||||
in_proxy_list = trimmed.to_lowercase().contains("proxylist");
|
||||
continue;
|
||||
}
|
||||
|
||||
if in_proxy_list {
|
||||
self.parse_proxy_line(trimmed, &mut config)?;
|
||||
} else {
|
||||
self.parse_config_line(trimmed, &mut config)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply environment variable overrides
|
||||
self.apply_env_overrides(&mut config)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Parse a configuration line
|
||||
fn parse_config_line(&self, line: &str, config: &mut Config) -> Result<()> {
|
||||
let parts: Vec<&str> = line.splitn(2, char::is_whitespace).collect();
|
||||
if parts.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let key = parts[0].to_lowercase();
|
||||
let value = parts.get(1).map(|s| s.trim()).unwrap_or("");
|
||||
|
||||
match key.as_str() {
|
||||
"dynamic_chain" => config.chain_type = ChainType::Dynamic,
|
||||
"strict_chain" => config.chain_type = ChainType::Strict,
|
||||
"random_chain" => config.chain_type = ChainType::Random,
|
||||
"load_balance" => config.chain_type = ChainType::LoadBalance,
|
||||
"failover" => config.chain_type = ChainType::Failover,
|
||||
"chain_len" => {
|
||||
config.chain_len = value.parse().ok();
|
||||
}
|
||||
"quiet_mode" => config.quiet_mode = true,
|
||||
"proxy_dns" => config.proxy_dns = true,
|
||||
"remote_dns_subnet" => {
|
||||
if let Ok(subnet) = value.parse() {
|
||||
config.remote_dns_subnet = subnet;
|
||||
}
|
||||
}
|
||||
"tcp_read_time_out" => {
|
||||
if let Ok(timeout) = value.parse::<u64>() {
|
||||
config.tcp_read_timeout = Duration::from_millis(timeout);
|
||||
}
|
||||
}
|
||||
"tcp_connect_time_out" => {
|
||||
if let Ok(timeout) = value.parse::<u64>() {
|
||||
config.tcp_connect_timeout = Duration::from_millis(timeout);
|
||||
}
|
||||
}
|
||||
"localnet" => {
|
||||
self.parse_localnet(value, config)?;
|
||||
}
|
||||
"dnat" => {
|
||||
self.parse_dnat(value, config)?;
|
||||
}
|
||||
_ => {} // Ignore unknown options
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse a localnet line
|
||||
fn parse_localnet(&self, value: &str, config: &mut Config) -> Result<()> {
|
||||
// Format: address/mask or address mask
|
||||
let parts: Vec<&str> = value.split(|c| c == '/' || c == ' ').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(Error::Config(format!("Invalid localnet format: {}", value)));
|
||||
}
|
||||
|
||||
let address: Ipv4Addr = parts[0].parse().map_err(|_| {
|
||||
Error::Config(format!("Invalid localnet address: {}", parts[0]))
|
||||
})?;
|
||||
|
||||
let netmask: Ipv4Addr = parts[1].parse().map_err(|_| {
|
||||
Error::Config(format!("Invalid localnet netmask: {}", parts[1]))
|
||||
})?;
|
||||
|
||||
config.localnets.push(LocalNet::new(address, netmask));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse a DNAT line
|
||||
fn parse_dnat(&self, value: &str, config: &mut Config) -> Result<()> {
|
||||
// Format: orig_addr:orig_port new_addr:new_port
|
||||
let parts: Vec<&str> = value.split_whitespace().collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(Error::Config(format!("Invalid DNAT format: {}", value)));
|
||||
}
|
||||
|
||||
let parse_addr_port = |s: &str| -> Result<(Ipv4Addr, u16)> {
|
||||
let ap: Vec<&str> = s.split(':').collect();
|
||||
if ap.len() != 2 {
|
||||
return Err(Error::Config(format!("Invalid address:port: {}", s)));
|
||||
}
|
||||
let addr: Ipv4Addr = ap[0].parse().map_err(|_| {
|
||||
Error::Config(format!("Invalid address: {}", ap[0]))
|
||||
})?;
|
||||
let port: u16 = ap[1].parse().map_err(|_| {
|
||||
Error::Config(format!("Invalid port: {}", ap[1]))
|
||||
})?;
|
||||
Ok((addr, port))
|
||||
};
|
||||
|
||||
let (orig_addr, orig_port) = parse_addr_port(parts[0])?;
|
||||
let (new_addr, new_port) = parse_addr_port(parts[1])?;
|
||||
|
||||
config.dnats.push(DnatRule::new(orig_addr, orig_port, new_addr, new_port));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse a proxy line
|
||||
fn parse_proxy_line(&self, line: &str, config: &mut Config) -> Result<()> {
|
||||
// Format: type host port [user pass]
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() < 3 {
|
||||
return Err(Error::Config(format!("Invalid proxy line: {}", line)));
|
||||
}
|
||||
|
||||
let proxy_type: ProxyType = parts[0].parse().map_err(|e| {
|
||||
Error::Config(e)
|
||||
})?;
|
||||
|
||||
let ip: Ipv4Addr = parts[1].parse().map_err(|_| {
|
||||
Error::Config(format!("Invalid proxy host: {}", parts[1]))
|
||||
})?;
|
||||
|
||||
let port: u16 = parts[2].parse().map_err(|_| {
|
||||
Error::Config(format!("Invalid proxy port: {}", parts[2]))
|
||||
})?;
|
||||
|
||||
let mut proxy = ProxyData::new(ip, port, proxy_type);
|
||||
|
||||
// Parse optional credentials
|
||||
if parts.len() >= 5 {
|
||||
proxy.user = Some(parts[3].to_string());
|
||||
proxy.pass = Some(parts[4].to_string());
|
||||
}
|
||||
|
||||
config.proxies.push(proxy);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply environment variable overrides
|
||||
fn apply_env_overrides(&self, config: &mut Config) -> Result<()> {
|
||||
if env::var(ENV_QUIET_MODE).is_ok() {
|
||||
config.quiet_mode = true;
|
||||
}
|
||||
|
||||
if env::var(ENV_DNS).is_ok() {
|
||||
config.proxy_dns = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ConfigParser {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple module to get home directory
|
||||
mod dirs {
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn home_dir() -> Option<PathBuf> {
|
||||
env::var_os("HOME")
|
||||
.or_else(|| env::var_os("USERPROFILE"))
|
||||
.map(PathBuf::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn test_parse_proxy_type() {
|
||||
assert_eq!(ProxyType::from_str("socks5").unwrap(), ProxyType::Socks5);
|
||||
assert_eq!(ProxyType::from_str("socks5h").unwrap(), ProxyType::Socks5);
|
||||
assert_eq!(ProxyType::from_str("SOCKS5").unwrap(), ProxyType::Socks5);
|
||||
assert_eq!(ProxyType::from_str("socks4").unwrap(), ProxyType::Socks4);
|
||||
assert_eq!(ProxyType::from_str("socks4a").unwrap(), ProxyType::Socks4);
|
||||
assert_eq!(ProxyType::from_str("http").unwrap(), ProxyType::Http);
|
||||
assert!(ProxyType::from_str("invalid").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_localnet_contains() {
|
||||
let localnet = LocalNet::new(
|
||||
Ipv4Addr::new(192, 168, 0, 0),
|
||||
Ipv4Addr::new(255, 255, 0, 0),
|
||||
);
|
||||
|
||||
assert!(localnet.contains(&Ipv4Addr::new(192, 168, 1, 1)));
|
||||
assert!(localnet.contains(&Ipv4Addr::new(192, 168, 255, 255)));
|
||||
assert!(!localnet.contains(&Ipv4Addr::new(10, 0, 0, 1)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
//! Configuration types for proxychains
|
||||
|
||||
use std::net::{Ipv4Addr, SocketAddrV4};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Proxy protocol type
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum ProxyType {
|
||||
#[default]
|
||||
Socks5,
|
||||
Socks4,
|
||||
Http,
|
||||
Raw,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ProxyType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ProxyType::Socks5 => write!(f, "socks5"),
|
||||
ProxyType::Socks4 => write!(f, "socks4"),
|
||||
ProxyType::Http => write!(f, "http"),
|
||||
ProxyType::Raw => write!(f, "raw"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for ProxyType {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"socks5" | "socks5h" => Ok(ProxyType::Socks5),
|
||||
"socks4" | "socks4a" => Ok(ProxyType::Socks4),
|
||||
"http" | "connect" => Ok(ProxyType::Http),
|
||||
"raw" => Ok(ProxyType::Raw),
|
||||
_ => Err(format!("Unknown proxy type: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Proxy state
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum ProxyState {
|
||||
#[default]
|
||||
Play,
|
||||
Down,
|
||||
Blocked,
|
||||
Busy,
|
||||
}
|
||||
|
||||
/// Chain type
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum ChainType {
|
||||
#[default]
|
||||
Dynamic,
|
||||
Strict,
|
||||
Random,
|
||||
LoadBalance,
|
||||
Failover,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ChainType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ChainType::Dynamic => write!(f, "dynamic"),
|
||||
ChainType::Strict => write!(f, "strict"),
|
||||
ChainType::Random => write!(f, "random"),
|
||||
ChainType::LoadBalance => write!(f, "load_balance"),
|
||||
ChainType::Failover => write!(f, "failover"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Proxy data structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyData {
|
||||
/// Proxy IP address
|
||||
pub ip: Ipv4Addr,
|
||||
/// Proxy port
|
||||
pub port: u16,
|
||||
/// Proxy type
|
||||
pub proxy_type: ProxyType,
|
||||
/// Proxy state
|
||||
pub state: ProxyState,
|
||||
/// Username for authentication
|
||||
pub user: Option<String>,
|
||||
/// Password for authentication
|
||||
pub pass: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ProxyData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ip: Ipv4Addr::new(127, 0, 0, 1),
|
||||
port: 1080,
|
||||
proxy_type: ProxyType::default(),
|
||||
state: ProxyState::default(),
|
||||
user: None,
|
||||
pass: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProxyData {
|
||||
/// Create a new proxy data
|
||||
pub fn new(ip: Ipv4Addr, port: u16, proxy_type: ProxyType) -> Self {
|
||||
Self {
|
||||
ip,
|
||||
port,
|
||||
proxy_type,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Set authentication credentials
|
||||
pub fn with_auth(mut self, user: String, pass: String) -> Self {
|
||||
self.user = Some(user);
|
||||
self.pass = Some(pass);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get socket address
|
||||
pub fn socket_addr(&self) -> SocketAddrV4 {
|
||||
SocketAddrV4::new(self.ip, self.port)
|
||||
}
|
||||
}
|
||||
|
||||
/// Local network for bypass
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LocalNet {
|
||||
pub address: Ipv4Addr,
|
||||
pub netmask: Ipv4Addr,
|
||||
}
|
||||
|
||||
impl LocalNet {
|
||||
pub fn new(address: Ipv4Addr, netmask: Ipv4Addr) -> Self {
|
||||
Self { address, netmask }
|
||||
}
|
||||
|
||||
/// Check if an IP is in this local network
|
||||
pub fn contains(&self, ip: &Ipv4Addr) -> bool {
|
||||
let addr_bits = u32::from(self.address);
|
||||
let mask_bits = u32::from(self.netmask);
|
||||
let ip_bits = u32::from(*ip);
|
||||
(addr_bits & mask_bits) == (ip_bits & mask_bits)
|
||||
}
|
||||
}
|
||||
|
||||
/// DNAT rule
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DnatRule {
|
||||
pub original: Ipv4Addr,
|
||||
pub original_port: u16,
|
||||
pub new: Ipv4Addr,
|
||||
pub new_port: u16,
|
||||
}
|
||||
|
||||
impl DnatRule {
|
||||
pub fn new(original: Ipv4Addr, original_port: u16, new: Ipv4Addr, new_port: u16) -> Self {
|
||||
Self {
|
||||
original,
|
||||
original_port,
|
||||
new,
|
||||
new_port,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Main configuration structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
/// Chain type
|
||||
pub chain_type: ChainType,
|
||||
/// Maximum chain length for random mode
|
||||
pub chain_len: Option<usize>,
|
||||
/// Quiet mode - suppress output
|
||||
pub quiet_mode: bool,
|
||||
/// Proxy DNS through the chain
|
||||
pub proxy_dns: bool,
|
||||
/// Remote DNS subnet for fake IPs
|
||||
pub remote_dns_subnet: u8,
|
||||
/// TCP read timeout
|
||||
pub tcp_read_timeout: Duration,
|
||||
/// TCP connect timeout
|
||||
pub tcp_connect_timeout: Duration,
|
||||
/// Local networks to bypass
|
||||
pub localnets: Vec<LocalNet>,
|
||||
/// DNAT rules
|
||||
pub dnats: Vec<DnatRule>,
|
||||
/// Proxy list
|
||||
pub proxies: Vec<ProxyData>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
chain_type: ChainType::default(),
|
||||
chain_len: None,
|
||||
quiet_mode: false,
|
||||
proxy_dns: false,
|
||||
remote_dns_subnet: 224,
|
||||
tcp_read_timeout: Duration::from_millis(15000),
|
||||
tcp_connect_timeout: Duration::from_millis(8000),
|
||||
localnets: Vec::new(),
|
||||
dnats: Vec::new(),
|
||||
proxies: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Create a new configuration with default values
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Check if an IP should bypass the proxy
|
||||
pub fn should_bypass(&self, ip: &Ipv4Addr) -> bool {
|
||||
// Always bypass localhost
|
||||
if ip.is_loopback() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check localnets
|
||||
for localnet in &self.localnets {
|
||||
if localnet.contains(ip) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Apply DNAT rule if applicable
|
||||
pub fn apply_dnat(&self, ip: &Ipv4Addr, port: u16) -> (Ipv4Addr, u16) {
|
||||
for dnat in &self.dnats {
|
||||
if &dnat.original == ip && dnat.original_port == port {
|
||||
return (dnat.new, dnat.new_port);
|
||||
}
|
||||
}
|
||||
(*ip, port)
|
||||
}
|
||||
|
||||
/// Get number of proxies
|
||||
pub fn proxy_count(&self) -> usize {
|
||||
self.proxies.len()
|
||||
}
|
||||
|
||||
/// Check if configuration has any proxies
|
||||
pub fn has_proxies(&self) -> bool {
|
||||
!self.proxies.is_empty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
//! DNS cache for storing hostname to fake IP mappings
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use parking_lot::RwLock;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// DNS cache entry
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DnsEntry {
|
||||
/// The hostname
|
||||
pub hostname: String,
|
||||
/// The fake IP assigned
|
||||
pub fake_ip: Ipv4Addr,
|
||||
/// Hash for lookup
|
||||
pub hash: u32,
|
||||
}
|
||||
|
||||
/// DNS cache for internal IP mapping
|
||||
pub struct DnsCache {
|
||||
/// Map from fake IP to hostname
|
||||
ip_to_host: RwLock<HashMap<Ipv4Addr, DnsEntry>>,
|
||||
/// Map from hostname to fake IP
|
||||
host_to_ip: RwLock<HashMap<String, Ipv4Addr>>,
|
||||
/// Counter for generating fake IPs
|
||||
counter: RwLock<u32>,
|
||||
/// Remote DNS subnet (default: 224)
|
||||
subnet: u8,
|
||||
/// Maximum entries
|
||||
max_entries: usize,
|
||||
}
|
||||
|
||||
impl DnsCache {
|
||||
/// Create a new DNS cache
|
||||
pub fn new(subnet: u8) -> Self {
|
||||
Self {
|
||||
ip_to_host: RwLock::new(HashMap::new()),
|
||||
host_to_ip: RwLock::new(HashMap::new()),
|
||||
counter: RwLock::new(0),
|
||||
subnet,
|
||||
max_entries: 65535,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create with default subnet (224)
|
||||
pub fn default_subnet() -> Self {
|
||||
Self::new(224)
|
||||
}
|
||||
|
||||
/// Get or create a fake IP for a hostname
|
||||
pub fn get_or_create(&self, hostname: &str) -> Ipv4Addr {
|
||||
// Check if already cached
|
||||
{
|
||||
let host_to_ip = self.host_to_ip.read();
|
||||
if let Some(&ip) = host_to_ip.get(hostname) {
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new entry
|
||||
let mut counter = self.counter.write();
|
||||
*counter += 1;
|
||||
|
||||
// Check for overflow
|
||||
if *counter >= self.max_entries as u32 {
|
||||
// Reset counter and clear cache
|
||||
*counter = 1;
|
||||
self.ip_to_host.write().clear();
|
||||
self.host_to_ip.write().clear();
|
||||
}
|
||||
|
||||
let fake_ip = self.make_internal_ip(*counter);
|
||||
|
||||
// Store in cache
|
||||
let entry = DnsEntry {
|
||||
hostname: hostname.to_string(),
|
||||
fake_ip,
|
||||
hash: self.hash_hostname(hostname),
|
||||
};
|
||||
|
||||
self.ip_to_host.write().insert(fake_ip, entry);
|
||||
self.host_to_ip.write().insert(hostname.to_string(), fake_ip);
|
||||
|
||||
fake_ip
|
||||
}
|
||||
|
||||
/// Get hostname from fake IP
|
||||
pub fn get_hostname(&self, ip: &Ipv4Addr) -> Option<String> {
|
||||
let ip_to_host = self.ip_to_host.read();
|
||||
ip_to_host.get(ip).map(|e| e.hostname.clone())
|
||||
}
|
||||
|
||||
/// Check if an IP is a fake IP
|
||||
pub fn is_fake_ip(&self, ip: &Ipv4Addr) -> bool {
|
||||
let octets = ip.octets();
|
||||
octets[0] == self.subnet
|
||||
}
|
||||
|
||||
/// Get the subnet being used
|
||||
pub fn subnet(&self) -> u8 {
|
||||
self.subnet
|
||||
}
|
||||
|
||||
/// Generate an internal/fake IP address
|
||||
fn make_internal_ip(&self, index: u32) -> Ipv4Addr {
|
||||
// Format: subnet.(index>>16).(index>>8).(index)
|
||||
let index = index + 1; // Start at .0.0.1
|
||||
Ipv4Addr::new(
|
||||
self.subnet,
|
||||
((index >> 16) & 0xFF) as u8,
|
||||
((index >> 8) & 0xFF) as u8,
|
||||
(index & 0xFF) as u8,
|
||||
)
|
||||
}
|
||||
|
||||
/// Hash a hostname for quick lookup
|
||||
fn hash_hostname(&self, hostname: &str) -> u32 {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(hostname.as_bytes());
|
||||
let result = hasher.finalize();
|
||||
u32::from_le_bytes([result[0], result[1], result[2], result[3]])
|
||||
}
|
||||
|
||||
/// Clear the cache
|
||||
pub fn clear(&self) {
|
||||
self.ip_to_host.write().clear();
|
||||
self.host_to_ip.write().clear();
|
||||
*self.counter.write() = 0;
|
||||
}
|
||||
|
||||
/// Get cache size
|
||||
pub fn size(&self) -> usize {
|
||||
self.ip_to_host.read().len()
|
||||
}
|
||||
|
||||
/// Check if hostname is cached
|
||||
pub fn contains(&self, hostname: &str) -> bool {
|
||||
self.host_to_ip.read().contains_key(hostname)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DnsCache {
|
||||
fn default() -> Self {
|
||||
Self::default_subnet()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse /etc/hosts file
|
||||
pub fn parse_etc_hosts() -> HashMap<String, Ipv4Addr> {
|
||||
let mut hosts = HashMap::new();
|
||||
|
||||
if let Ok(content) = std::fs::read_to_string("/etc/hosts") {
|
||||
for line in content.lines() {
|
||||
let line = line.trim();
|
||||
// Skip comments and empty lines
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse line: IP hostname [aliases...]
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
if let Ok(ip) = parts[0].parse::<Ipv4Addr>() {
|
||||
for hostname in &parts[1..] {
|
||||
// Skip if hostname starts with #
|
||||
if hostname.starts_with('#') {
|
||||
break;
|
||||
}
|
||||
hosts.insert(hostname.to_lowercase(), ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hosts
|
||||
}
|
||||
|
||||
/// Check if hostname is in /etc/hosts
|
||||
pub fn in_etc_hosts(hostname: &str) -> Option<Ipv4Addr> {
|
||||
let hosts = parse_etc_hosts();
|
||||
hosts.get(&hostname.to_lowercase()).copied()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_dns_cache() {
|
||||
let cache = DnsCache::default();
|
||||
|
||||
let ip1 = cache.get_or_create("example.com");
|
||||
assert!(cache.is_fake_ip(&ip1));
|
||||
assert!(cache.contains("example.com"));
|
||||
|
||||
let hostname = cache.get_hostname(&ip1);
|
||||
assert_eq!(hostname, Some("example.com".to_string()));
|
||||
|
||||
// Same hostname should return same IP
|
||||
let ip2 = cache.get_or_create("example.com");
|
||||
assert_eq!(ip1, ip2);
|
||||
|
||||
// Different hostname should return different IP
|
||||
let ip3 = cache.get_or_create("test.com");
|
||||
assert_ne!(ip1, ip3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fake_ip_format() {
|
||||
let cache = DnsCache::new(224);
|
||||
|
||||
// First IP should be 224.0.0.1
|
||||
let ip1 = cache.get_or_create("test1.com");
|
||||
assert_eq!(ip1.octets()[0], 224);
|
||||
|
||||
// Check it's recognized as fake
|
||||
assert!(cache.is_fake_ip(&ip1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_cache() {
|
||||
let cache = DnsCache::default();
|
||||
|
||||
cache.get_or_create("example.com");
|
||||
assert!(cache.size() > 0);
|
||||
|
||||
cache.clear();
|
||||
assert_eq!(cache.size(), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//! DNS handling module
|
||||
//!
|
||||
//! This module provides DNS resolution capabilities including:
|
||||
//! - Fake IP mapping for remote DNS resolution
|
||||
//! - /etc/hosts lookup
|
||||
//! - System DNS resolution
|
||||
|
||||
mod cache;
|
||||
mod resolver;
|
||||
|
||||
pub use cache::*;
|
||||
pub use resolver::*;
|
||||
@@ -0,0 +1,174 @@
|
||||
//! DNS resolver for proxychains
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use crate::dns::cache::{in_etc_hosts, DnsCache};
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// Global DNS cache instance
|
||||
static DNS_CACHE: once_cell::sync::Lazy<DnsCache> =
|
||||
once_cell::sync::Lazy::new(|| DnsCache::default_subnet());
|
||||
|
||||
/// DNS resolver
|
||||
pub struct DnsResolver {
|
||||
cache: &'static DnsCache,
|
||||
proxy_dns: bool,
|
||||
}
|
||||
|
||||
impl DnsResolver {
|
||||
/// Create a new DNS resolver
|
||||
pub fn new(proxy_dns: bool, _subnet: u8) -> Self {
|
||||
Self {
|
||||
cache: &DNS_CACHE,
|
||||
proxy_dns,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create with default settings
|
||||
pub fn default_resolver() -> Self {
|
||||
Self::new(true, 224)
|
||||
}
|
||||
|
||||
/// Resolve a hostname
|
||||
///
|
||||
/// If proxy_dns is enabled and hostname is not in /etc/hosts,
|
||||
/// returns a fake IP that will be resolved through the proxy.
|
||||
/// Otherwise, uses the system resolver.
|
||||
pub fn resolve(&self, hostname: &str) -> Result<Ipv4Addr> {
|
||||
// First check /etc/hosts
|
||||
if let Some(ip) = in_etc_hosts(hostname) {
|
||||
return Ok(ip);
|
||||
}
|
||||
|
||||
// If proxy DNS is enabled, return fake IP
|
||||
if self.proxy_dns {
|
||||
let fake_ip = self.cache.get_or_create(hostname);
|
||||
return Ok(fake_ip);
|
||||
}
|
||||
|
||||
// Use system resolver
|
||||
self.system_resolve(hostname)
|
||||
}
|
||||
|
||||
/// Get hostname from fake IP
|
||||
pub fn get_hostname(&self, ip: &Ipv4Addr) -> Option<String> {
|
||||
if self.cache.is_fake_ip(ip) {
|
||||
self.cache.get_hostname(ip)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if IP is a fake IP (needs remote resolution)
|
||||
pub fn is_fake_ip(&self, ip: &Ipv4Addr) -> bool {
|
||||
self.cache.is_fake_ip(ip)
|
||||
}
|
||||
|
||||
/// System resolver using getaddrinfo
|
||||
fn system_resolve(&self, hostname: &str) -> Result<Ipv4Addr> {
|
||||
let c_hostname = CString::new(hostname)?;
|
||||
|
||||
let mut hints: libc::addrinfo = unsafe { std::mem::zeroed() };
|
||||
hints.ai_family = libc::AF_INET; // IPv4 only
|
||||
hints.ai_socktype = libc::SOCK_STREAM;
|
||||
|
||||
let mut result: *mut libc::addrinfo = std::ptr::null_mut();
|
||||
|
||||
let ret = unsafe {
|
||||
libc::getaddrinfo(c_hostname.as_ptr(), std::ptr::null(), &hints, &mut result)
|
||||
};
|
||||
|
||||
if ret != 0 {
|
||||
return Err(Error::Dns(format!(
|
||||
"Failed to resolve {}: {}",
|
||||
hostname, ret
|
||||
)));
|
||||
}
|
||||
|
||||
if result.is_null() {
|
||||
return Err(Error::Dns(format!("No addresses for {}", hostname)));
|
||||
}
|
||||
|
||||
// Get the first IPv4 address
|
||||
let addr = unsafe {
|
||||
let ai = *result;
|
||||
if ai.ai_family == libc::AF_INET {
|
||||
let addr_in = ai.ai_addr as *const libc::sockaddr_in;
|
||||
let ip_bytes = (*addr_in).sin_addr.s_addr.to_ne_bytes();
|
||||
Ipv4Addr::from(ip_bytes)
|
||||
} else {
|
||||
libc::freeaddrinfo(result);
|
||||
return Err(Error::Dns(format!("No IPv4 address for {}", hostname)));
|
||||
}
|
||||
};
|
||||
|
||||
unsafe {
|
||||
libc::freeaddrinfo(result);
|
||||
}
|
||||
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
/// Get reference to global DNS cache
|
||||
pub fn cache(&self) -> &'static DnsCache {
|
||||
self.cache
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an IP is a fake IP
|
||||
pub fn is_fake_ip(ip: &Ipv4Addr) -> bool {
|
||||
DNS_CACHE.is_fake_ip(ip)
|
||||
}
|
||||
|
||||
/// Get hostname from fake IP
|
||||
pub fn get_hostname_from_ip(ip: &Ipv4Addr) -> Option<String> {
|
||||
if DNS_CACHE.is_fake_ip(ip) {
|
||||
DNS_CACHE.get_hostname(ip)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve hostname to fake IP (for proxy DNS)
|
||||
pub fn resolve_to_fake_ip(hostname: &str, _subnet: u8) -> Ipv4Addr {
|
||||
DNS_CACHE.get_or_create(hostname)
|
||||
}
|
||||
|
||||
/// Parse hosts file and lookup hostname
|
||||
pub fn lookup_in_hosts(hostname: &str) -> Option<Ipv4Addr> {
|
||||
in_etc_hosts(hostname)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_dns_resolver() {
|
||||
let resolver = DnsResolver::new(true, 224);
|
||||
|
||||
// Test fake IP generation
|
||||
let ip = resolver.resolve("example.invalid").unwrap();
|
||||
assert!(resolver.is_fake_ip(&ip));
|
||||
|
||||
// Same hostname should return same IP
|
||||
let ip2 = resolver.resolve("example.invalid").unwrap();
|
||||
assert_eq!(ip, ip2);
|
||||
|
||||
// Should be able to get hostname back
|
||||
let hostname = resolver.get_hostname(&ip).unwrap();
|
||||
assert_eq!(hostname, "example.invalid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_fake_ip() {
|
||||
let resolver = DnsResolver::new(true, 224);
|
||||
|
||||
let ip = resolver.resolve("test.invalid").unwrap();
|
||||
assert!(is_fake_ip(&ip));
|
||||
|
||||
let real_ip = Ipv4Addr::new(8, 8, 8, 8);
|
||||
assert!(!is_fake_ip(&real_ip));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//! Error types for proxychains
|
||||
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Main error type for proxychains operations
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
#[error("Configuration error: {0}")]
|
||||
Config(String),
|
||||
|
||||
#[error("Proxy connection failed: {0}")]
|
||||
ProxyConnection(String),
|
||||
|
||||
#[error("Proxy authentication failed: {0}")]
|
||||
AuthFailed(String),
|
||||
|
||||
#[error("Chain error: {0}")]
|
||||
Chain(String),
|
||||
|
||||
#[error("DNS resolution failed: {0}")]
|
||||
Dns(String),
|
||||
|
||||
#[error("Socket error: {0}")]
|
||||
Socket(String),
|
||||
|
||||
#[error("Timeout error: {0}")]
|
||||
Timeout(String),
|
||||
|
||||
#[error("Protocol error: {0}")]
|
||||
Protocol(String),
|
||||
|
||||
#[error("Blocked by proxy")]
|
||||
Blocked,
|
||||
|
||||
#[error("Chain is empty")]
|
||||
ChainEmpty,
|
||||
|
||||
#[error("Memory allocation failed")]
|
||||
MemoryFail,
|
||||
|
||||
#[error("Socket error")]
|
||||
SocketError,
|
||||
|
||||
#[error("Chain is down")]
|
||||
ChainDown,
|
||||
|
||||
#[error("Invalid address")]
|
||||
InvalidAddress,
|
||||
|
||||
#[error("Nix error: {0}")]
|
||||
Nix(#[from] nix::Error),
|
||||
|
||||
#[error("UTF-8 error: {0}")]
|
||||
Utf8(#[from] std::string::FromUtf8Error),
|
||||
}
|
||||
|
||||
/// Result type alias for proxychains operations
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
impl From<std::ffi::NulError> for Error {
|
||||
fn from(e: std::ffi::NulError) -> Self {
|
||||
Error::Config(e.to_string())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
//! Hook implementations for network functions
|
||||
//!
|
||||
//! This module provides the actual hook implementations that intercept
|
||||
//! network calls and redirect them through the proxy chain.
|
||||
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::c_char;
|
||||
use std::os::unix::io::IntoRawFd;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use libc::{c_int, socklen_t};
|
||||
use tracing::{debug, error, info, trace};
|
||||
|
||||
use crate::chain::ChainManager;
|
||||
use crate::config::Config;
|
||||
use crate::dns::is_fake_ip;
|
||||
use crate::error::Result;
|
||||
use crate::net::{get_ip_from_sockaddr, get_port_from_sockaddr};
|
||||
|
||||
use super::interpose::{
|
||||
init_original_functions, original_connect, original_getaddrinfo, original_gethostbyname,
|
||||
original_getnameinfo,
|
||||
};
|
||||
|
||||
/// Global state for the hook library
|
||||
pub struct HookState {
|
||||
pub config: Config,
|
||||
pub chain_manager: ChainManager,
|
||||
pub dns_resolver: crate::dns::DnsResolver,
|
||||
pub initialized: bool,
|
||||
}
|
||||
|
||||
impl HookState {
|
||||
pub fn new(config: Config) -> Self {
|
||||
let chain_manager = ChainManager::new(config.clone());
|
||||
let dns_resolver = crate::dns::DnsResolver::new(config.proxy_dns, config.remote_dns_subnet);
|
||||
Self {
|
||||
config,
|
||||
chain_manager,
|
||||
dns_resolver,
|
||||
initialized: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Global hook state
|
||||
static HOOK_STATE: OnceLock<HookState> = OnceLock::new();
|
||||
|
||||
/// Initialize the hook library
|
||||
pub fn init_hooks(config: Config) -> Result<()> {
|
||||
// Initialize original functions first
|
||||
init_original_functions()?;
|
||||
|
||||
// Create hook state
|
||||
let state = HookState::new(config);
|
||||
|
||||
// Store globally
|
||||
if HOOK_STATE.set(state).is_err() {
|
||||
error!("Hook state already initialized");
|
||||
}
|
||||
|
||||
info!("Proxychains hooks initialized");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if hooks are initialized
|
||||
pub fn is_initialized() -> bool {
|
||||
HOOK_STATE.get().map_or(false, |s| s.initialized)
|
||||
}
|
||||
|
||||
/// Get the hook state
|
||||
fn get_hook_state() -> Option<HookState> {
|
||||
HOOK_STATE.get().map(|s| HookState {
|
||||
config: s.config.clone(),
|
||||
chain_manager: ChainManager::new(s.config.clone()),
|
||||
dns_resolver: crate::dns::DnsResolver::new(s.config.proxy_dns, s.config.remote_dns_subnet),
|
||||
initialized: s.initialized,
|
||||
})
|
||||
}
|
||||
|
||||
/// Hook for connect() system call
|
||||
///
|
||||
/// This intercepts TCP connections and redirects them through the proxy chain.
|
||||
///
|
||||
/// # Safety
|
||||
/// This function makes unsafe FFI calls
|
||||
pub unsafe fn hook_connect(
|
||||
sock: c_int,
|
||||
addr: *const libc::sockaddr,
|
||||
len: socklen_t,
|
||||
) -> c_int {
|
||||
trace!("hook_connect called: sock={}", sock);
|
||||
|
||||
// Check if initialized
|
||||
let state = match get_hook_state() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
return original_connect(sock, addr, len);
|
||||
}
|
||||
};
|
||||
|
||||
// Get target address
|
||||
let target_ip = match get_ip_from_sockaddr(addr) {
|
||||
Some(ip) => ip,
|
||||
None => return original_connect(sock, addr, len),
|
||||
};
|
||||
|
||||
let target_port = get_port_from_sockaddr(addr);
|
||||
|
||||
// Check if we should bypass this connection
|
||||
if state.config.should_bypass(&target_ip) {
|
||||
debug!("Bypassing proxy for local address: {}:{}", target_ip, target_port);
|
||||
return original_connect(sock, addr, len);
|
||||
}
|
||||
|
||||
// Apply DNAT if configured
|
||||
let (final_ip, final_port) = state.config.apply_dnat(&target_ip, target_port);
|
||||
|
||||
// Check if this is a fake IP (needs remote DNS resolution)
|
||||
let target_domain = if is_fake_ip(&final_ip) {
|
||||
state.dns_resolver.get_hostname(&final_ip)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Check if proxy DNS is enabled and we have a domain
|
||||
if state.config.proxy_dns && target_domain.is_some() {
|
||||
debug!(
|
||||
"Connecting to {}:{} ({}) through proxy chain",
|
||||
final_ip, final_port,
|
||||
target_domain.as_ref().unwrap()
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
"Connecting to {}:{} through proxy chain",
|
||||
final_ip, final_port
|
||||
);
|
||||
}
|
||||
|
||||
// Close the original socket (we'll create a new one through the proxy)
|
||||
libc::close(sock);
|
||||
|
||||
// Connect through proxy chain
|
||||
match state.chain_manager.connect_proxy_chain(
|
||||
final_ip,
|
||||
final_port,
|
||||
target_domain.as_deref(),
|
||||
) {
|
||||
Ok(proxy_stream) => {
|
||||
// Get the file descriptor from the proxy stream
|
||||
let proxy_fd = proxy_stream.into_raw_fd();
|
||||
|
||||
// Use dup2 to make it the same fd as the original socket
|
||||
let result = libc::dup2(proxy_fd, sock);
|
||||
libc::close(proxy_fd);
|
||||
|
||||
if result < 0 {
|
||||
error!("Failed to duplicate socket fd");
|
||||
return -1;
|
||||
}
|
||||
|
||||
info!("Proxy connection established");
|
||||
0
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to establish proxy chain: {}", e);
|
||||
// Set errno using nix
|
||||
let _ = nix::errno::Errno::last_raw();
|
||||
unsafe {
|
||||
*libc::__error() = libc::ECONNREFUSED;
|
||||
}
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hook for getaddrinfo() system call
|
||||
///
|
||||
/// When proxy_dns is enabled, this returns fake IPs that will be
|
||||
/// resolved through the proxy.
|
||||
///
|
||||
/// # Safety
|
||||
/// This function makes unsafe FFI calls
|
||||
pub unsafe fn hook_getaddrinfo(
|
||||
node: *const c_char,
|
||||
service: *const c_char,
|
||||
hints: *const libc::addrinfo,
|
||||
res: *mut *mut libc::addrinfo,
|
||||
) -> c_int {
|
||||
trace!("hook_getaddrinfo called");
|
||||
|
||||
// Check if initialized and proxy_dns is enabled
|
||||
let state = match get_hook_state() {
|
||||
Some(s) if s.config.proxy_dns => s,
|
||||
_ => return original_getaddrinfo(node, service, hints, res),
|
||||
};
|
||||
|
||||
// If no node specified, use original
|
||||
if node.is_null() {
|
||||
return original_getaddrinfo(node, service, hints, res);
|
||||
}
|
||||
|
||||
// Get the hostname
|
||||
let hostname = match CStr::from_ptr(node).to_str() {
|
||||
Ok(s) => s.to_string(),
|
||||
Err(_) => return original_getaddrinfo(node, service, hints, res),
|
||||
};
|
||||
|
||||
debug!("getaddrinfo hook for: {}", hostname);
|
||||
|
||||
// Check /etc/hosts first
|
||||
if let Some(_ip) = crate::dns::lookup_in_hosts(&hostname) {
|
||||
debug!("Found {} in /etc/hosts", hostname);
|
||||
// Use original function for hosts entries
|
||||
return original_getaddrinfo(node, service, hints, res);
|
||||
}
|
||||
|
||||
// Generate fake IP
|
||||
let _fake_ip = state.dns_resolver.resolve(&hostname).unwrap();
|
||||
|
||||
debug!("Assigned fake IP {} for {}", _fake_ip, hostname);
|
||||
|
||||
// For now, use original getaddrinfo
|
||||
// A full implementation would return fake IPs
|
||||
original_getaddrinfo(node, service, hints, res)
|
||||
}
|
||||
|
||||
/// Hook for gethostbyname() system call
|
||||
///
|
||||
/// # Safety
|
||||
/// This function makes unsafe FFI calls
|
||||
pub unsafe fn hook_gethostbyname(name: *const c_char) -> *mut libc::hostent {
|
||||
trace!("hook_gethostbyname called");
|
||||
|
||||
// Check if initialized and proxy_dns is enabled
|
||||
let state = match get_hook_state() {
|
||||
Some(s) if s.config.proxy_dns => s,
|
||||
_ => return original_gethostbyname(name),
|
||||
};
|
||||
|
||||
if name.is_null() {
|
||||
return original_gethostbyname(name);
|
||||
}
|
||||
|
||||
// Get the hostname
|
||||
let hostname = match CStr::from_ptr(name).to_str() {
|
||||
Ok(s) => s.to_string(),
|
||||
Err(_) => return original_gethostbyname(name),
|
||||
};
|
||||
|
||||
debug!("gethostbyname hook for: {}", hostname);
|
||||
|
||||
// Check /etc/hosts first
|
||||
if let Some(_ip) = crate::dns::lookup_in_hosts(&hostname) {
|
||||
debug!("Found {} in /etc/hosts", hostname);
|
||||
return original_gethostbyname(name);
|
||||
}
|
||||
|
||||
// Generate fake IP
|
||||
let _fake_ip = state.dns_resolver.resolve(&hostname).unwrap();
|
||||
|
||||
debug!("Assigned fake IP {} for {}", _fake_ip, hostname);
|
||||
|
||||
// This is a simplified implementation
|
||||
// A full implementation would need to manage a static buffer
|
||||
// For now, fall back to original
|
||||
original_gethostbyname(name)
|
||||
}
|
||||
|
||||
/// Hook for freeaddrinfo() system call
|
||||
///
|
||||
/// # Safety
|
||||
/// This function makes unsafe FFI calls
|
||||
pub unsafe fn hook_freeaddrinfo(res: *mut libc::addrinfo) {
|
||||
trace!("hook_freeaddrinfo called");
|
||||
|
||||
if res.is_null() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Free our allocated memory
|
||||
let ai = Box::from_raw(res);
|
||||
|
||||
// Free ai_addr
|
||||
if !ai.ai_addr.is_null() {
|
||||
let _ = Box::from_raw(ai.ai_addr as *mut libc::sockaddr_in);
|
||||
}
|
||||
|
||||
// Free ai_canonname
|
||||
if !ai.ai_canonname.is_null() {
|
||||
let _ = std::ffi::CString::from_raw(ai.ai_canonname);
|
||||
}
|
||||
}
|
||||
|
||||
/// Hook for getnameinfo() system call
|
||||
///
|
||||
/// # Safety
|
||||
/// This function makes unsafe FFI calls
|
||||
pub unsafe fn hook_getnameinfo(
|
||||
sa: *const libc::sockaddr,
|
||||
salen: libc::socklen_t,
|
||||
host: *mut c_char,
|
||||
hostlen: libc::socklen_t,
|
||||
serv: *mut c_char,
|
||||
servlen: libc::socklen_t,
|
||||
flags: libc::c_int,
|
||||
) -> libc::c_int {
|
||||
trace!("hook_getnameinfo called");
|
||||
|
||||
// For now, just pass through to original
|
||||
// A full implementation would reverse lookup fake IPs
|
||||
original_getnameinfo(sa, salen, host, hostlen, serv, servlen, flags)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hook_state_creation() {
|
||||
let config = Config::default();
|
||||
let state = HookState::new(config);
|
||||
assert!(state.initialized);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
//! LD_PRELOAD interposition utilities
|
||||
//!
|
||||
//! This module provides utilities for loading original libc symbols
|
||||
//! and managing the hook mechanism.
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::os::raw::c_char;
|
||||
use std::ptr;
|
||||
|
||||
use libc::{c_int, c_void, dlsym, RTLD_NEXT};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// Load a symbol from the next library in the loading chain
|
||||
///
|
||||
/// This uses `dlsym(RTLD_NEXT, ...)` to get the original function
|
||||
/// before our LD_PRELOAD library intercepted it.
|
||||
pub fn load_symbol<T>(name: &str) -> Result<T> {
|
||||
let cname = CString::new(name).map_err(|_| {
|
||||
Error::Config(format!("Invalid symbol name: {}", name))
|
||||
})?;
|
||||
|
||||
let sym = unsafe { dlsym(RTLD_NEXT, cname.as_ptr()) };
|
||||
|
||||
if sym.is_null() {
|
||||
return Err(Error::Config(format!(
|
||||
"Failed to load symbol: {}",
|
||||
name
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(unsafe { std::mem::transmute_copy::<*mut c_void, T>(&sym) })
|
||||
}
|
||||
|
||||
/// Store for original function pointers
|
||||
pub struct OriginalFunctions {
|
||||
pub connect: Option<unsafe extern "C" fn(c_int, *const libc::sockaddr, libc::socklen_t) -> c_int>,
|
||||
pub getaddrinfo: Option<unsafe extern "C" fn(*const c_char, *const c_char, *const libc::addrinfo, *mut *mut libc::addrinfo) -> c_int>,
|
||||
pub freeaddrinfo: Option<unsafe extern "C" fn(*mut libc::addrinfo)>,
|
||||
pub gethostbyname: Option<unsafe extern "C" fn(*const c_char) -> *mut libc::hostent>,
|
||||
pub getnameinfo: Option<unsafe extern "C" fn(*const libc::sockaddr, libc::socklen_t, *mut c_char, libc::socklen_t, *mut c_char, libc::socklen_t, c_int) -> c_int>,
|
||||
pub gethostbyaddr: Option<unsafe extern "C" fn(*const c_void, libc::socklen_t, c_int) -> *mut libc::hostent>,
|
||||
}
|
||||
|
||||
impl OriginalFunctions {
|
||||
/// Create a new OriginalFunctions struct
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
connect: None,
|
||||
getaddrinfo: None,
|
||||
freeaddrinfo: None,
|
||||
gethostbyname: None,
|
||||
getnameinfo: None,
|
||||
gethostbyaddr: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Load all original functions
|
||||
pub fn load_all(&mut self) -> Result<()> {
|
||||
self.connect = Some(load_symbol("connect")?);
|
||||
self.getaddrinfo = Some(load_symbol("getaddrinfo")?);
|
||||
self.freeaddrinfo = Some(load_symbol("freeaddrinfo")?);
|
||||
self.gethostbyname = Some(load_symbol("gethostbyname")?);
|
||||
self.getnameinfo = Some(load_symbol("getnameinfo")?);
|
||||
self.gethostbyaddr = Some(load_symbol("gethostbyaddr")?);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OriginalFunctions {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Global storage for original function pointers
|
||||
static mut ORIGINAL_FUNCS: Option<OriginalFunctions> = None;
|
||||
|
||||
/// Initialize original functions
|
||||
///
|
||||
/// This should be called once during library initialization
|
||||
pub fn init_original_functions() -> Result<()> {
|
||||
unsafe {
|
||||
if ORIGINAL_FUNCS.is_none() {
|
||||
let mut funcs = OriginalFunctions::new();
|
||||
funcs.load_all()?;
|
||||
ORIGINAL_FUNCS = Some(funcs);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the original functions
|
||||
///
|
||||
/// # Safety
|
||||
/// This function should only be called after initialization
|
||||
pub unsafe fn get_original_functions() -> &'static OriginalFunctions {
|
||||
ORIGINAL_FUNCS.as_ref().expect("Original functions not initialized")
|
||||
}
|
||||
|
||||
/// Call the original connect function
|
||||
///
|
||||
/// # Safety
|
||||
/// This function makes unsafe FFI calls
|
||||
pub unsafe fn original_connect(
|
||||
sock: c_int,
|
||||
addr: *const libc::sockaddr,
|
||||
len: libc::socklen_t,
|
||||
) -> c_int {
|
||||
let funcs = get_original_functions();
|
||||
if let Some(connect_fn) = funcs.connect {
|
||||
connect_fn(sock, addr, len)
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
/// Call the original getaddrinfo function
|
||||
///
|
||||
/// # Safety
|
||||
/// This function makes unsafe FFI calls
|
||||
pub unsafe fn original_getaddrinfo(
|
||||
node: *const c_char,
|
||||
service: *const c_char,
|
||||
hints: *const libc::addrinfo,
|
||||
res: *mut *mut libc::addrinfo,
|
||||
) -> c_int {
|
||||
let funcs = get_original_functions();
|
||||
if let Some(getaddrinfo_fn) = funcs.getaddrinfo {
|
||||
getaddrinfo_fn(node, service, hints, res)
|
||||
} else {
|
||||
libc::EAI_FAIL
|
||||
}
|
||||
}
|
||||
|
||||
/// Call the original freeaddrinfo function
|
||||
///
|
||||
/// # Safety
|
||||
/// This function makes unsafe FFI calls
|
||||
pub unsafe fn original_freeaddrinfo(res: *mut libc::addrinfo) {
|
||||
let funcs = get_original_functions();
|
||||
if let Some(freeaddrinfo_fn) = funcs.freeaddrinfo {
|
||||
freeaddrinfo_fn(res);
|
||||
}
|
||||
}
|
||||
|
||||
/// Call the original gethostbyname function
|
||||
///
|
||||
/// # Safety
|
||||
/// This function makes unsafe FFI calls
|
||||
pub unsafe fn original_gethostbyname(name: *const c_char) -> *mut libc::hostent {
|
||||
let funcs = get_original_functions();
|
||||
if let Some(gethostbyname_fn) = funcs.gethostbyname {
|
||||
gethostbyname_fn(name)
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
|
||||
/// Call the original getnameinfo function
|
||||
///
|
||||
/// # Safety
|
||||
/// This function makes unsafe FFI calls
|
||||
pub unsafe fn original_getnameinfo(
|
||||
sa: *const libc::sockaddr,
|
||||
salen: libc::socklen_t,
|
||||
host: *mut c_char,
|
||||
hostlen: libc::socklen_t,
|
||||
serv: *mut c_char,
|
||||
servlen: libc::socklen_t,
|
||||
flags: c_int,
|
||||
) -> c_int {
|
||||
let funcs = get_original_functions();
|
||||
if let Some(getnameinfo_fn) = funcs.getnameinfo {
|
||||
getnameinfo_fn(sa, salen, host, hostlen, serv, servlen, flags)
|
||||
} else {
|
||||
libc::EAI_FAIL
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_original_functions_creation() {
|
||||
let funcs = OriginalFunctions::new();
|
||||
assert!(funcs.connect.is_none());
|
||||
assert!(funcs.getaddrinfo.is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//! Hook module for LD_PRELOAD interposition
|
||||
//!
|
||||
//! This module provides the infrastructure for intercepting
|
||||
//! network system calls and redirecting them through proxy chains.
|
||||
|
||||
mod hooks;
|
||||
mod interpose;
|
||||
|
||||
pub use hooks::*;
|
||||
pub use interpose::*;
|
||||
@@ -0,0 +1,61 @@
|
||||
//! # proxychains
|
||||
//!
|
||||
//! A Rust implementation of the classic proxychains tool for proxy chaining.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! - Multiple proxy protocols: SOCKS4, SOCKS4a, SOCKS5, HTTP CONNECT
|
||||
//! - Chain modes: Strict, Dynamic, Random, Load Balance, Failover
|
||||
//! - DNS handling: Local resolution, Remote DNS through proxy, Fake IP mapping
|
||||
//! - IPv6 support (planned)
|
||||
//! - LD_PRELOAD hooks for transparent proxying
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use proxychains::{Config, ConfigParser, ChainManager};
|
||||
//!
|
||||
//! // Parse configuration
|
||||
//! let parser = ConfigParser::new();
|
||||
//! let config = parser.parse()?;
|
||||
//!
|
||||
//! // Create chain manager
|
||||
//! let manager = ChainManager::new(config);
|
||||
//!
|
||||
//! // Connect through proxy chain
|
||||
//! let stream = manager.connect_proxy_chain(
|
||||
//! "93.184.216.34".parse()?, // Target IP
|
||||
//! 80, // Target port
|
||||
//! Some("example.com"), // Optional domain
|
||||
//! )?;
|
||||
//! ```
|
||||
|
||||
pub mod chain;
|
||||
pub mod config;
|
||||
pub mod dns;
|
||||
pub mod error;
|
||||
pub mod hook;
|
||||
pub mod net;
|
||||
pub mod proxy;
|
||||
|
||||
// Re-exports for convenience
|
||||
pub use chain::{ChainManager, ChainError};
|
||||
pub use config::{Config, ConfigParser, ProxyData, ProxyType, ChainType};
|
||||
pub use dns::{DnsResolver, DnsCache};
|
||||
pub use error::{Error, Result};
|
||||
|
||||
/// Library version
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// Library name
|
||||
pub const NAME: &str = "proxychains-rs";
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_version() {
|
||||
assert!(!VERSION.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//! Network utilities module
|
||||
|
||||
mod socket;
|
||||
mod timeout;
|
||||
|
||||
pub use socket::*;
|
||||
pub use timeout::*;
|
||||
@@ -0,0 +1,182 @@
|
||||
//! Socket utilities
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
/// IP address type union (compatible with C version)
|
||||
#[repr(C)]
|
||||
pub union IpType {
|
||||
pub octet: [u8; 4],
|
||||
pub as_int: u32,
|
||||
}
|
||||
|
||||
impl IpType {
|
||||
pub fn new(octets: [u8; 4]) -> Self {
|
||||
Self { octet: octets }
|
||||
}
|
||||
|
||||
pub fn from_int(val: u32) -> Self {
|
||||
Self { as_int: val.to_be() }
|
||||
}
|
||||
|
||||
pub fn to_ipv4(&self) -> Ipv4Addr {
|
||||
// SAFETY: Both variants are [u8; 4] equivalent
|
||||
unsafe { Ipv4Addr::from(self.octet) }
|
||||
}
|
||||
|
||||
pub fn from_ipv4(addr: &Ipv4Addr) -> Self {
|
||||
Self {
|
||||
octet: addr.octets(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IpType {
|
||||
fn default() -> Self {
|
||||
Self { octet: [0; 4] }
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for IpType {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
octet: unsafe { self.octet },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Copy for IpType {}
|
||||
|
||||
impl std::fmt::Debug for IpType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("IpType")
|
||||
.field("octet", unsafe { &self.octet })
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Ipv4Addr> for IpType {
|
||||
fn from(addr: Ipv4Addr) -> Self {
|
||||
Self::from_ipv4(&addr)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for IpType {
|
||||
fn from(val: u32) -> Self {
|
||||
Self::from_int(val)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a TCP socket
|
||||
pub fn create_tcp_socket() -> std::io::Result<std::net::TcpStream> {
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
|
||||
let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))?;
|
||||
socket.set_reuse_address(true)?;
|
||||
socket.set_nodelay(true)?;
|
||||
|
||||
Ok(socket.into())
|
||||
}
|
||||
|
||||
/// Create a non-blocking TCP socket
|
||||
pub fn create_nonblocking_tcp_socket() -> std::io::Result<socket2::Socket> {
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
|
||||
let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))?;
|
||||
socket.set_reuse_address(true)?;
|
||||
socket.set_nodelay(true)?;
|
||||
socket.set_nonblocking(true)?;
|
||||
|
||||
Ok(socket)
|
||||
}
|
||||
|
||||
/// Check if an IP is a private/local address
|
||||
pub fn is_private_ip(ip: &Ipv4Addr) -> bool {
|
||||
let octets = ip.octets();
|
||||
|
||||
// 10.0.0.0/8
|
||||
if octets[0] == 10 {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 172.16.0.0/12
|
||||
if octets[0] == 172 && (16..=31).contains(&octets[1]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 192.168.0.0/16
|
||||
if octets[0] == 192 && octets[1] == 168 {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if an IP is localhost
|
||||
pub fn is_localhost(ip: &Ipv4Addr) -> bool {
|
||||
ip.is_loopback()
|
||||
}
|
||||
|
||||
/// Check if an IP is the "any" address (0.0.0.0)
|
||||
pub fn is_any_addr(ip: &Ipv4Addr) -> bool {
|
||||
ip.is_unspecified()
|
||||
}
|
||||
|
||||
/// Check if an IP is a broadcast address
|
||||
pub fn is_broadcast(ip: &Ipv4Addr) -> bool {
|
||||
ip.is_broadcast()
|
||||
}
|
||||
|
||||
/// Get the port from a sockaddr structure (platform-specific)
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub fn get_port_from_sockaddr(addr: *const libc::sockaddr) -> u16 {
|
||||
unsafe {
|
||||
let sa_family = (*addr).sa_family;
|
||||
if sa_family == libc::AF_INET as libc::sa_family_t {
|
||||
let addr_in = addr as *const libc::sockaddr_in;
|
||||
u16::from_be((*addr_in).sin_port)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the IP address from a sockaddr structure (platform-specific)
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub fn get_ip_from_sockaddr(addr: *const libc::sockaddr) -> Option<Ipv4Addr> {
|
||||
unsafe {
|
||||
let sa_family = (*addr).sa_family;
|
||||
if sa_family == libc::AF_INET as libc::sa_family_t {
|
||||
let addr_in = addr as *const libc::sockaddr_in;
|
||||
let ip_bytes = (*addr_in).sin_addr.s_addr.to_ne_bytes();
|
||||
Some(Ipv4Addr::from(ip_bytes))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ip_type() {
|
||||
let ip = IpType::new([192, 168, 1, 1]);
|
||||
let ipv4 = ip.to_ipv4();
|
||||
assert_eq!(ipv4, Ipv4Addr::new(192, 168, 1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_private_ip() {
|
||||
assert!(is_private_ip(&Ipv4Addr::new(10, 0, 0, 1)));
|
||||
assert!(is_private_ip(&Ipv4Addr::new(172, 16, 0, 1)));
|
||||
assert!(is_private_ip(&Ipv4Addr::new(192, 168, 1, 1)));
|
||||
assert!(!is_private_ip(&Ipv4Addr::new(8, 8, 8, 8)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_localhost() {
|
||||
assert!(is_localhost(&Ipv4Addr::new(127, 0, 0, 1)));
|
||||
assert!(!is_localhost(&Ipv4Addr::new(192, 168, 1, 1)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
//! Network timeout utilities
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// Read bytes from a stream with timeout
|
||||
pub fn read_bytes_timeout<T: Read>(stream: &mut T, count: usize, timeout: Duration) -> Result<Vec<u8>> {
|
||||
let mut buf = vec![0u8; count];
|
||||
let mut read_total = 0;
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
while read_total < count {
|
||||
let remaining = timeout.saturating_sub(start.elapsed());
|
||||
if remaining.is_zero() {
|
||||
return Err(Error::Timeout("Read timeout".to_string()));
|
||||
}
|
||||
|
||||
// Set read timeout on the stream
|
||||
let n = match stream.read(&mut buf[read_total..]) {
|
||||
Ok(0) => {
|
||||
return Err(Error::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::UnexpectedEof,
|
||||
"Unexpected EOF",
|
||||
)));
|
||||
}
|
||||
Ok(n) => n,
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
// Wait for data
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
continue;
|
||||
}
|
||||
Err(e) => return Err(Error::Io(e)),
|
||||
};
|
||||
|
||||
read_total += n;
|
||||
}
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Write bytes to a stream with timeout
|
||||
pub fn write_bytes_timeout<T: Write>(stream: &mut T, data: &[u8], timeout: Duration) -> Result<()> {
|
||||
let mut written_total = 0;
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
while written_total < data.len() {
|
||||
let remaining = timeout.saturating_sub(start.elapsed());
|
||||
if remaining.is_zero() {
|
||||
return Err(Error::Timeout("Write timeout".to_string()));
|
||||
}
|
||||
|
||||
let n = match stream.write(&data[written_total..]) {
|
||||
Ok(0) => {
|
||||
return Err(Error::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::WriteZero,
|
||||
"Write zero bytes",
|
||||
)));
|
||||
}
|
||||
Ok(n) => n,
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
continue;
|
||||
}
|
||||
Err(e) => return Err(Error::Io(e)),
|
||||
};
|
||||
|
||||
written_total += n;
|
||||
}
|
||||
|
||||
stream.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Connect to an address with timeout
|
||||
pub fn connect_with_timeout(
|
||||
addr: &std::net::SocketAddrV4,
|
||||
timeout: Duration,
|
||||
) -> Result<std::net::TcpStream> {
|
||||
let socket = socket2::Socket::new(
|
||||
socket2::Domain::IPV4,
|
||||
socket2::Type::STREAM,
|
||||
Some(socket2::Protocol::TCP),
|
||||
)?;
|
||||
|
||||
// Set non-blocking
|
||||
socket.set_nonblocking(true)?;
|
||||
|
||||
// Attempt to connect
|
||||
let connect_result = socket.connect(&(*addr).into());
|
||||
|
||||
match connect_result {
|
||||
Ok(()) => {
|
||||
socket.set_nonblocking(false)?;
|
||||
return Ok(socket.into());
|
||||
}
|
||||
Err(e) => {
|
||||
// Check if it's in progress
|
||||
if e.raw_os_error() != Some(libc::EINPROGRESS) {
|
||||
return Err(Error::Io(std::io::Error::from(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for connection with poll
|
||||
let mut poll_fd = libc::pollfd {
|
||||
fd: socket.as_raw_fd(),
|
||||
events: libc::POLLOUT,
|
||||
revents: 0,
|
||||
};
|
||||
|
||||
let result = unsafe {
|
||||
libc::poll(
|
||||
&mut poll_fd,
|
||||
1,
|
||||
timeout.as_millis() as libc::c_int,
|
||||
)
|
||||
};
|
||||
|
||||
if result == 0 {
|
||||
return Err(Error::Timeout(format!(
|
||||
"Connection to {} timed out",
|
||||
addr
|
||||
)));
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
let mut err: libc::c_int = 0;
|
||||
let mut err_len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
|
||||
|
||||
let ret = unsafe {
|
||||
libc::getsockopt(
|
||||
socket.as_raw_fd(),
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_ERROR,
|
||||
&mut err as *mut libc::c_int as *mut libc::c_void,
|
||||
&mut err_len,
|
||||
)
|
||||
};
|
||||
|
||||
if ret < 0 {
|
||||
return Err(Error::Io(std::io::Error::last_os_error()));
|
||||
}
|
||||
|
||||
if err != 0 {
|
||||
return Err(Error::Io(std::io::Error::from_raw_os_error(err)));
|
||||
}
|
||||
|
||||
// Restore blocking mode
|
||||
socket.set_nonblocking(false)?;
|
||||
|
||||
Ok(socket.into())
|
||||
}
|
||||
|
||||
/// Check if a socket is connected and writable
|
||||
pub fn is_connected<F: AsRawFd>(fd: &F) -> Result<bool> {
|
||||
let mut poll_fd = libc::pollfd {
|
||||
fd: fd.as_raw_fd(),
|
||||
events: libc::POLLOUT,
|
||||
revents: 0,
|
||||
};
|
||||
|
||||
let result = unsafe { libc::poll(&mut poll_fd, 1, 0) };
|
||||
|
||||
if result > 0 {
|
||||
// Check socket error
|
||||
let mut err: libc::c_int = 0;
|
||||
let mut err_len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
|
||||
|
||||
let ret = unsafe {
|
||||
libc::getsockopt(
|
||||
fd.as_raw_fd(),
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_ERROR,
|
||||
&mut err as *mut libc::c_int as *mut libc::c_void,
|
||||
&mut err_len,
|
||||
)
|
||||
};
|
||||
|
||||
if ret < 0 {
|
||||
return Err(Error::Io(std::io::Error::last_os_error()));
|
||||
}
|
||||
|
||||
Ok(err == 0)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set socket timeout options
|
||||
pub fn set_socket_timeout<F: AsRawFd>(fd: &F, timeout: Duration) -> Result<()> {
|
||||
let tv = libc::timeval {
|
||||
tv_sec: timeout.as_secs() as libc::time_t,
|
||||
tv_usec: (timeout.subsec_micros() as libc::suseconds_t),
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let ret = libc::setsockopt(
|
||||
fd.as_raw_fd(),
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_RCVTIMEO,
|
||||
&tv as *const libc::timeval as *const libc::c_void,
|
||||
std::mem::size_of::<libc::timeval>() as libc::socklen_t,
|
||||
);
|
||||
|
||||
if ret < 0 {
|
||||
return Err(Error::Io(std::io::Error::last_os_error()));
|
||||
}
|
||||
|
||||
let ret = libc::setsockopt(
|
||||
fd.as_raw_fd(),
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_SNDTIMEO,
|
||||
&tv as *const libc::timeval as *const libc::c_void,
|
||||
std::mem::size_of::<libc::timeval>() as libc::socklen_t,
|
||||
);
|
||||
|
||||
if ret < 0 {
|
||||
return Err(Error::Io(std::io::Error::last_os_error()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_timeout_duration() {
|
||||
let d = Duration::from_millis(5000);
|
||||
assert_eq!(d.as_millis(), 5000);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//! HTTP CONNECT protocol implementation
|
||||
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::config::ProxyData;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::net::write_bytes_timeout;
|
||||
|
||||
/// HTTP CONNECT connector
|
||||
pub struct HttpConnector<'a> {
|
||||
proxy: &'a ProxyData,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl<'a> HttpConnector<'a> {
|
||||
/// Create a new HTTP CONNECT connector
|
||||
pub fn new(proxy: &'a ProxyData, timeout: Duration) -> Self {
|
||||
Self { proxy, timeout }
|
||||
}
|
||||
|
||||
/// Connect to target through HTTP CONNECT proxy
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `stream` - Connected stream to proxy
|
||||
/// * `target_host` - Target hostname or IP
|
||||
/// * `target_port` - Target port
|
||||
pub fn connect<T: Read + Write>(
|
||||
&self,
|
||||
stream: &mut T,
|
||||
target_host: &str,
|
||||
target_port: u16,
|
||||
) -> Result<()> {
|
||||
// Build CONNECT request
|
||||
let mut request = format!(
|
||||
"CONNECT {}:{} HTTP/1.0\r\n",
|
||||
target_host, target_port
|
||||
);
|
||||
|
||||
// Add authentication if credentials are provided
|
||||
if let (Some(user), Some(pass)) = (&self.proxy.user, &self.proxy.pass) {
|
||||
let credentials = format!("{}:{}", user, pass);
|
||||
let encoded = base64::Engine::encode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
credentials,
|
||||
);
|
||||
request.push_str(&format!("Proxy-Authorization: Basic {}\r\n", encoded));
|
||||
}
|
||||
|
||||
// End headers
|
||||
request.push_str("\r\n");
|
||||
|
||||
// Send request
|
||||
write_bytes_timeout(stream, request.as_bytes(), self.timeout)?;
|
||||
|
||||
// Read response
|
||||
let response = self.read_response(stream)?;
|
||||
|
||||
// Parse and validate response
|
||||
self.validate_response(&response)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read HTTP response from stream
|
||||
fn read_response<T: Read>(&self, stream: &mut T) -> Result<String> {
|
||||
let mut reader = BufReader::new(stream);
|
||||
let mut response = String::new();
|
||||
|
||||
// Read until we get \r\n\r\n (end of headers)
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
let bytes_read = reader.read_line(&mut line)?;
|
||||
if bytes_read == 0 {
|
||||
break; // EOF
|
||||
}
|
||||
|
||||
response.push_str(&line);
|
||||
|
||||
// Check for end of headers (empty line)
|
||||
if line == "\r\n" || line == "\n" {
|
||||
break;
|
||||
}
|
||||
|
||||
// Safety check for response size
|
||||
if response.len() > 65536 {
|
||||
return Err(Error::Protocol("HTTP response too large".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Validate HTTP response
|
||||
fn validate_response(&self, response: &str) -> Result<()> {
|
||||
// Parse status line
|
||||
let status_line = response
|
||||
.lines()
|
||||
.next()
|
||||
.ok_or_else(|| Error::Protocol("Empty HTTP response".to_string()))?;
|
||||
|
||||
// Parse status code
|
||||
// Format: HTTP/1.x <status_code> <reason>
|
||||
let parts: Vec<&str> = status_line.split_whitespace().collect();
|
||||
if parts.len() < 2 {
|
||||
return Err(Error::Protocol(format!(
|
||||
"Invalid HTTP response: {}",
|
||||
status_line
|
||||
)));
|
||||
}
|
||||
|
||||
let status_code: u16 = parts[1]
|
||||
.parse()
|
||||
.map_err(|_| Error::Protocol(format!("Invalid status code: {}", parts[1])))?;
|
||||
|
||||
// Check for success (2xx)
|
||||
if !(200..300).contains(&status_code) {
|
||||
return Err(Error::ProxyConnection(format!(
|
||||
"HTTP proxy returned status {}",
|
||||
status_code
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to target through HTTP CONNECT proxy
|
||||
pub fn http_connect<T: Read + Write>(
|
||||
stream: &mut T,
|
||||
proxy: &ProxyData,
|
||||
target_host: &str,
|
||||
target_port: u16,
|
||||
timeout: Duration,
|
||||
) -> Result<()> {
|
||||
let connector = HttpConnector::new(proxy, timeout);
|
||||
connector.connect(stream, target_host, target_port)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
fn create_test_proxy() -> ProxyData {
|
||||
ProxyData::new(Ipv4Addr::new(192, 168, 1, 1), 8080, crate::config::ProxyType::Http)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_validation() {
|
||||
let proxy = create_test_proxy();
|
||||
let connector = HttpConnector::new(&proxy, Duration::from_secs(5));
|
||||
|
||||
// Valid 200 response
|
||||
assert!(connector.validate_response("HTTP/1.0 200 OK\r\n\r\n").is_ok());
|
||||
|
||||
// Valid 2xx response
|
||||
assert!(connector.validate_response("HTTP/1.1 204 No Content\r\n\r\n").is_ok());
|
||||
|
||||
// Invalid response (403)
|
||||
assert!(connector.validate_response("HTTP/1.0 403 Forbidden\r\n\r\n").is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
//! Proxy protocol implementations
|
||||
//!
|
||||
//! This module provides implementations for various proxy protocols:
|
||||
//! - SOCKS5 (RFC 1928)
|
||||
//! - SOCKS4/4a
|
||||
//! - HTTP CONNECT
|
||||
//! - Raw (direct connection)
|
||||
|
||||
mod http;
|
||||
mod raw;
|
||||
mod socks4;
|
||||
mod socks5;
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::config::{ProxyData, ProxyType};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::net::connect_with_timeout;
|
||||
|
||||
pub use http::*;
|
||||
pub use raw::*;
|
||||
pub use socks4::*;
|
||||
pub use socks5::*;
|
||||
|
||||
/// Target address for proxy connection
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TargetAddress {
|
||||
/// IP address only
|
||||
Ip(Ipv4Addr),
|
||||
/// Domain name only
|
||||
Domain(String),
|
||||
/// Both IP and domain (for SOCKS4a/5)
|
||||
Both { ip: Ipv4Addr, domain: String },
|
||||
}
|
||||
|
||||
impl TargetAddress {
|
||||
/// Create from IP address
|
||||
pub fn from_ip(ip: Ipv4Addr) -> Self {
|
||||
TargetAddress::Ip(ip)
|
||||
}
|
||||
|
||||
/// Create from domain name
|
||||
pub fn from_domain(domain: impl Into<String>) -> Self {
|
||||
TargetAddress::Domain(domain.into())
|
||||
}
|
||||
|
||||
/// Create with both IP and domain
|
||||
pub fn from_both(ip: Ipv4Addr, domain: impl Into<String>) -> Self {
|
||||
TargetAddress::Both {
|
||||
ip,
|
||||
domain: domain.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the IP address if available
|
||||
pub fn ip(&self) -> Option<&Ipv4Addr> {
|
||||
match self {
|
||||
TargetAddress::Ip(ip) => Some(ip),
|
||||
TargetAddress::Domain(_) => None,
|
||||
TargetAddress::Both { ip, .. } => Some(ip),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the domain name if available
|
||||
pub fn domain(&self) -> Option<&str> {
|
||||
match self {
|
||||
TargetAddress::Ip(_) => None,
|
||||
TargetAddress::Domain(d) => Some(d),
|
||||
TargetAddress::Both { domain, .. } => Some(domain),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get host string (domain or IP string)
|
||||
pub fn host(&self) -> String {
|
||||
match self {
|
||||
TargetAddress::Ip(ip) => ip.to_string(),
|
||||
TargetAddress::Domain(d) => d.clone(),
|
||||
TargetAddress::Both { domain, .. } => domain.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to a proxy server
|
||||
pub fn connect_to_proxy(proxy: &ProxyData, timeout: Duration) -> Result<std::net::TcpStream> {
|
||||
let addr = proxy.socket_addr();
|
||||
connect_with_timeout(&addr, timeout)
|
||||
}
|
||||
|
||||
/// Tunnel through a proxy to a target
|
||||
pub fn tunnel_through_proxy<T: Read + Write>(
|
||||
stream: &mut T,
|
||||
proxy: &ProxyData,
|
||||
target: &TargetAddress,
|
||||
target_port: u16,
|
||||
timeout: Duration,
|
||||
) -> Result<()> {
|
||||
match proxy.proxy_type {
|
||||
ProxyType::Socks5 => {
|
||||
// For SOCKS5, prefer domain if available
|
||||
let target_addr = if let Some(domain) = target.domain() {
|
||||
TargetAddr::from_domain(domain)
|
||||
} else if let Some(ip) = target.ip() {
|
||||
TargetAddr::from_ip(*ip)
|
||||
} else {
|
||||
return Err(Error::InvalidAddress);
|
||||
};
|
||||
socks5_connect(stream, proxy, &target_addr, target_port, timeout)
|
||||
}
|
||||
ProxyType::Socks4 => {
|
||||
// SOCKS4 requires IP, SOCKS4a can use domain
|
||||
let ip = target.ip().copied().unwrap_or(Ipv4Addr::new(0, 0, 0, 1));
|
||||
if let Some(domain) = target.domain() {
|
||||
socks4a_connect(stream, proxy, &ip, domain, target_port, timeout)
|
||||
} else {
|
||||
socks4_connect(stream, proxy, &ip, target_port, timeout)
|
||||
}
|
||||
}
|
||||
ProxyType::Http => {
|
||||
// HTTP CONNECT can use either
|
||||
http_connect(stream, proxy, &target.host(), target_port, timeout)
|
||||
}
|
||||
ProxyType::Raw => {
|
||||
// Raw does nothing
|
||||
raw_connect(stream, proxy, timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Establish a full proxy chain connection
|
||||
pub fn establish_proxy_chain(
|
||||
proxies: &[ProxyData],
|
||||
target: &TargetAddress,
|
||||
target_port: u16,
|
||||
connect_timeout: Duration,
|
||||
read_timeout: Duration,
|
||||
) -> Result<std::net::TcpStream> {
|
||||
if proxies.is_empty() {
|
||||
return Err(Error::ChainEmpty);
|
||||
}
|
||||
|
||||
// Connect to first proxy
|
||||
let first_proxy = &proxies[0];
|
||||
let mut stream = connect_to_proxy(first_proxy, connect_timeout)?;
|
||||
|
||||
// Set read timeout
|
||||
stream.set_read_timeout(Some(read_timeout))?;
|
||||
stream.set_write_timeout(Some(read_timeout))?;
|
||||
|
||||
// If only one proxy, tunnel directly to target
|
||||
if proxies.len() == 1 {
|
||||
tunnel_through_proxy(&mut stream, first_proxy, target, target_port, read_timeout)?;
|
||||
return Ok(stream);
|
||||
}
|
||||
|
||||
// Chain through multiple proxies
|
||||
for i in 1..proxies.len() {
|
||||
// Tunnel to next proxy (or final target)
|
||||
if i == proxies.len() - 1 {
|
||||
// Last hop - connect to target
|
||||
tunnel_through_proxy(
|
||||
&mut stream,
|
||||
&proxies[i - 1],
|
||||
target,
|
||||
target_port,
|
||||
read_timeout,
|
||||
)?;
|
||||
} else {
|
||||
// Intermediate hop - connect to next proxy
|
||||
let next_proxy = &proxies[i];
|
||||
let next_target = TargetAddress::from_ip(next_proxy.ip);
|
||||
tunnel_through_proxy(
|
||||
&mut stream,
|
||||
&proxies[i - 1],
|
||||
&next_target,
|
||||
next_proxy.port,
|
||||
read_timeout,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_target_address() {
|
||||
let ip = TargetAddress::from_ip(Ipv4Addr::new(192, 168, 1, 1));
|
||||
assert!(ip.ip().is_some());
|
||||
assert!(ip.domain().is_none());
|
||||
|
||||
let domain = TargetAddress::from_domain("example.com");
|
||||
assert!(domain.ip().is_none());
|
||||
assert!(domain.domain().is_some());
|
||||
|
||||
let both = TargetAddress::from_both(Ipv4Addr::new(192, 168, 1, 1), "example.com");
|
||||
assert!(both.ip().is_some());
|
||||
assert!(both.domain().is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//! Raw proxy type - direct connection without protocol
|
||||
//!
|
||||
//! This is used for direct connections in a proxy chain,
|
||||
//! where no protocol transformation is needed.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::config::ProxyData;
|
||||
use crate::error::Result;
|
||||
|
||||
/// Raw connector - does nothing, just passes through
|
||||
pub struct RawConnector<'a> {
|
||||
_proxy: &'a ProxyData,
|
||||
_timeout: Duration,
|
||||
}
|
||||
|
||||
impl<'a> RawConnector<'a> {
|
||||
/// Create a new raw connector
|
||||
pub fn new(proxy: &'a ProxyData, timeout: Duration) -> Self {
|
||||
Self {
|
||||
_proxy: proxy,
|
||||
_timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
/// "Connect" - does nothing for raw type
|
||||
pub fn connect<T: Read + Write>(&self, _stream: &mut T) -> Result<()> {
|
||||
// Raw type doesn't send any protocol data
|
||||
// The connection is already established
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw "connect" - does nothing
|
||||
pub fn raw_connect<T: Read + Write>(
|
||||
_stream: &mut T,
|
||||
_proxy: &ProxyData,
|
||||
_timeout: Duration,
|
||||
) -> Result<()> {
|
||||
// Raw type doesn't send any protocol data
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_raw_connect() {
|
||||
let proxy = ProxyData::default();
|
||||
let connector = RawConnector::new(&proxy, Duration::from_secs(5));
|
||||
// Raw connector should always succeed
|
||||
// (no actual stream needed for this test)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
//! SOCKS4 and SOCKS4a protocol implementation
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::config::ProxyData;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::net::{read_bytes_timeout, write_bytes_timeout};
|
||||
|
||||
/// SOCKS4 version
|
||||
const SOCKS4_VERSION: u8 = 0x04;
|
||||
|
||||
/// SOCKS4 commands
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Command {
|
||||
Connect = 0x01,
|
||||
Bind = 0x02,
|
||||
}
|
||||
|
||||
/// SOCKS4 reply codes
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum ReplyCode {
|
||||
Granted = 0x5A,
|
||||
Rejected = 0x5B,
|
||||
IdentFailed = 0x5C,
|
||||
UserIdInvalid = 0x5D,
|
||||
}
|
||||
|
||||
impl ReplyCode {
|
||||
fn from_u8(value: u8) -> Self {
|
||||
match value {
|
||||
0x5A => ReplyCode::Granted,
|
||||
0x5B => ReplyCode::Rejected,
|
||||
0x5C => ReplyCode::IdentFailed,
|
||||
0x5D => ReplyCode::UserIdInvalid,
|
||||
_ => ReplyCode::Rejected,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_success(&self) -> bool {
|
||||
matches!(self, ReplyCode::Granted)
|
||||
}
|
||||
|
||||
fn to_error(&self) -> Error {
|
||||
match self {
|
||||
ReplyCode::Granted => Error::Protocol("Unexpected success".to_string()),
|
||||
ReplyCode::Rejected => Error::ProxyConnection("Request rejected or failed".to_string()),
|
||||
ReplyCode::IdentFailed => {
|
||||
Error::ProxyConnection("Request failed because identd unreachable".to_string())
|
||||
}
|
||||
ReplyCode::UserIdInvalid => {
|
||||
Error::ProxyConnection("Request rejected due to invalid user ID".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// SOCKS4 connector
|
||||
pub struct Socks4Connector<'a> {
|
||||
proxy: &'a ProxyData,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl<'a> Socks4Connector<'a> {
|
||||
/// Create a new SOCKS4 connector
|
||||
pub fn new(proxy: &'a ProxyData, timeout: Duration) -> Self {
|
||||
Self { proxy, timeout }
|
||||
}
|
||||
|
||||
/// Connect to target through SOCKS4/4a proxy
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `stream` - Connected stream to proxy
|
||||
/// * `target_ip` - Target IP address (for SOCKS4)
|
||||
/// * `target_domain` - Target domain name (for SOCKS4a)
|
||||
/// * `target_port` - Target port
|
||||
///
|
||||
/// If target_domain is Some, SOCKS4a protocol is used.
|
||||
/// Otherwise, SOCKS4 protocol is used with target_ip.
|
||||
pub fn connect<T: Read + Write>(
|
||||
&self,
|
||||
stream: &mut T,
|
||||
target_ip: &Ipv4Addr,
|
||||
target_domain: Option<&str>,
|
||||
target_port: u16,
|
||||
) -> Result<()> {
|
||||
// Build request
|
||||
let mut request = Vec::new();
|
||||
|
||||
// Version and command
|
||||
request.push(SOCKS4_VERSION);
|
||||
request.push(Command::Connect as u8);
|
||||
|
||||
// Port (big-endian)
|
||||
request.extend_from_slice(&target_port.to_be_bytes());
|
||||
|
||||
// IP address or SOCKS4a marker
|
||||
if let Some(domain) = target_domain {
|
||||
// SOCKS4a: use invalid IP 0.0.0.x where x != 0
|
||||
// Convention is to use 0.0.0.1 to 0.0.0.255
|
||||
request.extend_from_slice(&[0, 0, 0, 1]);
|
||||
// Null-terminated user ID
|
||||
if let Some(ref user) = self.proxy.user {
|
||||
request.extend_from_slice(user.as_bytes());
|
||||
}
|
||||
request.push(0); // Null terminator for user ID
|
||||
// Null-terminated domain name
|
||||
request.extend_from_slice(domain.as_bytes());
|
||||
request.push(0); // Null terminator for domain
|
||||
} else {
|
||||
// SOCKS4: use actual IP
|
||||
request.extend_from_slice(&target_ip.octets());
|
||||
// Null-terminated user ID
|
||||
if let Some(ref user) = self.proxy.user {
|
||||
request.extend_from_slice(user.as_bytes());
|
||||
}
|
||||
request.push(0); // Null terminator
|
||||
}
|
||||
|
||||
// Send request
|
||||
write_bytes_timeout(stream, &request, self.timeout)?;
|
||||
|
||||
// Read response (8 bytes)
|
||||
let response = read_bytes_timeout(stream, 8, self.timeout)?;
|
||||
|
||||
// Check response
|
||||
if response[0] != 0x00 {
|
||||
return Err(Error::Protocol(format!(
|
||||
"Invalid SOCKS4 response null byte: {}",
|
||||
response[0]
|
||||
)));
|
||||
}
|
||||
|
||||
let reply = ReplyCode::from_u8(response[1]);
|
||||
if !reply.is_success() {
|
||||
return Err(reply.to_error());
|
||||
}
|
||||
|
||||
// Response bytes 2-7 contain port and IP (we don't need them)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to target through SOCKS4 proxy
|
||||
pub fn socks4_connect<T: Read + Write>(
|
||||
stream: &mut T,
|
||||
proxy: &ProxyData,
|
||||
target_ip: &Ipv4Addr,
|
||||
target_port: u16,
|
||||
timeout: Duration,
|
||||
) -> Result<()> {
|
||||
let connector = Socks4Connector::new(proxy, timeout);
|
||||
connector.connect(stream, target_ip, None, target_port)
|
||||
}
|
||||
|
||||
/// Connect to target through SOCKS4a proxy (with domain name)
|
||||
pub fn socks4a_connect<T: Read + Write>(
|
||||
stream: &mut T,
|
||||
proxy: &ProxyData,
|
||||
target_ip: &Ipv4Addr,
|
||||
target_domain: &str,
|
||||
target_port: u16,
|
||||
timeout: Duration,
|
||||
) -> Result<()> {
|
||||
let connector = Socks4Connector::new(proxy, timeout);
|
||||
connector.connect(stream, target_ip, Some(target_domain), target_port)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_reply_code() {
|
||||
assert!(ReplyCode::Granted.is_success());
|
||||
assert!(!ReplyCode::Rejected.is_success());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
//! SOCKS5 protocol implementation
|
||||
//!
|
||||
//! Implements RFC 1928 (SOCKS5) and RFC 1929 (Username/Password Authentication)
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::config::ProxyData;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::net::{read_bytes_timeout, write_bytes_timeout};
|
||||
|
||||
/// SOCKS5 version
|
||||
const SOCKS5_VERSION: u8 = 0x05;
|
||||
|
||||
/// SOCKS5 authentication methods
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum AuthMethod {
|
||||
/// No authentication required
|
||||
NoAuth = 0x00,
|
||||
/// GSSAPI
|
||||
GssApi = 0x01,
|
||||
/// Username/Password authentication
|
||||
UserPass = 0x02,
|
||||
/// No acceptable methods
|
||||
NoAcceptable = 0xFF,
|
||||
}
|
||||
|
||||
/// SOCKS5 commands
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Command {
|
||||
Connect = 0x01,
|
||||
Bind = 0x02,
|
||||
UdpAssociate = 0x03,
|
||||
}
|
||||
|
||||
/// SOCKS5 address types
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum AddressType {
|
||||
Ipv4 = 0x01,
|
||||
DomainName = 0x03,
|
||||
Ipv6 = 0x04,
|
||||
}
|
||||
|
||||
/// SOCKS5 reply codes
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum ReplyCode {
|
||||
Succeeded = 0x00,
|
||||
GeneralFailure = 0x01,
|
||||
ConnectionNotAllowed = 0x02,
|
||||
NetworkUnreachable = 0x03,
|
||||
HostUnreachable = 0x04,
|
||||
ConnectionRefused = 0x05,
|
||||
TtlExpired = 0x06,
|
||||
CommandNotSupported = 0x07,
|
||||
AddressTypeNotSupported = 0x08,
|
||||
}
|
||||
|
||||
impl ReplyCode {
|
||||
fn from_u8(value: u8) -> Self {
|
||||
match value {
|
||||
0x00 => ReplyCode::Succeeded,
|
||||
0x01 => ReplyCode::GeneralFailure,
|
||||
0x02 => ReplyCode::ConnectionNotAllowed,
|
||||
0x03 => ReplyCode::NetworkUnreachable,
|
||||
0x04 => ReplyCode::HostUnreachable,
|
||||
0x05 => ReplyCode::ConnectionRefused,
|
||||
0x06 => ReplyCode::TtlExpired,
|
||||
0x07 => ReplyCode::CommandNotSupported,
|
||||
0x08 => ReplyCode::AddressTypeNotSupported,
|
||||
_ => ReplyCode::GeneralFailure,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_success(&self) -> bool {
|
||||
matches!(self, ReplyCode::Succeeded)
|
||||
}
|
||||
|
||||
fn to_error(&self) -> Error {
|
||||
match self {
|
||||
ReplyCode::Succeeded => Error::Protocol("Unexpected success".to_string()),
|
||||
ReplyCode::GeneralFailure => Error::ProxyConnection("General SOCKS server failure".to_string()),
|
||||
ReplyCode::ConnectionNotAllowed => Error::Blocked,
|
||||
ReplyCode::NetworkUnreachable => Error::ProxyConnection("Network unreachable".to_string()),
|
||||
ReplyCode::HostUnreachable => Error::ProxyConnection("Host unreachable".to_string()),
|
||||
ReplyCode::ConnectionRefused => Error::ProxyConnection("Connection refused".to_string()),
|
||||
ReplyCode::TtlExpired => Error::ProxyConnection("TTL expired".to_string()),
|
||||
ReplyCode::CommandNotSupported => Error::Protocol("Command not supported".to_string()),
|
||||
ReplyCode::AddressTypeNotSupported => Error::Protocol("Address type not supported".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Target address for SOCKS5 connection
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TargetAddr {
|
||||
/// IPv4 address
|
||||
Ip(Ipv4Addr),
|
||||
/// Domain name
|
||||
Domain(String),
|
||||
}
|
||||
|
||||
impl TargetAddr {
|
||||
/// Create from IP address
|
||||
pub fn from_ip(ip: Ipv4Addr) -> Self {
|
||||
TargetAddr::Ip(ip)
|
||||
}
|
||||
|
||||
/// Create from domain name
|
||||
pub fn from_domain(domain: &str) -> Self {
|
||||
TargetAddr::Domain(domain.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// SOCKS5 connector
|
||||
pub struct Socks5Connector<'a> {
|
||||
proxy: &'a ProxyData,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl<'a> Socks5Connector<'a> {
|
||||
/// Create a new SOCKS5 connector
|
||||
pub fn new(proxy: &'a ProxyData, timeout: Duration) -> Self {
|
||||
Self { proxy, timeout }
|
||||
}
|
||||
|
||||
/// Connect to target through SOCKS5 proxy
|
||||
pub fn connect<T: Read + Write>(
|
||||
&self,
|
||||
stream: &mut T,
|
||||
target_addr: &TargetAddr,
|
||||
target_port: u16,
|
||||
) -> Result<()> {
|
||||
// Step 1: Negotiate authentication method
|
||||
self.negotiate_auth(stream)?;
|
||||
|
||||
// Step 2: Authenticate if required
|
||||
if self.proxy.user.is_some() {
|
||||
self.authenticate(stream)?;
|
||||
}
|
||||
|
||||
// Step 3: Send CONNECT request
|
||||
self.send_connect_request(stream, target_addr, target_port)?;
|
||||
|
||||
// Step 4: Read response
|
||||
self.read_connect_response(stream)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Negotiate authentication method
|
||||
fn negotiate_auth<T: Read + Write>(&self, stream: &mut T) -> Result<()> {
|
||||
// Build greeting message
|
||||
let mut greeting = vec![SOCKS5_VERSION];
|
||||
|
||||
if self.proxy.user.is_some() {
|
||||
// Offer both no-auth and user/pass methods
|
||||
greeting.push(2);
|
||||
greeting.push(AuthMethod::NoAuth as u8);
|
||||
greeting.push(AuthMethod::UserPass as u8);
|
||||
} else {
|
||||
// Only offer no-auth
|
||||
greeting.push(1);
|
||||
greeting.push(AuthMethod::NoAuth as u8);
|
||||
}
|
||||
|
||||
// Send greeting
|
||||
write_bytes_timeout(stream, &greeting, self.timeout)?;
|
||||
|
||||
// Read server's method selection
|
||||
let response = read_bytes_timeout(stream, 2, self.timeout)?;
|
||||
|
||||
if response[0] != SOCKS5_VERSION {
|
||||
return Err(Error::Protocol(format!(
|
||||
"Invalid SOCKS version: {}",
|
||||
response[0]
|
||||
)));
|
||||
}
|
||||
|
||||
let method = response[1];
|
||||
if method == AuthMethod::NoAcceptable as u8 {
|
||||
return Err(Error::AuthFailed(
|
||||
"No acceptable authentication method".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check if the server selected a method we offered
|
||||
if method == AuthMethod::NoAuth as u8 {
|
||||
// No authentication required, we're done
|
||||
Ok(())
|
||||
} else if method == AuthMethod::UserPass as u8 {
|
||||
if self.proxy.user.is_none() {
|
||||
return Err(Error::AuthFailed(
|
||||
"Server requires authentication but no credentials provided".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Protocol(format!(
|
||||
"Server selected unsupported auth method: {}",
|
||||
method
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform username/password authentication (RFC 1929)
|
||||
fn authenticate<T: Read + Write>(&self, stream: &mut T) -> Result<()> {
|
||||
let user = self.proxy.user.as_ref().unwrap();
|
||||
let pass = self.proxy.pass.as_ref().unwrap();
|
||||
|
||||
// Validate lengths
|
||||
if user.len() > 255 || pass.len() > 255 {
|
||||
return Err(Error::AuthFailed("Username or password too long".to_string()));
|
||||
}
|
||||
|
||||
// Build auth request: [version][ulen][username][plen][password]
|
||||
let mut auth_request = Vec::with_capacity(3 + user.len() + pass.len());
|
||||
auth_request.push(0x01); // Sub-negotiation version
|
||||
auth_request.push(user.len() as u8);
|
||||
auth_request.extend_from_slice(user.as_bytes());
|
||||
auth_request.push(pass.len() as u8);
|
||||
auth_request.extend_from_slice(pass.as_bytes());
|
||||
|
||||
// Send auth request
|
||||
write_bytes_timeout(stream, &auth_request, self.timeout)?;
|
||||
|
||||
// Read auth response
|
||||
let response = read_bytes_timeout(stream, 2, self.timeout)?;
|
||||
|
||||
if response[1] != 0x00 {
|
||||
return Err(Error::AuthFailed("Authentication failed".to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send CONNECT request
|
||||
fn send_connect_request<T: Read + Write>(
|
||||
&self,
|
||||
stream: &mut T,
|
||||
target_addr: &TargetAddr,
|
||||
target_port: u16,
|
||||
) -> Result<()> {
|
||||
let mut request = vec![
|
||||
SOCKS5_VERSION,
|
||||
Command::Connect as u8,
|
||||
0x00, // Reserved
|
||||
];
|
||||
|
||||
// Add address
|
||||
match target_addr {
|
||||
TargetAddr::Ip(ip) => {
|
||||
request.push(AddressType::Ipv4 as u8);
|
||||
request.extend_from_slice(&ip.octets());
|
||||
}
|
||||
TargetAddr::Domain(domain) => {
|
||||
if domain.len() > 255 {
|
||||
return Err(Error::Protocol("Domain name too long".to_string()));
|
||||
}
|
||||
request.push(AddressType::DomainName as u8);
|
||||
request.push(domain.len() as u8);
|
||||
request.extend_from_slice(domain.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
// Add port (big-endian)
|
||||
request.extend_from_slice(&target_port.to_be_bytes());
|
||||
|
||||
// Send request
|
||||
write_bytes_timeout(stream, &request, self.timeout)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read CONNECT response
|
||||
fn read_connect_response<T: Read + Write>(&self, stream: &mut T) -> Result<()> {
|
||||
// Read header: [version][reply][reserved][atype]
|
||||
let header = read_bytes_timeout(stream, 4, self.timeout)?;
|
||||
|
||||
if header[0] != SOCKS5_VERSION {
|
||||
return Err(Error::Protocol(format!(
|
||||
"Invalid SOCKS version in response: {}",
|
||||
header[0]
|
||||
)));
|
||||
}
|
||||
|
||||
let reply = ReplyCode::from_u8(header[1]);
|
||||
if !reply.is_success() {
|
||||
return Err(reply.to_error());
|
||||
}
|
||||
|
||||
// Read bound address based on address type
|
||||
let atyp = header[3];
|
||||
match atyp {
|
||||
0x01 => {
|
||||
// IPv4 - read 4 bytes + 2 bytes port
|
||||
let _ = read_bytes_timeout(stream, 6, self.timeout)?;
|
||||
}
|
||||
0x03 => {
|
||||
// Domain name - read length first
|
||||
let len_byte = read_bytes_timeout(stream, 1, self.timeout)?;
|
||||
let len = len_byte[0] as usize;
|
||||
|
||||
// Read domain + port
|
||||
let _ = read_bytes_timeout(stream, len + 2, self.timeout)?;
|
||||
}
|
||||
0x04 => {
|
||||
// IPv6 - read 16 bytes + 2 bytes port
|
||||
let _ = read_bytes_timeout(stream, 18, self.timeout)?;
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::Protocol(format!(
|
||||
"Unknown address type in response: {}",
|
||||
atyp
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to target through SOCKS5 proxy
|
||||
pub fn socks5_connect<T: Read + Write>(
|
||||
stream: &mut T,
|
||||
proxy: &ProxyData,
|
||||
target_addr: &TargetAddr,
|
||||
target_port: u16,
|
||||
timeout: Duration,
|
||||
) -> Result<()> {
|
||||
let connector = Socks5Connector::new(proxy, timeout);
|
||||
connector.connect(stream, target_addr, target_port)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_reply_code_is_success() {
|
||||
assert!(ReplyCode::Succeeded.is_success());
|
||||
assert!(!ReplyCode::GeneralFailure.is_success());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_target_addr() {
|
||||
let ip = TargetAddr::from_ip(Ipv4Addr::new(192, 168, 1, 1));
|
||||
assert!(matches!(ip, TargetAddr::Ip(_)));
|
||||
|
||||
let domain = TargetAddr::from_domain("example.com");
|
||||
assert!(matches!(domain, TargetAddr::Domain(_)));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user