diff --git a/Cargo.lock b/Cargo.lock index 3a07834..7d7cf1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -53,6 +102,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -115,6 +170,52 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + [[package]] name = "core-foundation" version = "0.9.4" @@ -319,7 +420,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", "indexmap", "slab", "tokio", @@ -333,6 +434,36 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http 0.2.12", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http 0.2.12", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -350,6 +481,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -357,7 +499,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -384,8 +549,8 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -397,6 +562,87 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +dependencies = [ + "bytes", + "http 1.1.0", + "http-body 1.0.1", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-proxy" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca815a891b24fdfb243fa3239c86154392b0953ee584aa1a2a1f66d20cbe75cc" +dependencies = [ + "bytes", + "futures", + "headers", + "http 0.2.12", + "hyper 0.14.30", + "hyper-tls 0.5.0", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.30", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.5.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.5.0", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -430,6 +676,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.11" @@ -456,6 +708,12 @@ dependencies = [ "serde", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.158" @@ -490,6 +748,12 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.8.0" @@ -699,15 +963,20 @@ dependencies = [ [[package]] name = "proxer" -version = "0.1.2" +version = "0.2.0" dependencies = [ - "base64", + "base64 0.22.1", "chrono", + "clap", "futures", - "hyper", + "hyper 0.14.30", + "hyper-proxy", + "hyper-tls 0.6.0", "json5", + "lazy_static", "port_check", "serde", + "sha2", "signal-hook", "signal-hook-tokio", "tokio", @@ -820,6 +1089,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.8" @@ -893,6 +1173,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.77" @@ -1072,6 +1358,12 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index b6cd953..a11ee64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "proxer" -version = "0.1.2" +version = "0.2.0" edition = "2021" authors = ["doroved"] -description = "Proxy manager all network requests on macOS + spoofDPI all direct connections." +description = "Proxy all macOS network requests with domain-based filtering. Basic spoof DPI for direct connections." readme = "README.md" repository = "https://github.com/doroved/proxer" license = "MIT OR Apache-2.0" @@ -27,6 +27,23 @@ futures = "0.3.30" wildmatch = "2.3.4" json5 = "0.4.1" chrono = "0.4.38" +clap = { version = "4.5.20", features = ["derive"] } +sha2 = "0.10.8" +lazy_static = "1.5.0" +# simple-home-dir = "0.4.3" +hyper-tls = "0.6.0" +hyper-proxy = "0.9.1" +# hyper-util = "0.1.9" +# http-body-util = "0.1.2" +# bytes = "1.7.2" +# hyper-tls = "0.5" +# hyper-proxy = "0.7" +# Ошибка с этой версией +# headers = "0.4.0" +# headers = "0.3.0" +# reqwest = "0.12.8" +# hyper-proxy2 = "0.1.0" +# hyper-http-proxy = "1.0.0" [profile.release] panic = "abort" # Strip expensive panic clean-up logic diff --git a/README.md b/README.md index 4582870..4440b87 100644 --- a/README.md +++ b/README.md @@ -4,44 +4,166 @@ Network request proxy manager with host filtering on macOS + spoofDPI direct con ![proxer screenshot](screenshot.png) -## How to use +## How to Install -1. Clone the repository. +Just log into your macOS terminal and run the command: ```bash -git clone https://github.com/doroved/proxer.git +curl -fsSL https://raw.githubusercontent.com/doroved/proxer/main/install.sh | bash ``` +After installation, be sure to run this command to make proxer available in the current terminal session: -2. Rename `proxer.example.json5` to `proxer.json5` and edit it. +```bash +export PATH=$PATH:~/.proxer/bin +``` +To update proxer to the latest version, use the same command that was used for installation. + +## Key Features: +- Traffic filtering by hosts with caching. +- Basic SpoofDPI support. +- Setting secret token for [Proxerver](https://github.com/doroved/proxerver) to protect against proxy detection. + +``` +proxer --help + +Proxy all macOS network requests with domain-based filtering. Basic spoof DPI for direct connections. + +Usage: proxer [OPTIONS] + +Options: + --port Set port for proxer. By default, a random port is used. + --dpi Enable DPI spoofing for direct connections. Spoofing is disabled by default. + --config Path to the configuration file. Example: '/path/to/proxer.(json5|json)'. Default is ~/.proxer/config.json5. + --token Secret token to access the HTTP/S proxerver. Must match the token specified in the proxerver configuration. + --log-error-all Show all errors. By default, only critical errors are shown. This option is useful for debugging. + -h, --help Print help + -V, --version Print version +``` + +The default configuration file is located in `~/.proxer/config.json5`. To edit it, you can quickly open it using the terminal command: ```bash -mv proxer.example.json5 proxer.json5 +open -a TextEdit ~/.proxer/config.json5 ``` -3. Run `cargo build --release` to build the binary. +If you want to use your own configuration file, you can specify it at startup using the `--config` flag. +For example, if you are in a directory with the config, you can run proxer as follows: + +```bash +proxer --config ./config.json5 +``` +Configuration file structure: + +```json5 +[ + { + "name": "Proxer Free [DE] proxerver", + "enabled": true, + "scheme": "HTTPS", + "host": "proxerver.freemyip.com", + "port": 443, + "auth_credentials": { + "username": "proxerver", + "password": "onelove" + }, + "filter": [ + { + "name": "YouTube", + "domains": ["*.youtube.com", "*.googlevideo.com", "*.ggpht.com"] + }, + { + "name": "Discord", + "domains": [ + "discord.com", + "*.discord.com", + "*.discordapp.com", + "discord-attachments-*.storage.googleapis.com", + "*.discordapp.net", + "gateway.discord.gg" + ] + }, + { + "name": "Test", + "domains": ["api.ipify.org"] + } + ] + } +] +``` +If your proxies don't require authentication, you can leave the `auth_credentials.username` and `auth_credentials.password` fields empty. + +## Examples of Proxer Launch Commands + +Set port 5555 for the local Proxer server. + +```bash +proxer --port 5555 +``` + +Enable basic DPI bypass. +```bash +proxer --dpi +``` + +Set the secret token that was set when starting the `Proxerver` proxy server. This will allow the proxy server to accept requests only from your client. +```bash +proxer --token 'HelloProxerver' +``` + +Enable display of all proxy server connection errors. +```bash +proxer --log-error-all +``` + +To run the Proxer in the background, use nohup, for example: + +```bash +nohup proxer [OPTIONS] >/dev/null 2>&1 & +``` + +Running the Proxer in the background using nohup and saving the output to a file: + +```bash +nohup proxer [OPTIONS] > ~/.proxer/log.txt 2>&1 & +``` + +## Local Build and Run + +1. Clone the repository. + +```bash +git clone https://github.com/doroved/proxer.git +``` + +2. Run `cargo build --release` to build the binary. ```bash cargo build --release ``` -4. Run `./target/release/proxer` to start Proxer. +3. Run the Proxer binary with configuration. ```bash -./target/release/proxer +./target/release/proxer --config 'proxer.dev.json5' ``` -5. Or run it in background process using `nohup`. +4. Or run it in background process using `nohup`. ```bash -nohup ./target/release/proxer >/dev/null 2>&1 & +nohup ./target/release/proxer --config 'proxer.dev.json5' >/dev/null 2>&1 & ``` -6. Run `kill $(pgrep proxer)` to stop Proxer. +5. To stop Proxer, run this command. ```bash kill $(pgrep proxer) ``` +6. See Proxer running on your machine +```bash +lsof -i -P | grep LISTEN | grep proxer +``` + ## Interesting projects - [DumbProxy](https://github.com/SenseUnit/dumbproxy) - Great proxy server with various features diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..f2a3295 --- /dev/null +++ b/install.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +# Check architecture +arch=$(uname -m) +if [[ "$arch" != "x86_64" && "$arch" != "arm64" ]]; then + echo "Error: Unsupported architecture $arch. Exiting script." + exit 1 +fi + +# Determine the appropriate architecture for the orb command +if [ "$arch" = "aarch64" ]; then + short_arch="arm64" +else + short_arch="x86_64" +fi + +# Function to add PATH to the configuration file +update_path() { + local rc_file=$1 + if ! grep -q "export PATH=.*proxer/bin" "$rc_file"; then + echo "# Proxer" >> "$rc_file" + echo "export PATH=\$PATH:~/.proxer/bin" >> "$rc_file" + source "$rc_file" + echo "Updated $rc_file" + else + echo "Path already added in $rc_file" + fi +} + +# Fetch the latest release tag from GitHub +curl "https://api.github.com/repos/doroved/proxer/releases/latest" | + grep '"tag_name":' | + sed -E 's/.*"([^"]+)".*/\1/' | + xargs -I {} curl -OL "https://github.com/doroved/proxer/releases/download/"\{\}"/proxer.darwin-${short_arch}.tar.gz" + +# Create directory for installation +mkdir -p ~/.proxer/bin + +# Extract and move the files +tar -xzvf ./proxer.darwin-${short_arch}.tar.gz && \ + rm -rf ./proxer.darwin-${short_arch}.tar.gz && \ + mv ./proxer ~/.proxer/bin + +# Check for errors in the previous commands +if [ $? -ne 0 ]; then + echo "Error. Exiting now." + exit +fi + +# Remove the quarantine attribute from the proxer executable to allow it to run without restrictions +xattr -d com.apple.quarantine ~/.proxer/bin/proxer + +# Check if config.json5 exists, if not, create it +if [ ! -f ~/.proxer/config.json5 ]; then + cat < ~/.proxer/config.json5 +[ + { + "name": "Proxer Free [DE] proxerver", + "enabled": true, + "scheme": "HTTPS", + "host": "proxerver.freemyip.com", + "port": 443, + "auth_credentials": { + "username": "proxerver", + "password": "onelove" + }, + "filter": [ + { + "name": "YouTube", + "domains": ["*.youtube.com", "*.googlevideo.com", "*.ggpht.com"] + }, + { + "name": "Discord", + "domains": [ + "discord.com", + "*.discord.com", + "*.discordapp.com", + "discord-attachments-*.storage.googleapis.com", + "*.discordapp.net", + "gateway.discord.gg" + ] + }, + { + "name": "Test", + "domains": ["api.ipify.org"] + } + ] + } +] +EOL + echo "Created config.json5 in ~/.proxer" +else + echo "config.json5 already exists in ~/.proxer" +fi + +# Add to PATH +export PATH=$PATH:~/.proxer/bin + +# Check for .bashrc and .zshrc and append PATH export if they exist +if [ -f ~/.bashrc ]; then + update_path ~/.bashrc +fi + +if [ -f ~/.zshrc ]; then + update_path ~/.zshrc +fi + +# Success message with version +proxer_version=$(proxer -V) +echo "" +echo "Successfully installed $proxer_version" + +# Run the proxer help command +proxer --help +echo "" +echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"; +echo "Please copy and paste this command into the terminal and press Enter:" +echo "export PATH=\$PATH:~/.proxer/bin" diff --git a/proxer.dev.json5 b/proxer.dev.json5 new file mode 100644 index 0000000..742f3c4 --- /dev/null +++ b/proxer.dev.json5 @@ -0,0 +1,34 @@ +[ + { + name: "Proxer Free [DE] proxerver", + enabled: true, + scheme: "HTTPS", + host: "proxerver.freemyip.com", + port: 443, + auth_credentials: { + username: "proxerver", + password: "onelove", + }, + filter: [ + { + name: "YouTube", + domains: ["*.youtube.com", "*.googlevideo.com", "*.ggpht.com"], + }, + { + name: "Discord", + domains: [ + "discord.com", + "*.discord.com", + "*.discordapp.com", + "discord-attachments-*.storage.googleapis.com", + "*.discordapp.net", + "gateway.discord.gg", + ], + }, + { + name: "Test", + domains: ["api.ipify.org"], + }, + ], + }, +] diff --git a/proxer.example.json5 b/proxer.example.json5 deleted file mode 100644 index 36ed2a0..0000000 --- a/proxer.example.json5 +++ /dev/null @@ -1,68 +0,0 @@ -[ - { - name: "My dumbproxy [NL]", - enabled: true, // false to disable - scheme: "HTTPS", // HTTP, HTTPS - host: "proxy.example.com", - port: 8443, - auth_credentials: { - username: "admin", - password: "hello", - }, - filter: [ - { - name: "Facebook", - domains: [ - "*.facebook.com", - "*.fbcdn.net", - "fb.com", - "fb.me", - "*.fbsbx.com", - "messenger.com", - ], - }, - { - name: "Instagram", - domains: [ - "instagram.com", - "cdninstagram.com", - "*.instagram.com", - "*.cdninstagram.com", - ], - }, - ], - }, - { - name: "My dumbproxy [DE]", - enabled: false, - scheme: "HTTP", - host: "91.201.153.132", - port: 8080, - auth_credentials: { - username: "admin", - password: "hello", - }, - filter: [ - { - name: "Spotify", - domains: [ - "spotify.com", - "*.spotify.com", - "*.spotifycdn.com", - "*.scdn.co", - ], - }, - { - name: "Twitter", - domains: [ - "x.com", - "*.x.com", - "twitter.com", - "*.twitter.com", - "*.twimg.com", - "t.co", - ], - }, - ], - }, -] diff --git a/release.sh b/release.sh new file mode 100644 index 0000000..1405f91 --- /dev/null +++ b/release.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Extract the project name and version from Cargo.toml +project_name=$(grep '^name' Cargo.toml | sed 's/name = "\(.*\)"/\1/' | tr -d '[:space:]') +version=$(grep '^version' Cargo.toml | sed 's/version = "\(.*\)"/\1/' | tr -d '[:space:]') + +# Define architectures +architectures=("aarch64-apple-darwin" "x86_64-apple-darwin") + +# Build for each architecture +for arch in "${architectures[@]}"; do + cargo build --release --target=$arch + # Extract architecture for naming + short_arch=$(echo $arch | sed 's/-apple-darwin//') + # Move the binary to the release directory with new naming convention + mkdir -p ./target/release/v${version} + mv ./target/$arch/release/$project_name ./target/release/v${version}/${project_name}.darwin-${short_arch} +done + +# Change directory to release +cd ./target/release/v${version} || exit + +# Create tar.gz and delete original binaries +for arch in "${architectures[@]}"; do + short_arch=$(echo $arch | sed 's/-apple-darwin//') + binary_name="${project_name}.darwin-${short_arch}" + mv ${binary_name} ${project_name} + tar -czf "${binary_name}.tar.gz" "${project_name}" + rm "${project_name}" +done diff --git a/screenshot.png b/screenshot.png index 98bdc15..ff834d7 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..ac34ad4 --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,37 @@ +use lazy_static::lazy_static; +use std::{collections::HashMap, sync::Mutex}; + +// Store config_index and filter_index by host +#[derive(Debug)] +pub struct HostCache { + pub cache: HashMap, +} + +impl HostCache { + // Create a new HostCache instance + pub fn new() -> HostCache { + HostCache { + cache: HashMap::new(), + } + } + + // Add host, config_index and filter_index to cache + pub fn add(&mut self, host: String, config_index: usize, filter_index: usize) { + self.cache.insert(host, (config_index, filter_index)); + } + + // Check if the host is in the cache + pub fn contains(&self, host: &str) -> bool { + self.cache.contains_key(host) + } + + // Get config_index and filter_index by host + pub fn get(&self, host: &str) -> Option<&(usize, usize)> { + self.cache.get(host) + } +} + +// Create a static mutex for the HostCache +lazy_static! { + pub static ref HOST_CACHE: Mutex = Mutex::new(HostCache::new()); +} diff --git a/src/lib.rs b/src/lib.rs index e522c7d..c64f7e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,16 @@ use base64::{engine::general_purpose::STANDARD as b64, Engine as _}; +use cache::HOST_CACHE; use chrono::Local; +use clap::Parser; +use hyper::client::connect::HttpConnector; use hyper::{Body, Client, Method, Request, Response, StatusCode}; +use hyper_proxy::{Intercept, Proxy as HyperProxy, ProxyConnector}; +// use hyper_tls::HttpsConnector; use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::io; use std::{process::Command, string::FromUtf8Error, sync::Arc, time::Duration}; +use tokio::time::{sleep, Instant}; use tokio::{ io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, net::TcpStream, @@ -12,6 +20,11 @@ use tokio_native_tls::{native_tls, TlsConnector}; use toml::from_str; use wildmatch::WildMatch; +mod cache; +mod options; + +use options::Opt; + // Struct for storing package information #[derive(Debug, Deserialize)] pub struct CargoToml { @@ -42,35 +55,58 @@ pub struct ProxyConfig { name: String, enabled: bool, scheme: String, - host: String, - port: u16, + pub host: String, + pub port: u16, auth_credentials: AuthCredentials, filter: Vec, } +pub enum ProxyState { + On, + Off, +} + pub struct Proxy { pub interface: String, pub server: String, pub port: u16, } +// Clients store +// enum ProxiedClient { +// Http(Client>), +// Https(Client>>), +// } + // Handle HTTP and HTTPS requests pub async fn handle_request( req: Request, config: Arc>, ) -> Result, hyper::Error> { let addr = req.uri().authority().unwrap().to_string(); + let host = req.uri().host().unwrap(); + + let options = Opt::parse(); let time = formatted_time(); + let found_proxy = find_matching_proxy(config.as_ref(), host); + if req.method() == Method::CONNECT { if let Some(_) = req.uri().authority().map(|auth| auth.to_string()) { - let config_clone = Arc::clone(&config); - tokio::spawn(async move { - match tunnel(req, config_clone).await { + match tunnel(req, found_proxy.clone()).await { Ok(_) => {} Err(e) => { - eprintln!("\x1B[31m\x1B[1m[{time}] {} -> {}\x1B[0m", addr, e); + let error_msg = e.to_string(); + + if options.log_error_all { + eprintln!("\x1B[31m\x1B[1m[{time}] {addr} → {error_msg}\x1B[0m"); + } else if error_msg.contains("os error 60") + || error_msg.contains("Proxy connection failed") + || error_msg.contains("deadline has elapsed") + { + eprintln!("\x1B[31m\x1B[1m[{time}] {addr} → {error_msg}\x1B[0m"); + } } } }); @@ -86,11 +122,8 @@ pub async fn handle_request( .unwrap()) } } else { - println!("HTTP request {}", req.uri().to_string()); - // TODO: Proxy HTTP requests based on allow_hosts - - // Create client for HTTP request - let client = Client::new(); + println!("HTTP request {}, req: {:?}", addr, req); + // TODO: Proxy HTTP requests based on filters through HTTPS proxy, for HTTP proxies works. // Copy the necessary data from req let method = req.method().clone(); @@ -112,105 +145,174 @@ pub async fn handle_request( // Copy headers new_req.headers_mut().extend(headers); - // Send the request and return the response - client.request(new_req).await + // Processing of the found proxy + if let Some((proxy_config, _filter_name)) = found_proxy { + match send_request_through_proxy(new_req, &proxy_config).await { + Ok(response) => { + return Ok(response); + } + Err(err) => { + return Ok(Response::builder() + .status(StatusCode::BAD_GATEWAY) + .body(Body::from(format!("Error: {}", err))) + .unwrap()); + } + } + } + + // If no proxy is found, simply send the request + let client = Client::new(); + let response = client.request(new_req).await?; + Ok(response) + } +} + +async fn send_request_through_proxy( + req: Request, + proxy_config: &ProxyConfig, +) -> Result, hyper::Error> { + let mut new_req = req; + new_req.headers_mut().insert("proxy-authorization", { + let credentials = format!( + "{}:{}", + proxy_config.auth_credentials.username, proxy_config.auth_credentials.password + ); + hyper::header::HeaderValue::from_str(&format!("Basic {}", b64.encode(credentials))).unwrap() + }); + + if proxy_config.scheme.eq_ignore_ascii_case("http") { + // Create a new proxy connector + let http = HttpConnector::new(); + + let proxy_url = format!("http://{}:{}", proxy_config.host, proxy_config.port); + let proxy = HyperProxy::new( + Intercept::All, + proxy_url.parse::().expect("Invalid proxy URI"), + ); + + // Create proxy client + let proxy_connector = ProxyConnector::from_proxy(http, proxy).unwrap(); + let client = Client::builder().build::<_, hyper::Body>(proxy_connector); + + // Send request + let res = client.request(new_req).await?; + + Ok(res) + } else { + // Create a new proxy connector + // let https = HttpsConnector::new(); + + // let proxy_url = format!("https://{}:{}", proxy_config.host, proxy_config.port); + // let proxy = HyperProxy::new(Intercept::All, proxy_url.parse().unwrap()); + + // let proxy_connector = ProxyConnector::from_proxy(https, proxy).unwrap(); + + // Create proxy client + // let client = Client::builder().build::<_, hyper::Body>(proxy_connector); + + // Send request + // let res = client.request(new_req).await?; + // Ok(res) + + // return Ok(Response::builder() + // .status(StatusCode::OK) + // .body(Body::from("Proxer does not currently support HTTPS proxies for `http://` connections; please use an HTTP proxy. Support: https://t.me/macproxer")) + // .unwrap()); + + let client = Client::new(); + let response = client.request(new_req).await?; + Ok(response) } } async fn tunnel( req: Request, - config: Arc>, + found_proxy: Option<(ProxyConfig, String)>, ) -> Result<(), Box> { + let options = Opt::parse(); + let addr = req.uri().authority().unwrap().to_string(); - let host = req.uri().host().unwrap(); let time = formatted_time(); - match find_matching_proxy(config.as_ref(), host) { - Some(proxy) => { - println!( - "\x1B[34m\x1B[1m[{time}] {} -> {} · {}\x1B[0m", - addr, proxy.name, proxy.scheme - ); + if let Some((proxy, filter_name)) = found_proxy { + println!( + "\x1B[34m\x1B[1m[{time}] {addr} → {} · {} · {filter_name}\x1B[0m", + proxy.name, proxy.scheme + ); - let proxy_addr = format!("{}:{}", proxy.host, proxy.port); - let proxy_user = proxy.auth_credentials.username; - let proxy_pass = proxy.auth_credentials.password; + let proxy_addr = format!("{}:{}", proxy.host, proxy.port); - let tcp_stream = - timeout(Duration::from_secs(10), TcpStream::connect(&proxy_addr)).await??; + let tcp_stream = + timeout(Duration::from_secs(10), TcpStream::connect(&proxy_addr)).await??; - match proxy.scheme.as_str() { - "HTTP" => { - handle_http_proxy(req, tcp_stream, &addr, &proxy_user, &proxy_pass).await?; - } - "HTTPS" => { - handle_https_proxy( - req, - tcp_stream, - &addr, - &proxy.host, - &proxy_user, - &proxy_pass, - ) - .await?; - } - _ => return Err(format!("Unsupported proxy scheme: {}", proxy.scheme).into()), + match proxy.scheme.as_str() { + "HTTP" => { + handle_http_proxy(req, tcp_stream, &addr, &proxy).await?; } - - return Ok(()); + "HTTPS" => { + handle_https_proxy(req, tcp_stream, &addr, &proxy).await?; + } + _ => return Err(format!("Unsupported proxy scheme: {}", proxy.scheme).into()), } - None => { - println!("[{time}] {} -> Direct connection", addr); - - // Connect to the server - let mut server = timeout(Duration::from_secs(10), TcpStream::connect(&addr)).await??; - - // Get the upgraded connection from the client - let upgraded = hyper::upgrade::on(req).await?; - let (mut client_reader, mut client_writer) = tokio::io::split(upgraded); - - // Buffer for reading the first bytes of Client Hello - let mut buffer = [0u8; 1024]; - let mut bytes_read = 0; - - // Read the first bytes of Client Hello - while bytes_read < 5 { - // Minimum length of TLS record - let n = client_reader.read(&mut buffer[bytes_read..]).await?; - if n == 0 { - return Err("Connection closed".into()); - } - bytes_read += n; + + return Ok(()); + } else { + println!("\x1b[1m[{time}] {addr} → Direct connection\x1b[0m"); + + // Connect to the server + let mut server = timeout(Duration::from_secs(10), TcpStream::connect(&addr)).await??; + + // Get the upgraded connection from the client + let upgraded = hyper::upgrade::on(req).await?; + let (mut client_reader, mut client_writer) = tokio::io::split(upgraded); + + // Buffer for reading the first bytes of Client Hello + let mut buffer = [0u8; 1024]; + let mut bytes_read = 0; + + // Read the first bytes of Client Hello + while bytes_read < 5 { + let n = client_reader.read(&mut buffer[bytes_read..]).await?; + + if n == 0 { + return Err("Connection closed".into()); } + bytes_read += n; + } + + if options.dpi { // Send the first byte to the server server.write_all(&buffer[..1]).await?; // Send the rest of the bytes to the server - server.write_all(&buffer[1..bytes_read]).await?; + server.write_all(&buffer[1..]).await?; + } else { + // Send the all bytes to the server + server.write_all(&buffer[..bytes_read]).await?; + } - // Split the server connection into reader and writer - let (mut server_reader, mut server_writer) = server.split(); + // Split the server connection into reader and writer + let (mut server_reader, mut server_writer) = server.split(); - // Copy data from client to server - let client_to_server = async { - tokio::io::copy(&mut client_reader, &mut server_writer).await?; - server_writer.shutdown().await - }; + // Copy data from client to server + let client_to_server = async { + tokio::io::copy(&mut client_reader, &mut server_writer).await?; + server_writer.shutdown().await + }; - // Copy data from server to client - let server_to_client = async { - tokio::io::copy(&mut server_reader, &mut client_writer).await?; - client_writer.shutdown().await - }; + // Copy data from server to client + let server_to_client = async { + tokio::io::copy(&mut server_reader, &mut client_writer).await?; + client_writer.shutdown().await + }; - // Wait for both tasks to complete - tokio::try_join!(client_to_server, server_to_client)?; + // Wait for both tasks to complete + tokio::try_join!(client_to_server, server_to_client)?; - return Ok(()); - } - }; + return Ok(()); + } } pub fn get_default_interface() -> String { @@ -235,6 +337,7 @@ fn package_info() -> Package { cargo.package } +// Kill all proxer processes pub fn terminate_proxer() { let _ = Command::new("sh") .args(&["-c", "kill $(pgrep proxer)"]) @@ -242,6 +345,7 @@ pub fn terminate_proxer() { .expect("Failed to execute `kill $(pgrep proxer)` command to terminate proxer processes"); } +// Check if the host is allowed by the allowed_hosts list fn is_host_allowed(req_host: &str, allowed_hosts: &[String]) -> bool { for allowed_host in allowed_hosts { if WildMatch::new(allowed_host).matches(req_host) { @@ -252,18 +356,35 @@ fn is_host_allowed(req_host: &str, allowed_hosts: &[String]) -> bool { } // Implement a function to load the configuration from a JSON file and search for a matching proxy -pub fn find_matching_proxy(config_file: &[ProxyConfig], req_host: &str) -> Option { - // Read the contents of the file proxer.json5 - for config in config_file { +pub fn find_matching_proxy( + config_file: &[ProxyConfig], + req_host: &str, +) -> Option<(ProxyConfig, String)> { + let mut host_cache = HOST_CACHE.lock().unwrap(); + // println!("{:?}", host_cache); + + // Check host in cache + if host_cache.contains(req_host) { + if let Some((config_index, filter_index)) = host_cache.get(req_host) { + let proxy_config = config_file[*config_index].clone(); + let filter_name = proxy_config.filter[*filter_index].name.clone(); + + return Some((proxy_config, filter_name)); + } + } + + for (config_index, proxy_config) in config_file.iter().enumerate() { // Skip disabled proxies - if !config.enabled { + if !proxy_config.enabled { continue; } // Find by filters - for filter in &config.filter { + for (filter_index, filter) in proxy_config.filter.iter().enumerate() { if is_host_allowed(req_host, &filter.domains) { - return Some(config.clone()); + // Add host to cache + host_cache.add(req_host.to_string(), config_index, filter_index); + return Some((proxy_config.clone(), filter.name.clone())); } } } @@ -274,17 +395,15 @@ pub fn find_matching_proxy(config_file: &[ProxyConfig], req_host: &str) -> Optio fn formatted_time() -> String { let now = Local::now(); now.format("%H:%M:%S").to_string() - // now.format("%Y-%m-%d %H:%M:%S").to_string() } async fn handle_http_proxy( req: Request, mut tcp_stream: TcpStream, addr: &str, - proxy_user: &str, - proxy_pass: &str, + proxy: &ProxyConfig, ) -> Result<(), Box> { - send_connect_request(req, &mut tcp_stream, addr, proxy_user, proxy_pass).await?; + send_connect_request(req, &mut tcp_stream, addr, &proxy).await?; Ok(()) } @@ -292,18 +411,16 @@ async fn handle_https_proxy( req: Request, tcp_stream: TcpStream, addr: &str, - proxy_host: &str, - proxy_user: &str, - proxy_pass: &str, + proxy: &ProxyConfig, ) -> Result<(), Box> { let connector = TlsConnector::from(native_tls::TlsConnector::new()?); let mut tls_stream = timeout( Duration::from_secs(10), - connector.connect(proxy_host, tcp_stream), + connector.connect(&proxy.host, tcp_stream), ) .await??; - send_connect_request(req, &mut tls_stream, addr, proxy_user, proxy_pass).await?; + send_connect_request(req, &mut tls_stream, addr, &proxy).await?; Ok(()) } @@ -311,24 +428,45 @@ async fn send_connect_request( req: Request, stream: &mut T, addr: &str, - proxy_user: &str, - proxy_pass: &str, + proxy: &ProxyConfig, ) -> Result<(), Box> where T: AsyncRead + AsyncWrite + Unpin, { + let options = Opt::parse(); let package = package_info(); - let auth = b64.encode(format!("{}:{}", proxy_user, proxy_pass)); - let connect_req = format!( - "CONNECT {} HTTP/1.1\r\n\ - Host: {}\r\n\ + + let mut connect_req = format!( + "CONNECT {addr} HTTP/1.1\r\n\ + Host: {addr}\r\n\ Proxy-Connection: Keep-Alive\r\n\ - Proxy-Authorization: Basic {}\r\n\ - User-Agent: {}/{}\r\n\ - \r\n", - addr, addr, auth, package.name, package.version + User-Agent: {}/{}\r\n", + package.name, package.version ); + // Get the auth credentials from the proxy config + let auth_username = proxy.auth_credentials.username.clone(); + let auth_password = proxy.auth_credentials.password.clone(); + + // Add auth credentials to the request if they are provided + if !auth_username.is_empty() && !auth_password.is_empty() { + connect_req.push_str(&format!( + "Proxy-Authorization: Basic {auth_credentials}\r\n", + auth_credentials = b64.encode(format!("{}:{}", auth_username, auth_password)) + )); + } + + // Add token to the request if it is provided + if let Some(token) = options.token.as_ref() { + if proxy.scheme.eq_ignore_ascii_case("http") { + connect_req.push_str(&format!("x-http-secret-token: {}\r\n", to_sha256(token))); + } else if proxy.scheme.eq_ignore_ascii_case("https") { + connect_req.push_str(&format!("x-https-secret-token: {}\r\n", to_sha256(token))); + } + } + + connect_req.push_str("\r\n"); + stream.write_all(connect_req.as_bytes()).await?; let mut response = [0u8; 1024]; @@ -370,7 +508,7 @@ impl Proxy { // Go through each proxy type and set server and port for proxy_type in proxy_types.iter() { - let command = format!("-set{}", proxy_type); + let command = format!("-set{proxy_type}"); let _ = self .execute_command(&[ @@ -379,19 +517,23 @@ impl Proxy { &self.server, &self.port.to_string(), ]) - .expect(&format!("Failed to set {}", proxy_type)); + .expect(&format!("Failed to set {proxy_type}")); } } - pub fn set_state(&self, state: &str) { + pub fn set_state(&self, state: ProxyState) { let proxy_types = self.get_proxy_types(); + let proxy_state = match state { + ProxyState::On => "on", + ProxyState::Off => "off", + }; for proxy_type in proxy_types.iter() { - let command = format!("-set{}state", proxy_type); + let command = format!("-set{proxy_type}state"); let _ = self - .execute_command(&[&command, &self.interface, state]) - .expect(&format!("Failed to set {} state", proxy_type)); + .execute_command(&[&command, &self.interface, proxy_state]) + .expect(&format!("Failed to set {proxy_type} state")); } } @@ -408,3 +550,69 @@ impl Proxy { String::from_utf8(output.stdout) } } + +pub fn to_sha256(input: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(input); + + let result = hasher.finalize(); + format!("{:x}", result) +} + +// pub async fn measure_ping(server_addr: &str) -> Result { +// let start = Instant::now(); + +// match TcpStream::connect(server_addr).await { +// Ok(_) => Ok(start.elapsed()), +// Err(e) => Err(e), +// } +// } + +// pub async fn ping_loop(server_addr: &str) { +// loop { +// match measure_ping(server_addr).await { +// Ok(duration) => println!("{server_addr} | Ping: {} ms", duration.as_millis()), +// Err(e) => eprintln!("Error connecting to server: {}", e), +// } + +// sleep(Duration::from_secs(5)).await; +// } +// } + +pub async fn measure_ping(proxy_config: &[ProxyConfig]) -> Result { + let start = Instant::now(); + + for proxy in proxy_config { + if proxy.enabled { + let address = format!("{}:{}", proxy.host, proxy.port); + + match TcpStream::connect(&address).await { + Ok(_) => { + println!("{address} | Ping: {} ms", start.elapsed().as_millis()); + return Ok(start.elapsed()); + } + Err(e) => { + eprintln!("Error connecting to {address}: {e}"); + return Err(e); + } + } + } + } + + Err(io::Error::new( + io::ErrorKind::NotFound, + "Unable to connect to any proxy server", + )) +} + +pub async fn ping_loop(proxy_config: &[ProxyConfig]) { + loop { + let _ = measure_ping(proxy_config).await; + // match measure_ping(proxy_config).await { + // Ok(duration) => println!("{server_addr} | Ping: {} ms", duration.as_millis()), + // Err(e) => eprintln!("Error connecting to server: {}", e), + // } + + sleep(Duration::from_secs(5)).await; + } +} diff --git a/src/main.rs b/src/main.rs index c519466..36cd6a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,45 +1,76 @@ +use clap::Parser; use futures::stream::StreamExt; use hyper::{ service::{make_service_fn, service_fn}, Server, }; +use options::Opt; use port_check::*; -use proxer::{get_default_interface, handle_request, terminate_proxer, Proxy, ProxyConfig}; +use proxer::{ + get_default_interface, handle_request, terminate_proxer, Proxy, ProxyConfig, ProxyState, +}; use signal_hook::consts::signal::*; use signal_hook_tokio::Signals; -use std::sync::Arc; use std::{fs, net::SocketAddr}; +use std::{process::exit, sync::Arc}; + +mod options; #[tokio::main] async fn main() -> Result<(), Box> { // Close all proxer processes terminate_proxer(); - // Read the config file - let config_file = "proxer.json5"; - let config = fs::read_to_string(config_file).expect("Error reading config file"); - let parsed_config: Vec = - json5::from_str(&config).expect("Error parsing config file"); + // Parse command line options + let options = Opt::parse(); + + if options.dpi { + println!("DPI Spoofing enabled"); + } + + // Read the config file or use the default one + let config_file = if let Some(config_file) = options.config { + println!("Using config file: {config_file}"); + config_file + } else { + println!("Using default config file ~/.proxer/config.json5"); + let home_dir = std::env::var("HOME").expect("$HOME environment variable not set"); + format!("{home_dir}/.proxer/config.json5") + }; + + let config = fs::read_to_string(&config_file) + .expect(format!("Failed to read config file: {config_file}").as_str()); + let parsed_config: Vec = json5::from_str(&config) + .expect(format!("Failed to parse config file: {config_file}").as_str()); let shared_config = Arc::new(parsed_config); + // Run ping proxy in loop + // tokio::spawn(async move { + // ping_loop(shared_config).await; + // }); + // Get the default interface let interface = get_default_interface(); // Check if the default interface is empty if interface.is_empty() { eprintln!("Failed to find default interface. If you have VPN enabled, try disabling it and running the program again."); - return Ok(()); + exit(1); } else { - println!("Default interface: {}", interface); + println!("Default interface: {interface}"); } - // Find a free port - let port = free_local_port().unwrap(); + // Get a free port or use the one specified from the command line + let port = if let Some(port) = options.port { + port + } else { + free_local_port().unwrap() + }; - // Create a system proxy with the default interface and the free port + // Create a system proxy with the default interface and port let system_proxy = Proxy::init(interface, "127.0.0.1", port); system_proxy.set(); - system_proxy.set_state("on"); + system_proxy.set_state(ProxyState::On); // Create an Arc around the system proxy let system_proxy_arc = Arc::new(system_proxy); @@ -53,7 +84,7 @@ async fn main() -> Result<(), Box> { while let Some(signal) = signals.next().await { match signal { SIGINT | SIGTERM | SIGQUIT => { - system_proxy_arc.set_state("off"); + system_proxy_arc.set_state(ProxyState::Off); std::process::exit(0); } _ => unreachable!(), @@ -67,6 +98,7 @@ async fn main() -> Result<(), Box> { // Create a service builder to handle incoming connections let make_svc = make_service_fn(move |_conn| { let config = Arc::clone(&shared_config); + async { Ok::<_, hyper::Error>(service_fn(move |req| { handle_request(req, Arc::clone(&config)) diff --git a/src/options.rs b/src/options.rs new file mode 100644 index 0000000..1b4875f --- /dev/null +++ b/src/options.rs @@ -0,0 +1,38 @@ +use clap::Parser; + +#[derive(Parser, Debug, Clone)] +#[clap(author, version, about, long_about = None)] +pub struct Opt { + #[clap( + long, + value_name = "u16", + help = "Set port for proxer. By default, a random port is used." + )] + pub port: Option, + + #[clap( + long, + help = "Enable DPI spoofing for direct connections. Spoofing is disabled by default." + )] + pub dpi: bool, + + #[clap( + long, + value_name = "string", + help = "Path to the configuration file. Example: '/path/to/proxer.(json5|json)'. Default is ~/.proxer/config.json5." + )] + pub config: Option, + + #[clap( + long, + value_name = "string", + help = "Secret token to access the HTTP/S proxerver. Must match the token specified in the proxerver configuration." + )] + pub token: Option, + + #[clap( + long, + help = "Show all errors. By default, only critical errors are shown. This option is useful for debugging." + )] + pub log_error_all: bool, +}