diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml
new file mode 100644
index 0000000..13820a8
--- /dev/null
+++ b/.github/workflows/build-linux.yml
@@ -0,0 +1,104 @@
+name: Build Linux
+
+on:
+ push:
+ branches: [ master, dev ]
+ pull_request:
+ branches: [ master, dev ]
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Install build dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ build-essential \
+ gcc \
+ make \
+ libnetfilter-queue-dev \
+ libnfnetlink-dev \
+ libgtk-3-dev \
+ pkg-config
+ shell: bash
+
+ - name: Verify dependencies
+ run: |
+ echo "=== Checking GCC ==="
+ gcc --version
+ echo ""
+ echo "=== Checking Make ==="
+ make --version
+ echo ""
+ echo "=== Checking pkg-config ==="
+ pkg-config --version
+ echo ""
+ echo "=== Checking GTK3 ==="
+ pkg-config --modversion gtk+-3.0
+ echo ""
+ echo "=== Checking libnetfilter_queue ==="
+ pkg-config --modversion libnetfilter_queue || echo "Package info not available, but headers should be present"
+ shell: bash
+
+ - name: Build project
+ run: |
+ cd Linux
+ chmod +x build.sh
+ ./build.sh
+ shell: bash
+
+ - name: Verify build output
+ run: |
+ echo "=== Build Output ==="
+ ls -lh Linux/output/
+ echo ""
+ if [ -f "Linux/output/libproxybridge.so" ]; then
+ echo "✓ Library built successfully"
+ file Linux/output/libproxybridge.so
+ else
+ echo "✗ Library build failed"
+ exit 1
+ fi
+ echo ""
+ if [ -f "Linux/output/ProxyBridge" ]; then
+ echo "✓ CLI built successfully"
+ file Linux/output/ProxyBridge
+ else
+ echo "✗ CLI build failed"
+ exit 1
+ fi
+ echo ""
+ if [ -f "Linux/output/ProxyBridgeGUI" ]; then
+ echo "✓ GUI built successfully"
+ file Linux/output/ProxyBridgeGUI
+ else
+ echo "⚠ GUI build skipped (GTK3 not available or build failed)"
+ fi
+ shell: bash
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: ProxyBridge-Linux-Build-${{ github.sha }}
+ path: Linux/output/
+ retention-days: 30
+
+ - name: Display build summary
+ run: |
+ echo ""
+ echo "========================================="
+ echo "Build Complete!"
+ echo "========================================="
+ cd Linux/output
+ for file in *; do
+ size=$(du -h "$file" | cut -f1)
+ echo " $file - $size"
+ done
+ echo "========================================="
+ shell: bash
diff --git a/.github/workflows/release-linux.yml b/.github/workflows/release-linux.yml
new file mode 100644
index 0000000..4e2f9b6
--- /dev/null
+++ b/.github/workflows/release-linux.yml
@@ -0,0 +1,129 @@
+name: Release ProxyBridge Linux
+
+on:
+ release:
+ types: [published, created]
+ workflow_dispatch:
+
+jobs:
+ build-and-release:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.actor == github.repository_owner)
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Install build dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ build-essential \
+ gcc \
+ make \
+ libnetfilter-queue-dev \
+ libnfnetlink-dev \
+ libgtk-3-dev \
+ pkg-config
+ shell: bash
+
+ - name: Build project
+ run: |
+ cd Linux
+ chmod +x build.sh
+ ./build.sh
+ shell: bash
+
+ - name: Copy setup script to output
+ run: |
+ echo "Copying setup.sh to output directory..."
+ cp Linux/setup.sh Linux/output/
+ chmod +x Linux/output/setup.sh
+ echo "✓ Setup script copied"
+ shell: bash
+
+ - name: Verify build output
+ run: |
+ echo "=== Build Output ==="
+ ls -lh Linux/output/
+ echo ""
+ if [ -f "Linux/output/libproxybridge.so" ]; then
+ echo "✓ Library built successfully"
+ file Linux/output/libproxybridge.so
+ else
+ echo "✗ Library build failed"
+ exit 1
+ fi
+ echo ""
+ if [ -f "Linux/output/ProxyBridge" ]; then
+ echo "✓ CLI built successfully"
+ file Linux/output/ProxyBridge
+ else
+ echo "✗ CLI build failed"
+ exit 1
+ fi
+ echo ""
+ if [ -f "Linux/output/setup.sh" ]; then
+ echo "✓ Setup script copied"
+ else
+ echo "✗ Setup script missing"
+ exit 1
+ fi
+ shell: bash
+
+ - name: Extract version from tag
+ id: version
+ run: |
+ if [ "${{ github.event_name }}" == "release" ]; then
+ VERSION="${{ github.event.release.tag_name }}"
+ else
+ VERSION="dev-$(date +%Y%m%d-%H%M%S)"
+ fi
+ # Remove 'v' prefix if present
+ VERSION="${VERSION#v}"
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ echo "Version: $VERSION"
+ shell: bash
+
+ - name: Create release archive
+ run: |
+ VERSION="${{ steps.version.outputs.version }}"
+ ARCHIVE_NAME="ProxyBridge-Linux-v${VERSION}.tar.gz"
+
+ echo "Creating archive: $ARCHIVE_NAME"
+ cd Linux/output
+ tar -czf "../$ARCHIVE_NAME" ./*
+
+ echo ""
+ echo "Archive created successfully:"
+ ls -lh "../$ARCHIVE_NAME"
+
+ # Move archive to root for upload
+ mv "../$ARCHIVE_NAME" "../../$ARCHIVE_NAME"
+ shell: bash
+
+ - name: List release files
+ run: |
+ echo ""
+ echo "==================================="
+ echo "Release Files:"
+ echo "==================================="
+ ls -lh ProxyBridge-Linux-*.tar.gz
+
+ echo ""
+ echo "Archive contents:"
+ tar -tzf ProxyBridge-Linux-*.tar.gz
+ shell: bash
+
+ - name: Upload archive to release
+ if: github.event_name == 'release'
+ uses: softprops/action-gh-release@v1
+ with:
+ files: ProxyBridge-Linux-v*.tar.gz
+
+ - name: Upload build artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: ProxyBridge-Linux-Release-${{ steps.version.outputs.version }}
+ path: ProxyBridge-Linux-*.tar.gz
+ retention-days: 90
diff --git a/.github/workflows/release-windows.yml b/.github/workflows/release-windows.yml
index f3b1dfc..cfd1307 100644
--- a/.github/workflows/release-windows.yml
+++ b/.github/workflows/release-windows.yml
@@ -2,11 +2,13 @@ name: Release ProxyBridge Windows
on:
release:
- types: [created]
+ types: [published, created]
+ workflow_dispatch:
jobs:
build-and-release:
runs-on: self-hosted
+ if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.actor == github.repository_owner)
steps:
- name: Checkout code
@@ -15,7 +17,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
- dotnet-version: '9.0.x'
+ dotnet-version: '10.0.x'
- name: Verify WinDivert installation
run: |
diff --git a/.gitignore b/.gitignore
index bbf80e0..3a0bbe7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -95,3 +95,7 @@ Linux/output/**
Linux/build/**
MacOS/ProxyBridge/config/Signing-Config-ext.xcconfig
MacOS/ProxyBridge/config/Signing-Config-app.xcconfig
+Linux/cli/main.o
+Linux/cli/proxybridge-cli
+Linux/src/libproxybridge.so
+Linux/src/ProxyBridge.o
diff --git a/Linux/README.md b/Linux/README.md
new file mode 100644
index 0000000..11ef83f
--- /dev/null
+++ b/Linux/README.md
@@ -0,0 +1,590 @@
+# ProxyBridge for Linux
+
+Universal proxy client for Linux applications - Route any application through SOCKS5/HTTP proxies.
+
+## Table of Contents
+
+- [Installation](#installation)
+ - [Quick Install (Recommended)](#quick-install-recommended)
+ - [Manual Installation](#manual-installation)
+- [Usage](#usage)
+ - [GUI Application](#gui-application)
+ - [Command Line Interface (CLI)](#command-line-interface-cli)
+ - [Basic Usage](#basic-usage)
+ - [Command Line Options](#command-line-options)
+ - [Rule Format](#rule-format)
+- [Use Cases](#use-cases)
+- [Current Limitations](#current-limitations)
+- [Things to Note](#things-to-note)
+- [How It Works](#how-it-works)
+- [Build from Source](#build-from-source)
+- [Uninstallation](#uninstallation)
+- [License](#license)
+
+## Installation
+
+### Quick Install (Recommended)
+
+**One-command automatic installation:**
+
+```bash
+curl -Lo deploy.sh https://raw.githubusercontent.com/InterceptSuite/ProxyBridge/refs/heads/master/Linux/deploy.sh && sudo bash deploy.sh
+```
+
+This script will:
+- Download the latest release from GitHub automatically
+- Detect your Linux distribution
+- Install all required dependencies (libnetfilter-queue, iptables, GTK3)
+- Install ProxyBridge CLI, GUI, and library to system paths
+- Update library cache (ldconfig)
+- Verify installation
+
+**Supported distributions:**
+- Debian/Ubuntu/Mint/Pop!_OS/Elementary/Zorin/Kali/Raspbian
+- Fedora
+- RHEL/CentOS/Rocky/AlmaLinux
+- Arch/Manjaro/EndeavourOS/Garuda
+- openSUSE/SLES
+- Void Linux
+
+### Manual Installation
+
+**From GitHub Releases:**
+
+1. Download the latest `ProxyBridge-Linux-vX.X.X.tar.gz` from the [Releases](https://github.com/InterceptSuite/ProxyBridge/releases) page
+2. Extract the archive:
+ ```bash
+ tar -xzf ProxyBridge-Linux-vX.X.X.tar.gz
+ cd ProxyBridge-Linux-vX.X.X
+ ```
+3. Run the setup script with root privileges:
+ ```bash
+ sudo ./setup.sh
+ ```
+
+The setup script will:
+- Install runtime dependencies (libnetfilter-queue, iptables, GTK3)
+- Copy binaries to `/usr/local/bin` (ProxyBridge CLI and GUI)
+- Copy library to `/usr/local/lib` (libproxybridge.so)
+- Update library cache
+- Verify installation
+
+**What gets installed:**
+- `/usr/local/bin/ProxyBridge` - Command-line interface
+- `/usr/local/bin/ProxyBridgeGUI` - Graphical interface (if GTK3 available)
+- `/usr/local/lib/libproxybridge.so` - Core library
+- `/etc/proxybridge/` - Configuration directory
+
+## Usage
+
+### GUI Application
+
+**Launch GUI (requires GTK3):**
+
+```bash
+sudo ProxyBridgeGUI
+```
+
+The GTK3-based GUI provides:
+- Proxy configuration (SOCKS5/HTTP with authentication)
+- Visual rule management with process selection
+- Real-time connection monitoring
+- Import/Export rules (JSON format compatible with Windows/macOS)
+- DNS via Proxy toggle
+
+
+### Command Line Interface (CLI)
+
+The CLI provides powerful automation and scripting capabilities with rule-based traffic control.
+
+#### Basic Usage
+
+```bash
+# Help menu
+ProxyBridge --help
+
+# Route curl through SOCKS5 proxy
+sudo ProxyBridge --proxy socks5://127.0.0.1:1080 --rule "curl:*:*:TCP:PROXY"
+
+# Route multiple processes in single rule (semicolon-separated)
+sudo ProxyBridge --proxy http://127.0.0.1:8080 --rule "curl;wget;firefox:*:*:TCP:PROXY"
+
+# Multiple rules with verbose connection logging
+sudo ProxyBridge --proxy http://127.0.0.1:8080 \
+ --rule "curl:*:*:TCP:PROXY" \
+ --rule "wget:*:*:TCP:PROXY" \
+ --verbose 2
+
+# Block specific application from internet access
+sudo ProxyBridge --rule "malware:*:*:BOTH:BLOCK"
+
+# Route specific apps through proxy, block everything else
+sudo ProxyBridge --proxy socks5://127.0.0.1:1080 \
+ --rule "curl:*:*:TCP:PROXY" \
+ --rule "firefox:*:*:TCP:PROXY" \
+ --rule "*:*:*:BOTH:BLOCK"
+
+# Route all through proxy except proxy app itself
+sudo ProxyBridge --proxy socks5://127.0.0.1:1080 \
+ --rule "*:*:*:TCP:PROXY" \
+ --rule "burpsuite:*:*:TCP:DIRECT"
+
+# Target specific IPs and ports
+sudo ProxyBridge --proxy socks5://127.0.0.1:1080 \
+ --rule "curl:192.168.*.*;10.10.*.*:80;443;8080:TCP:PROXY"
+
+# IP range support
+sudo ProxyBridge --proxy socks5://192.168.1.4:4444 \
+ --rule "curl:3.19.110.0-3.19.115.255:*:TCP:PROXY"
+
+# Cleanup after crash (removes iptables rules)
+sudo ProxyBridge --cleanup
+```
+
+#### Command Line Options
+
+```
+sudo ProxyBridge --help
+
+ ____ ____ _ _
+ | _ \ _ __ _____ ___ _ | __ ) _ __(_) __| | __ _ ___
+ | |_) | '__/ _ \ \/ / | | | | _ \| '__| |/ _` |/ _` |/ _ \
+ | __/| | | (_) > <| |_| | | |_) | | | | (_| | (_| | __/
+ |_| |_| \___/_/\_\\__, | |____/|_| |_|\__,_|\__, |\___|
+ |___/ |___/ V3.2.0
+
+ Universal proxy client for Linux applications
+
+ Author: Sourav Kalal/InterceptSuite
+ GitHub: https://github.com/InterceptSuite/ProxyBridge
+
+USAGE:
+ ProxyBridge [OPTIONS]
+
+OPTIONS:
+ --proxy Proxy server URL with optional authentication
+ Format: type://ip:port or type://ip:port:username:password
+ Examples: socks5://127.0.0.1:1080
+ http://proxy.com:8080:myuser:mypass
+ Default: socks5://127.0.0.1:4444
+
+ --rule Traffic routing rule (can be specified multiple times)
+ Format: process:hosts:ports:protocol:action
+ process - Process name(s): curl, cur*, *, or multiple separated by ;
+ hosts - IP/host(s): *, google.com, 192.168.*.*, or multiple separated by ; or ,
+ ports - Port(s): *, 443, 80;8080, 80-100, or multiple separated by ; or ,
+ protocol - TCP, UDP, or BOTH
+ action - PROXY, DIRECT, or BLOCK
+ Examples:
+ curl:*:*:TCP:PROXY
+ curl;wget:*:*:TCP:PROXY
+ *:*:53:UDP:PROXY
+ firefox:*:80;443:TCP:DIRECT
+
+ --dns-via-proxy Route DNS queries through proxy
+ Values: true, false, 1, 0
+ Default: true
+
+ --verbose Logging verbosity level
+ 0 - No logs (default)
+ 1 - Show log messages only
+ 2 - Show connection events only
+ 3 - Show both logs and connections
+
+ --cleanup Cleanup resources (iptables, etc.) from crashed instance
+ Use if ProxyBridge crashed without proper cleanup
+
+ --help, -h Show this help message
+
+EXAMPLES:
+ # Basic usage with default proxy
+ sudo ProxyBridge --rule curl:*:*:TCP:PROXY
+
+ # Multiple rules with custom proxy
+ sudo ProxyBridge --proxy socks5://192.168.1.10:1080 \
+ --rule curl:*:*:TCP:PROXY \
+ --rule wget:*:*:TCP:PROXY \
+ --verbose 2
+
+ # Route DNS through proxy with multiple apps
+ sudo ProxyBridge --proxy socks5://127.0.0.1:1080 \
+ --rule "curl;wget;firefox:*:*:BOTH:PROXY" \
+ --dns-via-proxy true --verbose 3
+
+NOTE:
+ ProxyBridge requires root privileges to use nfqueue.
+ Run with 'sudo' or as root user.
+
+```
+
+#### Rule Format
+
+**Format:** `process:hosts:ports:protocol:action`
+
+- **process** - Process name(s): `curl`, `curl;wget;firefox`, `fire*`, or `*`
+- **hosts** - Target IP/hostname(s): `*`, `192.168.1.1`, `192.168.*.*`, `10.10.1.1-10.10.255.255`, or `192.168.1.1;10.10.10.10`
+- **ports** - Target port(s): `*`, `443`, `80;443;8080`, `80-8000`, or `80;443;8000-9000`
+- **protocol** - `TCP`, `UDP`, or `BOTH`
+- **action** - `PROXY`, `DIRECT`, or `BLOCK`
+
+**Examples:**
+```bash
+# Single process to proxy
+--rule "curl:*:*:TCP:PROXY"
+
+# Multiple processes in one rule
+--rule "curl;wget;firefox:*:*:TCP:PROXY"
+
+# Wildcard process names
+--rule "fire*:*:*:TCP:PROXY" # Matches firefox, firebird, etc.
+
+# Target specific IPs and ports
+--rule "curl:192.168.*;10.10.*.*:80;443;8080:TCP:PROXY"
+
+# IP range matching
+--rule "curl:3.19.110.0-3.19.115.255:*:TCP:PROXY"
+
+# Allow direct connection (bypass proxy)
+--rule "burpsuite:*:*:TCP:DIRECT"
+```
+
+**Notes:**
+- Process names are case-sensitive on Linux
+- Use `*` as the process name to set a default action for all traffic
+- Press `Ctrl+C` to stop ProxyBridge
+- On crash, use `--cleanup` flag to remove iptables rules
+
+## Use Cases
+
+- Redirect proxy-unaware applications (games, desktop apps) through InterceptSuite/Burp Suite for security testing
+- Route specific applications through Tor, SOCKS5, or HTTP proxies
+- Intercept and analyze traffic from applications that don't support proxy configuration
+- Test application behavior under different network conditions
+- Analyze protocols and communication patterns
+- Penetration testing of Linux applications
+- Network traffic monitoring and debugging
+
+## Current Limitations
+
+- **IPv4 only** (IPv6 not supported)
+- **WSL (Windows Subsystem for Linux) NOT supported** - Neither WSL1 nor WSL2 work with ProxyBridge:
+ - **WSL2**: Kernel does not support `nfnetlink_queue` module
+ - Extension shows as "builtin" but is non-functional at runtime
+ - NFQUEUE handle creation fails
+ - **WSL1**: Uses a translation layer instead of a real Linux kernel
+ - Does not support Netfilter/iptables properly
+ - NFQUEUE is completely unavailable
+ - **Works on:** Native Linux, VirtualBox VMs, VMware, cloud instances (AWS, GCP, Azure), bare-metal servers
+ - **Does NOT work on:** WSL1, WSL2, Docker containers with limited capabilities
+ - **Windows users:** Use the Windows version of ProxyBridge instead - it's specifically designed for Windows and works perfectly
+- **Root privileges required** - NFQUEUE and iptables require root access
+- **GUI requires GTK3** - Command-line interface works without GUI dependencies
+
+## Things to Note
+
+- **DNS Traffic Handling**: DNS traffic on TCP and UDP port 53 is handled separately from proxy rules. Even if you configure rules for port 53, they will be ignored. Instead, DNS routing is controlled by the `--dns-via-proxy` flag (enabled by default). When enabled, all DNS queries are routed through the proxy; when disabled, DNS queries use direct connection.
+
+
+- **Automatic Direct Routing**: Certain IP addresses and ports automatically use direct connection regardless of proxy rules:
+ - **Broadcast addresses** (255.255.255.255 and x.x.x.255) - Network broadcast
+ - **Multicast addresses** (224.0.0.0 - 239.255.255.255) - Group communication
+ - **APIPA addresses** (169.254.0.0/16) - Automatic Private IP Addressing (link-local)
+ - **DHCP ports** (UDP 67, 68) - Dynamic Host Configuration Protocol
+
+ These addresses and ports are used by system components, network discovery, and essential Linux services.
+
+- **UDP Proxy Requirements**: UDP traffic only works when a SOCKS5 proxy is configured. If an HTTP proxy server is configured, ProxyBridge will ignore UDP proxy rules and route UDP traffic as direct connection instead. This limitation does not affect UDP rules with BLOCK or DIRECT actions.
+
+ **Important UDP Considerations**:
+ - Configuring a SOCKS5 proxy does not guarantee UDP will work. Most SOCKS5 proxies do not support UDP traffic, including SSH SOCKS5 proxies.
+ - The SOCKS5 proxy must support UDP ASSOCIATE command. If ProxyBridge fails to establish a UDP association with the SOCKS5 proxy, packets will fail to connect.
+ - Many UDP applications use HTTP/3 and DTLS protocols. Even if your SOCKS5 proxy supports UDP ASSOCIATE, ensure it can handle DTLS and HTTP/3 UDP traffic, as they require separate handling beyond raw UDP packets.
+ - **Testing UDP/HTTP3/DTLS Support**: If you need to test UDP, HTTP/3, and DTLS support with a SOCKS5 proxy, try [Nexus Proxy](https://github.com/InterceptSuite/nexus-proxy) - a proxy application created specifically to test ProxyBridge with advanced UDP protocols.
+
+- **Root Privileges**: ProxyBridge requires root access to:
+ - Create NFQUEUE handles for packet interception
+ - Add/remove iptables rules in mangle and nat tables
+ - Listen on relay ports (34010 for TCP, 34011 for UDP)
+
+ Always run with `sudo` or as root user.
+
+- **Process Name Matching**: Linux process names are case-sensitive. Use exact process names or wildcard patterns:
+ - Exact: `firefox` matches only "firefox"
+ - Wildcard: `fire*` matches "firefox", "firebird", "firestorm", etc.
+ - All: `*` matches all processes
+
+## How It Works
+
+ProxyBridge uses Linux Netfilter NFQUEUE to intercept TCP/UDP packets and applies user-defined rules to route traffic through proxies.
+
+**Case 1: Packet does not match any rules**
+
+```
+Application → TCP/UDP Packet → NFQUEUE → ProxyBridge
+ ↓
+ [No Match or DIRECT]
+ ↓
+ Packet Verdict: ACCEPT
+ ↓
+ Direct Connection → Internet
+```
+
+**Case 2: Packet matches proxy rule**
+
+```
+Application → TCP/UDP Packet → NFQUEUE → ProxyBridge
+ ↓
+ [PROXY Rule Match]
+ ↓
+ Packet Verdict: ACCEPT + Mark
+ ↓
+ iptables NAT REDIRECT
+ ↓
+ Relay Server (34010/34011) ← Packet
+ ↓
+ [Store Original Destination]
+ ↓
+ SOCKS5/HTTP Protocol Conversion
+ ↓
+ Proxy Server (Burp Suite/InterceptSuite)
+ ↓
+ Forward to Original Destination
+ ↓
+ Internet
+ ↓
+ Response Returns
+ ↓
+ Relay Server
+ ↓
+ [Restore Original Source IP/Port]
+ ↓
+ Application Receives Response
+```
+
+**Detailed Traffic Flow:**
+
+1. **Applications Generate Traffic** - User-mode applications (curl, wget, firefox, games) create TCP/UDP packets
+
+2. **Kernel Interception** - iptables rules in the mangle table send packets to NFQUEUE:
+ ```bash
+ iptables -t mangle -A OUTPUT -p tcp -j NFQUEUE --queue-num 0
+ iptables -t mangle -A OUTPUT -p udp -j NFQUEUE --queue-num 0
+ ```
+
+3. **NFQUEUE Delivery** - libnetfilter_queue delivers packets to ProxyBridge in userspace
+
+4. **Rule Evaluation** - ProxyBridge inspects each packet and applies configured rules:
+ - **BLOCK** → Packet verdict: DROP (no network access)
+ - **DIRECT** → Packet verdict: ACCEPT (direct connection)
+ - **NO MATCH** → Packet verdict: ACCEPT (direct connection)
+ - **PROXY** → Packet verdict: ACCEPT + set mark (1 for TCP, 2 for UDP)
+
+5. **NAT Redirection** - For PROXY-matched packets, iptables NAT rules redirect marked packets:
+ ```bash
+ iptables -t nat -A OUTPUT -p tcp -m mark --mark 1 -j REDIRECT --to-port 34010
+ iptables -t nat -A OUTPUT -p udp -m mark --mark 2 -j REDIRECT --to-port 34011
+ ```
+
+6. **Relay Servers** - Local relay servers (34010 for TCP, 34011 for UDP):
+ - Intercept redirected packets using getsockopt(SO_ORIGINAL_DST)
+ - Store original destination IP and port
+ - Convert raw TCP/UDP to SOCKS5/HTTP proxy protocol
+ - Perform proxy authentication if configured
+ - Forward to configured proxy server
+
+7. **Proxy Forwarding** - Proxy server (Burp Suite/InterceptSuite) forwards traffic to the original destination
+
+8. **Response Handling** - Return traffic flows back through relay servers, which restore original source IP/port
+
+**Key Technical Points:**
+
+- **NFQUEUE vs WinDivert**: Linux uses Netfilter NFQUEUE (kernel feature) instead of WinDivert (Windows kernel driver)
+
+- **Why NFQUEUE Instead of eBPF**: While eBPF is the modern approach for Linux networking tasks and offers better performance, ProxyBridge uses NFQUEUE due to fundamental limitations discovered during development:
+ - **Original Plan**: ProxyBridge 3.1.0 for Linux was initially developed using eBPF after weeks of implementation
+ - **eBPF Memory Limitations**: eBPF provides limited memory space, which proved insufficient for ProxyBridge's feature set:
+ - ProxyBridge supports multiple complex proxy rules with wildcard matching, IP ranges, and process patterns
+ - Storing and evaluating these rules within eBPF's memory constraints was not feasible
+ - Alternative workarounds added excessive latency (200-500ms+ per packet)
+ - **Performance Requirements**: ProxyBridge's core design goals couldn't be met with eBPF:
+ - Work efficiently with minimal memory usage under high load
+ - Handle high traffic volumes (10,000+ concurrent connections)
+ - **Network speed impact must be ≤2-5% for proxied traffic only**
+ - **Zero performance impact on direct (non-proxied) traffic**
+ - eBPF implementation with all required features caused 15-30% slowdown on all traffic
+ - **NFQUEUE Advantages**:
+ - Userspace processing allows unlimited memory for complex rule evaluation
+ - Selective packet inspection - only examines packets, doesn't slow down uninspected traffic
+ - Mature, stable kernel interface available on all Linux distributions
+ - Lower latency than eBPF workarounds when handling complex rule sets
+
+ **Verdict**: NFQUEUE provides the right balance of flexibility, performance, and compatibility for ProxyBridge's requirements. While eBPF excels for simple packet filtering, ProxyBridge's advanced rule engine and protocol conversion needs are better served by NFQUEUE's userspace processing model.
+
+- **Packet Marking**: ProxyBridge marks packets (mark=1 for TCP, mark=2 for UDP) instead of modifying destination
+- **iptables Integration**: Uses mangle table (pre-routing processing) + nat table (port redirection)
+- **Transparent Redirection**: Applications remain completely unaware of proxying
+- **SO_ORIGINAL_DST**: Socket option retrieves original destination after NAT redirect
+- **Multi-threaded**: Separate threads for packet processing, TCP relay, UDP relay, and connection cleanup
+
+**Architecture Advantages:**
+
+- No kernel modules required (uses built-in Netfilter)
+- Works on any Linux distribution with iptables and NFQUEUE support
+- Leverages proven kernel infrastructure (Netfilter/iptables)
+- Separate packet marking prevents packet modification in NFQUEUE
+- Clean separation: NFQUEUE for inspection, iptables for redirection
+
+## Build from Source
+
+### Requirements
+
+- Linux kernel with NFQUEUE support (virtually all modern distributions)
+- GCC compiler
+- Make
+- Development libraries:
+ - libnetfilter-queue-dev (Debian/Ubuntu) or libnetfilter_queue-devel (Fedora/RHEL)
+ - libnfnetlink-dev (Debian/Ubuntu) or libnfnetlink-devel (Fedora/RHEL)
+ - libgtk-3-dev (optional, for GUI) or gtk3-devel (Fedora/RHEL)
+ - pkg-config
+
+### Building
+
+1. **Install build dependencies:**
+
+ **Debian/Ubuntu/Mint:**
+ ```bash
+ sudo apt-get update
+ sudo apt-get install build-essential gcc make \
+ libnetfilter-queue-dev libnfnetlink-dev \
+ libgtk-3-dev pkg-config
+ ```
+
+ **Fedora:**
+ ```bash
+ sudo dnf install gcc make \
+ libnetfilter_queue-devel libnfnetlink-devel \
+ gtk3-devel pkg-config
+ ```
+
+ **Arch/Manjaro:**
+ ```bash
+ sudo pacman -S base-devel gcc make \
+ libnetfilter_queue libnfnetlink \
+ gtk3 pkg-config
+ ```
+
+2. **Clone or download ProxyBridge source code:**
+ ```bash
+ git clone https://github.com/InterceptSuite/ProxyBridge.git
+ cd ProxyBridge/Linux
+ ```
+
+3. **Build using the build script:**
+ ```bash
+ chmod +x build.sh
+ ./build.sh
+ ```
+
+ This will compile:
+ - `libproxybridge.so` - Core library
+ - `ProxyBridge` - CLI application
+ - `ProxyBridgeGUI` - GUI application (if GTK3 is available)
+
+4. **Install to system paths:**
+ ```bash
+ sudo ./setup.sh
+ ```
+
+ Or manually copy binaries:
+ ```bash
+ sudo cp output/libproxybridge.so /usr/local/lib/
+ sudo cp output/ProxyBridge /usr/local/bin/
+ sudo cp output/ProxyBridgeGUI /usr/local/bin/ # If GUI was built
+ sudo ldconfig
+ ```
+
+### Build Output
+
+After successful build, binaries will be in the `output/` directory:
+- `output/libproxybridge.so` - Shared library
+- `output/ProxyBridge` - CLI binary
+- `output/ProxyBridgeGUI` - GUI binary (if GTK3 available)
+
+### Manual Compilation
+
+**Library:**
+```bash
+cd src
+gcc -Wall -Wextra -O3 -fPIC -D_GNU_SOURCE \
+ -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE \
+ -Wformat -Wformat-security -Werror=format-security \
+ -fno-strict-overflow -fno-delete-null-pointer-checks -fwrapv \
+ -c ProxyBridge.c -o ProxyBridge.o
+
+gcc -shared -Wl,-z,relro,-z,now -Wl,-z,noexecstack -s \
+ -o libproxybridge.so ProxyBridge.o \
+ -lpthread -lnetfilter_queue -lnfnetlink
+```
+
+**CLI:**
+```bash
+cd cli
+gcc -Wall -Wextra -O3 -I../src \
+ -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE \
+ -Wformat -Wformat-security -Werror=format-security \
+ -fno-strict-overflow -fno-delete-null-pointer-checks -fwrapv \
+ -c main.c -o main.o
+
+gcc -o ProxyBridge main.o \
+ -L../src -pie -Wl,-z,relro,-z,now -Wl,-z,noexecstack -s \
+ -Wl,-rpath,/usr/local/lib \
+ -lproxybridge -lpthread
+```
+
+## Uninstallation
+
+To remove ProxyBridge from your system:
+
+```bash
+# Remove binaries
+sudo rm -f /usr/local/bin/ProxyBridge
+sudo rm -f /usr/local/bin/ProxyBridgeGUI
+
+# Remove library
+sudo rm -f /usr/local/lib/libproxybridge.so
+
+# Remove configuration
+sudo rm -rf /etc/proxybridge
+
+# Update library cache
+sudo ldconfig
+
+# Remove ld.so.conf entry (if exists)
+sudo rm -f /etc/ld.so.conf.d/proxybridge.conf
+sudo ldconfig
+```
+
+**Cleanup after crash:**
+
+If ProxyBridge crashed and left iptables rules:
+```bash
+sudo ProxyBridge --cleanup
+```
+
+Or manually remove iptables rules:
+```bash
+sudo iptables -t mangle -D OUTPUT -p tcp -j NFQUEUE --queue-num 0
+sudo iptables -t mangle -D OUTPUT -p udp -j NFQUEUE --queue-num 0
+sudo iptables -t nat -D OUTPUT -p tcp -m mark --mark 1 -j REDIRECT --to-port 34010
+sudo iptables -t nat -D OUTPUT -p udp -m mark --mark 2 -j REDIRECT --to-port 34011
+```
+
+## License
+
+MIT License - See LICENSE file for details
+
+---
+
+**Author:** Sourav Kalal / InterceptSuite
+**GitHub:** https://github.com/InterceptSuite/ProxyBridge
+**Documentation:** https://github.com/InterceptSuite/ProxyBridge/tree/master/Linux
diff --git a/Linux/build.sh b/Linux/build.sh
new file mode 100755
index 0000000..6d5f49b
--- /dev/null
+++ b/Linux/build.sh
@@ -0,0 +1,101 @@
+#!/bin/bash
+
+set -e # Exit on error
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+OUTPUT_DIR="$SCRIPT_DIR/output"
+
+# Remove and recreate output directory
+if [ -d "$OUTPUT_DIR" ]; then
+ echo "Removing existing output directory..."
+ rm -rf "$OUTPUT_DIR"
+fi
+
+echo "Creating output directory..."
+mkdir -p "$OUTPUT_DIR"
+echo ""
+
+# Build library
+echo "=== Building Library ==="
+cd "$SCRIPT_DIR/src"
+make clean 2>/dev/null || true
+make
+
+if [ -f "libproxybridge.so" ]; then
+ echo "Library build successful"
+else
+ echo "Library build failed!"
+ exit 1
+fi
+echo ""
+
+# Build CLI (library must stay for linking)
+echo "=== Building CLI ==="
+cd "$SCRIPT_DIR/cli"
+make clean 2>/dev/null || true
+make
+
+if [ -f "ProxyBridge" ]; then
+ echo "CLI build successful"
+else
+ echo "CLI build failed!"
+ exit 1
+fi
+echo ""
+
+# Move binaries to output
+echo "=== Building GUI ==="
+cd "$SCRIPT_DIR"
+rm -f ProxyBridgeGUI
+if pkg-config --exists gtk+-3.0; then
+ GUI_CFLAGS="-Wall -Wno-unused-parameter -O3 -Isrc -D_GNU_SOURCE -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE -Wformat -Wformat-security -Werror=format-security -fno-strict-overflow -fno-delete-null-pointer-checks -fwrapv $(pkg-config --cflags gtk+-3.0)"
+ GUI_LDFLAGS="-Lsrc -pie -Wl,-z,relro,-z,now -Wl,-z,noexecstack -s -Wl,-rpath,'$ORIGIN/.' -lproxybridge -lpthread $(pkg-config --libs gtk+-3.0) -export-dynamic"
+
+ # Compile all GUI source files
+ GUI_OBJS=""
+ for src in gui/*.c; do
+ obj="${src%.c}.o"
+ gcc $GUI_CFLAGS -c "$src" -o "$obj"
+ GUI_OBJS="$GUI_OBJS $obj"
+ done
+
+ gcc -o ProxyBridgeGUI $GUI_OBJS $GUI_LDFLAGS
+
+ rm -f gui/*.o
+ echo "GUI build successful"
+else
+ echo "GTK3 not found. Skipping GUI build."
+ echo " Install with: sudo apt install libgtk-3-dev (Debian/Ubuntu/Mint)"
+ echo " sudo dnf install gtk3-devel (Fedora)"
+fi
+
+echo ""
+echo "Moving binaries to output directory..."
+mv "$SCRIPT_DIR/src/libproxybridge.so" "$OUTPUT_DIR/"
+mv "$SCRIPT_DIR/cli/ProxyBridge" "$OUTPUT_DIR/"
+if [ -f ProxyBridgeGUI ]; then
+ mv ProxyBridgeGUI "$OUTPUT_DIR/"
+fi
+echo "Binaries moved to output"
+echo ""
+
+# Cleanup build files
+echo "Cleaning up build artifacts..."
+cd "$SCRIPT_DIR/src"
+rm -f *.o
+make clean 2>/dev/null || true
+cd "$SCRIPT_DIR/cli"
+rm -f *.o
+make clean 2>/dev/null || true
+echo "Cleanup complete"
+echo ""
+
+# Show results
+echo "==================================="
+echo "Build Complete!"
+echo "==================================="
+cd "$OUTPUT_DIR"
+ls -lh
+echo ""
+echo "Output location: $OUTPUT_DIR"
+
diff --git a/Linux/cli/Makefile b/Linux/cli/Makefile
new file mode 100644
index 0000000..6aff0ac
--- /dev/null
+++ b/Linux/cli/Makefile
@@ -0,0 +1,32 @@
+CC = gcc
+CFLAGS = -Wall -Wextra -O3 -I../src \
+ -fstack-protector-strong \
+ -D_FORTIFY_SOURCE=2 \
+ -fPIE \
+ -Wformat -Wformat-security \
+ -Werror=format-security \
+ -fno-strict-overflow \
+ -fno-delete-null-pointer-checks \
+ -fwrapv
+LDFLAGS = -L../src -pie -Wl,-z,relro,-z,now -Wl,-z,noexecstack -s \
+ -Wl,-rpath,/usr/local/lib
+LIBS = -lproxybridge -lpthread
+
+TARGET = ProxyBridge
+SOURCES = main.c
+OBJECTS = $(SOURCES:.c=.o)
+
+all: $(TARGET)
+
+$(TARGET): $(OBJECTS)
+ $(CC) -o $@ $^ $(LDFLAGS) $(LIBS)
+ @echo "Stripping debug symbols..."
+ strip --strip-all $@
+
+%.o: %.c
+ $(CC) $(CFLAGS) -c $< -o $@
+
+clean:
+ rm -f $(OBJECTS) $(TARGET)
+
+.PHONY: all clean
diff --git a/Linux/cli/main.c b/Linux/cli/main.c
new file mode 100644
index 0000000..45a540a
--- /dev/null
+++ b/Linux/cli/main.c
@@ -0,0 +1,496 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "../src/ProxyBridge.h"
+
+#define MAX_RULES 100
+#define MAX_RULE_STR 512
+
+typedef struct {
+ char process_name[256];
+ char target_hosts[256];
+ char target_ports[256];
+ RuleProtocol protocol;
+ RuleAction action;
+} ProxyRule;
+
+static volatile bool keep_running = false;
+static int verbose_level = 0;
+
+static void log_callback(const char* message)
+{
+ if (verbose_level == 1 || verbose_level == 3)
+ {
+ printf("[LOG] %s\n", message);
+ }
+}
+
+static void connection_callback(const char* process_name, uint32_t pid, const char* dest_ip, uint16_t dest_port, const char* proxy_info)
+{
+ if (verbose_level == 2 || verbose_level == 3)
+ {
+ printf("[CONN] %s (PID:%u) -> %s:%u via %s\n",
+ process_name, pid, dest_ip, dest_port, proxy_info);
+ }
+}
+
+static void signal_handler(int sig)
+{
+ if (sig == SIGSEGV || sig == SIGABRT || sig == SIGBUS)
+ {
+ printf("\n\n=== CLI CRASH DETECTED ===\n");
+ printf("Signal: %d (%s)\n", sig,
+ sig == SIGSEGV ? "SEGFAULT" :
+ sig == SIGABRT ? "ABORT" : "BUS ERROR");
+ printf("Calling emergency cleanup...\n");
+ ProxyBridge_Stop();
+ _exit(1);
+ }
+
+ if (keep_running)
+ {
+ printf("\n\nStopping ProxyBridge...\n");
+ keep_running = false;
+ }
+}
+
+static void show_banner(void)
+{
+ printf("\n");
+ printf(" ____ ____ _ _ \n");
+ printf(" | _ \\ _ __ _____ ___ _ | __ ) _ __(_) __| | __ _ ___ \n");
+ printf(" | |_) | '__/ _ \\ \\/ / | | | | _ \\| '__| |/ _` |/ _` |/ _ \\\n");
+ printf(" | __/| | | (_) > <| |_| | | |_) | | | | (_| | (_| | __/\n");
+ printf(" |_| |_| \\___/_/\\_\\\\__, | |____/|_| |_|\\__,_|\\__, |\\___|\n");
+ printf(" |___/ |___/ V4.0-Beta\n");
+ printf("\n");
+ printf(" Universal proxy client for Linux applications\n");
+ printf("\n");
+ printf("\tAuthor: Sourav Kalal/InterceptSuite\n");
+ printf("\tGitHub: https://github.com/InterceptSuite/ProxyBridge\n");
+ printf("\n");
+}
+
+static void show_help(const char* prog)
+{
+ show_banner();
+ printf("USAGE:\n");
+ printf(" %s [OPTIONS]\n\n", prog);
+
+ printf("OPTIONS:\n");
+ printf(" --proxy Proxy server URL with optional authentication\n");
+ printf(" Format: type://ip:port or type://ip:port:username:password\n");
+ printf(" Examples: socks5://127.0.0.1:1080\n");
+ printf(" http://proxy.com:8080:myuser:mypass\n");
+ printf(" Default: socks5://127.0.0.1:4444\n\n");
+
+ printf(" --rule Traffic routing rule (can be specified multiple times)\n");
+ printf(" Format: process:hosts:ports:protocol:action\n");
+ printf(" process - Process name(s): curl, cur*, *, or multiple separated by ;\n");
+ printf(" hosts - IP/host(s): *, google.com, 192.168.*.*, or multiple separated by ; or ,\n");
+ printf(" ports - Port(s): *, 443, 80;8080, 80-100, or multiple separated by ; or ,\n");
+ printf(" protocol - TCP, UDP, or BOTH\n");
+ printf(" action - PROXY, DIRECT, or BLOCK\n");
+ printf(" Examples:\n");
+ printf(" curl:*:*:TCP:PROXY\n");
+ printf(" curl;wget:*:*:TCP:PROXY\n");
+ printf(" *:*:53:UDP:PROXY\n");
+ printf(" firefox:*:80;443:TCP:DIRECT\n\n");
+
+ printf(" --dns-via-proxy Route DNS queries through proxy\n");
+ printf(" Values: true, false, 1, 0\n");
+ printf(" Default: true\n\n");
+
+ printf(" --verbose Logging verbosity level\n");
+ printf(" 0 - No logs (default)\n");
+ printf(" 1 - Show log messages only\n");
+ printf(" 2 - Show connection events only\n");
+ printf(" 3 - Show both logs and connections\n\n");
+
+ printf(" --cleanup Cleanup resources (iptables, etc.) from crashed instance\n");
+ printf(" Use if ProxyBridge crashed without proper cleanup\n\n");
+
+ printf(" --help, -h Show this help message\n\n");
+
+ printf("EXAMPLES:\n");
+ printf(" # Basic usage with default proxy\n");
+ printf(" sudo %s --rule curl:*:*:TCP:PROXY\n\n", prog);
+
+ printf(" # Multiple rules with custom proxy\n");
+ printf(" sudo %s --proxy socks5://192.168.1.10:1080 \\\n", prog);
+ printf(" --rule curl:*:*:TCP:PROXY \\\n");
+ printf(" --rule wget:*:*:TCP:PROXY \\\n");
+ printf(" --verbose 2\n\n");
+
+ printf(" # Route DNS through proxy with multiple apps\n");
+ printf(" sudo %s --proxy socks5://127.0.0.1:1080 \\\n", prog);
+ printf(" --rule \"curl;wget;firefox:*:*:BOTH:PROXY\" \\\n");
+ printf(" --dns-via-proxy true --verbose 3\n\n");
+
+ printf("NOTE:\n");
+ printf(" ProxyBridge requires root privileges to use nfqueue.\n");
+ printf(" Run with 'sudo' or as root user.\n\n");
+}
+
+static RuleProtocol parse_protocol(const char* str)
+{
+ char upper[16];
+ for (size_t i = 0; str[i] && i < 15; i++)
+ upper[i] = toupper(str[i]);
+ upper[strlen(str) < 15 ? strlen(str) : 15] = '\0';
+
+ if (strcmp(upper, "TCP") == 0)
+ return RULE_PROTOCOL_TCP;
+ else if (strcmp(upper, "UDP") == 0)
+ return RULE_PROTOCOL_UDP;
+ else if (strcmp(upper, "BOTH") == 0)
+ return RULE_PROTOCOL_BOTH;
+ else
+ {
+ fprintf(stderr, "ERROR: Invalid protocol '%s'. Use TCP, UDP, or BOTH\n", str);
+ exit(1);
+ }
+}
+
+static RuleAction parse_action(const char* str)
+{
+ char upper[16];
+ for (size_t i = 0; str[i] && i < 15; i++)
+ upper[i] = toupper(str[i]);
+ upper[strlen(str) < 15 ? strlen(str) : 15] = '\0';
+
+ if (strcmp(upper, "PROXY") == 0)
+ return RULE_ACTION_PROXY;
+ else if (strcmp(upper, "DIRECT") == 0)
+ return RULE_ACTION_DIRECT;
+ else if (strcmp(upper, "BLOCK") == 0)
+ return RULE_ACTION_BLOCK;
+ else
+ {
+ fprintf(stderr, "ERROR: Invalid action '%s'. Use PROXY, DIRECT, or BLOCK\n", str);
+ exit(1);
+ }
+}
+
+static void default_if_empty(char* dest, const char* src, const char* default_val, size_t dest_size)
+{
+ if (src == NULL || src[0] == '\0' || strcmp(src, " ") == 0)
+ strncpy(dest, default_val, dest_size - 1);
+ else
+ strncpy(dest, src, dest_size - 1);
+ dest[dest_size - 1] = '\0';
+}
+
+static bool parse_rule(const char* rule_str, ProxyRule* rule)
+{
+ char buffer[MAX_RULE_STR];
+ strncpy(buffer, rule_str, sizeof(buffer) - 1);
+ buffer[sizeof(buffer) - 1] = '\0';
+
+ char* parts[5] = {NULL, NULL, NULL, NULL, NULL};
+ int part_idx = 0;
+ char* token = strtok(buffer, ":");
+
+ while (token != NULL && part_idx < 5)
+ {
+ parts[part_idx++] = token;
+ token = strtok(NULL, ":");
+ }
+
+ if (part_idx != 5)
+ {
+ fprintf(stderr, "ERROR: Invalid rule format '%s'\n", rule_str);
+ fprintf(stderr, "Expected format: process:hosts:ports:protocol:action\n");
+ return false;
+ }
+
+ default_if_empty(rule->process_name, parts[0], "*", sizeof(rule->process_name));
+ default_if_empty(rule->target_hosts, parts[1], "*", sizeof(rule->target_hosts));
+ default_if_empty(rule->target_ports, parts[2], "*", sizeof(rule->target_ports));
+
+ rule->protocol = parse_protocol(parts[3]);
+ rule->action = parse_action(parts[4]);
+
+ return true;
+}
+
+static bool parse_proxy_url(const char* url, ProxyType* type, char* host, uint16_t* port, char* username, char* password)
+{
+ char buffer[512];
+ strncpy(buffer, url, sizeof(buffer) - 1);
+ buffer[sizeof(buffer) - 1] = '\0';
+
+ username[0] = '\0';
+ password[0] = '\0';
+
+ // parse type://
+ char* scheme_end = strstr(buffer, "://");
+ if (scheme_end == NULL)
+ {
+ fprintf(stderr, "ERROR: Invalid proxy URL format. Expected type://host:port\n");
+ return false;
+ }
+
+ *scheme_end = '\0';
+ char* scheme = buffer;
+ char* rest = scheme_end + 3;
+
+ char upper_scheme[16];
+ for (size_t i = 0; scheme[i] && i < 15; i++)
+ upper_scheme[i] = toupper(scheme[i]);
+ upper_scheme[strlen(scheme) < 15 ? strlen(scheme) : 15] = '\0';
+
+ if (strcmp(upper_scheme, "SOCKS5") == 0)
+ *type = PROXY_TYPE_SOCKS5;
+ else if (strcmp(upper_scheme, "HTTP") == 0)
+ *type = PROXY_TYPE_HTTP;
+ else
+ {
+ fprintf(stderr, "ERROR: Invalid proxy type '%s'. Use 'socks5' or 'http'\n", scheme);
+ return false;
+ }
+
+ // parse host:port[:user:pass]
+ char* parts[4];
+ int num_parts = 0;
+ char* token = strtok(rest, ":");
+ while (token != NULL && num_parts < 4)
+ {
+ parts[num_parts++] = token;
+ token = strtok(NULL, ":");
+ }
+
+ if (num_parts < 2)
+ {
+ fprintf(stderr, "ERROR: Invalid proxy URL. Missing host or port\n");
+ return false;
+ }
+
+ strncpy(host, parts[0], 255);
+ host[255] = '\0';
+
+ *port = atoi(parts[1]);
+ if (*port == 0)
+ {
+ fprintf(stderr, "ERROR: Invalid proxy port '%s'\n", parts[1]);
+ return false;
+ }
+
+ if (num_parts >= 4)
+ {
+ strncpy(username, parts[2], 255);
+ username[255] = '\0';
+ strncpy(password, parts[3], 255);
+ password[255] = '\0';
+ }
+
+ return true;
+}
+
+static bool is_root(void)
+{
+ return getuid() == 0;
+}
+
+int main(int argc, char *argv[])
+{
+ // check cleanup flag
+ for (int i = 1; i < argc; i++)
+ {
+ if (strcmp(argv[i], "--cleanup") == 0)
+ {
+ printf("Running cleanup...\n");
+ ProxyBridge_Stop();
+ printf("Cleanup complete.\n");
+ return 0;
+ }
+ }
+
+ char proxy_url[512] = "socks5://127.0.0.1:4444";
+ ProxyRule rules[MAX_RULES];
+ int num_rules = 0;
+ bool dns_via_proxy = true;
+
+ // parse args
+ for (int i = 1; i < argc; i++)
+ {
+ if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0)
+ {
+ show_help(argv[0]);
+ return 0;
+ }
+ else if (strcmp(argv[i], "--proxy") == 0 && i + 1 < argc)
+ {
+ strncpy(proxy_url, argv[++i], sizeof(proxy_url) - 1);
+ proxy_url[sizeof(proxy_url) - 1] = '\0';
+ }
+ else if (strcmp(argv[i], "--rule") == 0 && i + 1 < argc)
+ {
+ if (num_rules >= MAX_RULES)
+ {
+ fprintf(stderr, "ERROR: Maximum %d rules supported\n", MAX_RULES);
+ return 1;
+ }
+ if (!parse_rule(argv[++i], &rules[num_rules]))
+ return 1;
+ num_rules++;
+ }
+ else if (strcmp(argv[i], "--dns-via-proxy") == 0 && i + 1 < argc)
+ {
+ char* value = argv[++i];
+ if (strcmp(value, "true") == 0 || strcmp(value, "1") == 0)
+ dns_via_proxy = true;
+ else if (strcmp(value, "false") == 0 || strcmp(value, "0") == 0)
+ dns_via_proxy = false;
+ else
+ {
+ fprintf(stderr, "ERROR: Invalid value for --dns-via-proxy. Use: true, false, 1, or 0\n");
+ return 1;
+ }
+ }
+ else if (strcmp(argv[i], "--verbose") == 0 && i + 1 < argc)
+ {
+ verbose_level = atoi(argv[++i]);
+ if (verbose_level < 0 || verbose_level > 3)
+ {
+ fprintf(stderr, "ERROR: Verbose level must be 0-3\n");
+ return 1;
+ }
+ }
+ else
+ {
+ fprintf(stderr, "ERROR: Unknown option '%s'\n", argv[i]);
+ fprintf(stderr, "Use --help for usage information\n");
+ return 1;
+ }
+ }
+
+ show_banner();
+
+ // need root
+ if (!is_root())
+ {
+ printf("\033[31m\nERROR: ProxyBridge requires root privileges!\033[0m\n");
+ printf("Please run this application with sudo or as root.\n\n");
+ return 1;
+ }
+
+ // parse proxy config
+ ProxyType proxy_type;
+ char proxy_host[256];
+ uint16_t proxy_port;
+ char proxy_username[256];
+ char proxy_password[256];
+
+ if (!parse_proxy_url(proxy_url, &proxy_type, proxy_host, &proxy_port, proxy_username, proxy_password))
+ return 1;
+
+ // setup callbacks based on verbose
+ // 0=nothing 1=logs 2=connections 3=both
+
+ if (verbose_level == 1 || verbose_level == 3)
+ ProxyBridge_SetLogCallback(log_callback);
+ else
+ ProxyBridge_SetLogCallback(NULL); // Explicitly disable
+
+ if (verbose_level == 2 || verbose_level == 3)
+ ProxyBridge_SetConnectionCallback(connection_callback);
+ else
+ ProxyBridge_SetConnectionCallback(NULL); // Explicitly disable
+
+ // turn on traffic logging when needed
+ ProxyBridge_SetTrafficLoggingEnabled(verbose_level > 0);
+
+ // show config
+ printf("Proxy: %s://%s:%u\n",
+ proxy_type == PROXY_TYPE_HTTP ? "http" : "socks5",
+ proxy_host, proxy_port);
+
+ if (proxy_username[0] != '\0')
+ printf("Proxy Auth: %s:***\n", proxy_username);
+
+ printf("DNS via Proxy: %s\n", dns_via_proxy ? "Enabled" : "Disabled");
+
+ // setup proxy
+ if (!ProxyBridge_SetProxyConfig(proxy_type, proxy_host, proxy_port,
+ proxy_username[0] ? proxy_username : "",
+ proxy_password[0] ? proxy_password : ""))
+ {
+ fprintf(stderr, "ERROR: Failed to set proxy configuration\n");
+ return 1;
+ }
+
+ ProxyBridge_SetDnsViaProxy(dns_via_proxy);
+
+ // add rules
+ if (num_rules > 0)
+ {
+ printf("Rules: %d\n", num_rules);
+ for (int i = 0; i < num_rules; i++)
+ {
+ const char* protocol_str = rules[i].protocol == RULE_PROTOCOL_TCP ? "TCP" :
+ rules[i].protocol == RULE_PROTOCOL_UDP ? "UDP" : "BOTH";
+ const char* action_str = rules[i].action == RULE_ACTION_PROXY ? "PROXY" :
+ rules[i].action == RULE_ACTION_DIRECT ? "DIRECT" : "BLOCK";
+
+ uint32_t rule_id = ProxyBridge_AddRule(
+ rules[i].process_name,
+ rules[i].target_hosts,
+ rules[i].target_ports,
+ rules[i].protocol,
+ rules[i].action);
+
+ if (rule_id > 0)
+ {
+ printf(" [%u] %s:%s:%s:%s -> %s\n",
+ rule_id,
+ rules[i].process_name,
+ rules[i].target_hosts,
+ rules[i].target_ports,
+ protocol_str,
+ action_str);
+ }
+ else
+ {
+ fprintf(stderr, " ERROR: Failed to add rule for %s\n", rules[i].process_name);
+ }
+ }
+ }
+ else
+ {
+ printf("\033[33mWARNING: No rules specified. No traffic will be proxied.\033[0m\n");
+ printf("Use --rule to add proxy rules. See --help for examples.\n");
+ }
+
+ // start proxybridge
+ if (!ProxyBridge_Start())
+ {
+ fprintf(stderr, "ERROR: Failed to start ProxyBridge\n");
+ return 1;
+ }
+
+ keep_running = true;
+ printf("\nProxyBridge started. Press Ctrl+C to stop...\n\n");
+
+ signal(SIGINT, signal_handler);
+ signal(SIGTERM, signal_handler);
+ signal(SIGSEGV, signal_handler); // Catch segfault
+ signal(SIGABRT, signal_handler); // Catch abort
+ signal(SIGBUS, signal_handler); // Catch bus error
+
+ // main loop
+ while (keep_running)
+ {
+ sleep(1);
+ }
+
+ // cleanup
+ ProxyBridge_Stop();
+ printf("ProxyBridge stopped.\n");
+
+ return 0;
+}
diff --git a/Linux/deploy.sh b/Linux/deploy.sh
new file mode 100644
index 0000000..d9dd929
--- /dev/null
+++ b/Linux/deploy.sh
@@ -0,0 +1,346 @@
+#!/bin/bash
+
+set -e
+
+GITHUB_REPO="InterceptSuite/ProxyBridge"
+GITHUB_API="https://api.github.com/repos/$GITHUB_REPO/releases/latest"
+TEMP_DIR="/tmp/proxybridge-install-$$"
+
+echo ""
+echo "==================================="
+echo "ProxyBridge Auto-Deploy Script"
+echo "==================================="
+echo ""
+
+# Check if running as root
+if [ "$EUID" -ne 0 ]; then
+ echo "ERROR: This script must be run as root"
+ echo "Please run: sudo bash deploy.sh"
+ exit 1
+fi
+
+# Check for required tools
+check_requirements() {
+ echo "Checking required tools..."
+
+ local missing_tools=()
+
+ if ! command -v curl &> /dev/null; then
+ missing_tools+=("curl")
+ fi
+
+ if ! command -v tar &> /dev/null; then
+ missing_tools+=("tar")
+ fi
+
+ if ! command -v jq &> /dev/null; then
+ echo "WARNING: jq not found. Attempting to parse JSON manually."
+ fi
+
+ if [ ${#missing_tools[@]} -gt 0 ]; then
+ echo "ERROR: Missing required tools: ${missing_tools[*]}"
+ echo ""
+ echo "Please install them first:"
+ echo " Debian/Ubuntu: sudo apt install curl tar jq"
+ echo " Fedora: sudo dnf install curl tar jq"
+ echo " Arch: sudo pacman -S curl tar jq"
+ exit 1
+ fi
+
+ echo "All required tools available"
+}
+
+# Download latest release
+download_latest_release() {
+ echo ""
+ echo "Fetching latest release information..."
+
+ # Create temp directory
+ mkdir -p "$TEMP_DIR"
+ cd "$TEMP_DIR"
+
+ # Get release info from GitHub API
+ local api_response
+ api_response=$(curl -sL "$GITHUB_API")
+
+ if [ -z "$api_response" ]; then
+ echo "ERROR: Failed to fetch release information from GitHub"
+ exit 1
+ fi
+
+ # Extract download URL for Linux tar.gz
+ local download_url
+ if command -v jq &> /dev/null; then
+ download_url=$(echo "$api_response" | jq -r '.assets[] | select(.name | test("ProxyBridge-Linux-.*\\.tar\\.gz$")) | .browser_download_url' | head -1)
+ else
+ # Fallback: manual parsing
+ download_url=$(echo "$api_response" | grep -o '"browser_download_url": *"[^"]*ProxyBridge-Linux-[^"]*\.tar\.gz"' | head -1 | sed 's/.*"browser_download_url": *"\([^"]*\)".*/\1/')
+ fi
+
+ if [ -z "$download_url" ]; then
+ echo "ERROR: Could not find Linux release archive in latest release"
+ echo ""
+ echo "Please check if a Linux release is available at:"
+ echo " https://github.com/$GITHUB_REPO/releases/latest"
+ exit 1
+ fi
+
+ # Extract version from URL
+ local filename
+ filename=$(basename "$download_url")
+ echo "Found release: $filename"
+ echo "Download URL: $download_url"
+
+ echo ""
+ echo "Downloading $filename..."
+ if ! curl -L -o "$filename" "$download_url"; then
+ echo "ERROR: Failed to download release archive"
+ exit 1
+ fi
+
+ echo "Download complete"
+
+ # Extract archive
+ echo ""
+ echo "Extracting archive..."
+ if ! tar -xzf "$filename"; then
+ echo "ERROR: Failed to extract archive"
+ exit 1
+ fi
+
+ echo "Extraction complete"
+ echo "Files extracted to: $TEMP_DIR"
+ ls -lh
+}
+
+# Detect Linux distribution
+detect_distro() {
+ if [ -f /etc/os-release ]; then
+ . /etc/os-release
+ DISTRO=$ID
+ DISTRO_LIKE=$ID_LIKE
+ elif [ -f /etc/lsb-release ]; then
+ . /etc/lsb-release
+ DISTRO=$DISTRIB_ID
+ else
+ DISTRO=$(uname -s)
+ fi
+ echo ""
+ echo "Detected distribution: $DISTRO"
+}
+
+# Install dependencies based on distribution
+install_dependencies() {
+ echo ""
+ echo "Checking and installing dependencies..."
+
+ # Normalize distro name using ID_LIKE fallback
+ local distro_family="$DISTRO"
+ if [ -n "$DISTRO_LIKE" ]; then
+ case "$DISTRO_LIKE" in
+ *ubuntu*|*debian*) distro_family="debian" ;;
+ *fedora*) distro_family="fedora" ;;
+ *rhel*|*centos*) distro_family="rhel" ;;
+ *arch*) distro_family="arch" ;;
+ *suse*) distro_family="opensuse" ;;
+ esac
+ fi
+
+ case "$distro_family" in
+ ubuntu|debian|linuxmint|pop|elementary|zorin|kali|raspbian|mx|antix|deepin|lmde)
+ echo "Using apt package manager..."
+ apt-get update -qq
+ apt-get install -y libnetfilter-queue1 libnfnetlink0 iptables libgtk-3-0
+ ;;
+ fedora)
+ echo "Using dnf package manager..."
+ dnf install -y libnetfilter_queue libnfnetlink iptables gtk3
+ ;;
+ rhel|centos|rocky|almalinux)
+ echo "Using yum package manager..."
+ yum install -y libnetfilter_queue libnfnetlink iptables gtk3
+ ;;
+ arch|manjaro|endeavouros|garuda)
+ echo "Using pacman package manager..."
+ pacman -Sy --noconfirm libnetfilter_queue libnfnetlink iptables gtk3
+ ;;
+ opensuse*|sles)
+ echo "Using zypper package manager..."
+ zypper install -y libnetfilter_queue1 libnfnetlink0 iptables gtk3
+ ;;
+ void)
+ echo "Using xbps package manager..."
+ xbps-install -Sy libnetfilter_queue libnfnetlink iptables gtk3
+ ;;
+ *)
+ echo "WARNING: Unknown distribution '$DISTRO' (family: '$DISTRO_LIKE')"
+ echo ""
+ echo "Please manually install the following packages:"
+ echo " Debian/Ubuntu: sudo apt install libnetfilter-queue1 libnfnetlink0 iptables libgtk-3-0"
+ echo " Fedora: sudo dnf install libnetfilter_queue libnfnetlink iptables gtk3"
+ echo " Arch: sudo pacman -S libnetfilter_queue libnfnetlink iptables gtk3"
+ echo ""
+ read -p "Continue anyway? (y/n) " -n 1 -r
+ echo
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ exit 1
+ fi
+ ;;
+ esac
+
+ echo "Dependencies installed"
+}
+
+# Use /usr/local/lib (matches RPATH in binary)
+detect_lib_path() {
+ LIB_PATH="/usr/local/lib"
+ echo "Library installation path: $LIB_PATH"
+}
+
+# Check if files exist in extracted directory
+check_files() {
+ echo ""
+ echo "Checking for required files..."
+
+ if [ ! -f "$TEMP_DIR/libproxybridge.so" ]; then
+ echo "ERROR: libproxybridge.so not found in extracted archive"
+ exit 1
+ fi
+
+ if [ ! -f "$TEMP_DIR/ProxyBridge" ]; then
+ echo "ERROR: ProxyBridge binary not found in extracted archive"
+ exit 1
+ fi
+
+ if [ ! -f "$TEMP_DIR/ProxyBridgeGUI" ]; then
+ echo "WARNING: ProxyBridgeGUI binary not found - GUI will not be installed"
+ fi
+
+ echo "All required files present"
+}
+
+# Install files
+install_files() {
+ echo ""
+ echo "Installing ProxyBridge..."
+
+ # Create directories if they don't exist
+ mkdir -p "$LIB_PATH" /usr/local/bin /etc/proxybridge
+ chmod 755 /etc/proxybridge
+
+ # Copy library
+ echo "Installing libproxybridge.so to $LIB_PATH..."
+ cp "$TEMP_DIR/libproxybridge.so" "$LIB_PATH/"
+ chmod 755 "$LIB_PATH/libproxybridge.so"
+
+ # Copy binary
+ echo "Installing ProxyBridge to /usr/local/bin..."
+ cp "$TEMP_DIR/ProxyBridge" /usr/local/bin/
+ chmod 755 /usr/local/bin/ProxyBridge
+
+ if [ -f "$TEMP_DIR/ProxyBridgeGUI" ]; then
+ echo "Installing ProxyBridgeGUI to /usr/local/bin..."
+ cp "$TEMP_DIR/ProxyBridgeGUI" /usr/local/bin/
+ chmod 755 /usr/local/bin/ProxyBridgeGUI
+ fi
+
+ echo "Files installed"
+}
+
+# Update library cache
+update_ldconfig() {
+ echo ""
+ echo "Updating library cache..."
+
+ # Add /usr/local/lib to ld.so.conf if not already there
+ if [ -d /etc/ld.so.conf.d ]; then
+ if ! grep -q "^/usr/local/lib" /etc/ld.so.conf.d/* 2>/dev/null; then
+ echo "/usr/local/lib" > /etc/ld.so.conf.d/proxybridge.conf
+ if [ "$LIB_PATH" = "/usr/local/lib64" ]; then
+ echo "/usr/local/lib64" >> /etc/ld.so.conf.d/proxybridge.conf
+ fi
+ fi
+ fi
+
+ # Run ldconfig
+ if command -v ldconfig &> /dev/null; then
+ ldconfig 2>/dev/null || true
+ ldconfig -v 2>/dev/null | grep -q proxybridge || true
+ echo "Library cache updated"
+ else
+ echo "WARNING: ldconfig not found. You may need to reboot."
+ fi
+}
+
+# Verify installation
+verify_installation() {
+ echo ""
+ echo "Verifying installation..."
+
+ # Check if binary is in PATH
+ if command -v ProxyBridge &> /dev/null; then
+ echo "ProxyBridge binary found in PATH"
+ else
+ echo "ProxyBridge binary not found in PATH"
+ echo " You may need to add /usr/local/bin to your PATH"
+ fi
+
+ # Check if library is loadable
+ if ldd /usr/local/bin/ProxyBridge 2>/dev/null | grep -q "libproxybridge.so"; then
+ if ldd /usr/local/bin/ProxyBridge 2>/dev/null | grep "libproxybridge.so" | grep -q "not found"; then
+ echo "libproxybridge.so not loadable"
+ else
+ echo "libproxybridge.so is loadable"
+ fi
+ fi
+
+ # Final test - try to run --help
+ if /usr/local/bin/ProxyBridge --help &>/dev/null; then
+ echo "ProxyBridge executable is working"
+ else
+ echo "⚠ ProxyBridge may have issues - try: sudo ldconfig"
+ fi
+}
+
+# Cleanup temp directory
+cleanup() {
+ echo ""
+ echo "Cleaning up temporary files..."
+ rm -rf "$TEMP_DIR"
+ echo "Cleanup complete"
+}
+
+# Main deployment
+main() {
+ check_requirements
+ download_latest_release
+ detect_distro
+ check_files
+ install_dependencies
+ detect_lib_path
+ install_files
+ update_ldconfig
+ verify_installation
+ cleanup
+
+ echo ""
+ echo "==================================="
+ echo "Installation Complete!"
+ echo "==================================="
+ echo ""
+ echo "You can now run ProxyBridge from anywhere:"
+ echo " sudo ProxyBridge --help"
+ if [ -f /usr/local/bin/ProxyBridgeGUI ]; then
+ echo " sudo ProxyBridgeGUI (Graphical Interface)"
+ fi
+ echo " sudo ProxyBridge --proxy socks5://IP:PORT --rule \"app:*:*:TCP:PROXY\""
+ echo ""
+ echo "For cleanup after crash:"
+ echo " sudo ProxyBridge --cleanup"
+ echo ""
+}
+
+# Trap errors and cleanup
+trap 'echo ""; echo "ERROR: Installation failed. Cleaning up..."; rm -rf "$TEMP_DIR"; exit 1' ERR
+
+main
diff --git a/Linux/gui/gui.h b/Linux/gui/gui.h
new file mode 100644
index 0000000..035c1ee
--- /dev/null
+++ b/Linux/gui/gui.h
@@ -0,0 +1,107 @@
+#ifndef PROXYBRIDGE_GUI_H
+#define PROXYBRIDGE_GUI_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "ProxyBridge.h"
+typedef struct {
+ char *process_name;
+ uint32_t pid;
+ char *dest_ip;
+ uint16_t dest_port;
+ char *proxy_info;
+ char *timestamp;
+} ConnectionData;
+
+typedef struct {
+ char *message;
+} LogData;
+
+typedef struct {
+ GtkWidget *dialog;
+ GtkWidget *ip_entry;
+ GtkWidget *port_entry;
+ GtkWidget *type_combo;
+ GtkWidget *user_entry;
+ GtkWidget *pass_entry;
+ GtkWidget *test_host;
+ GtkWidget *test_port;
+ GtkTextBuffer *output_buffer;
+ GtkWidget *test_btn;
+} ConfigInfo;
+
+typedef struct {
+ uint32_t id;
+ char *process_name;
+ char *target_hosts;
+ char *target_ports;
+ RuleProtocol protocol;
+ RuleAction action;
+ bool enabled;
+ bool selected;
+} RuleData;
+
+struct TestRunnerData {
+ char *host;
+ uint16_t port;
+ ConfigInfo *ui_info;
+};
+
+typedef struct {
+ char *result_text;
+ GtkTextBuffer *buffer;
+ GtkWidget *btn;
+} TestResultData;
+
+extern GtkWidget *window;
+extern GtkTextBuffer *conn_buffer;
+extern GtkTextBuffer *log_buffer;
+extern GtkWidget *status_bar;
+extern guint status_context_id;
+
+extern char g_proxy_ip[256];
+extern uint16_t g_proxy_port;
+extern ProxyType g_proxy_type;
+extern char g_proxy_user[256];
+extern char g_proxy_pass[256];
+
+extern GList *g_rules_list;
+extern bool g_chk_logging;
+extern bool g_chk_dns;
+
+long safe_strtol(const char *nptr);
+void show_message(GtkWindow *parent, GtkMessageType type, const char *format, ...);
+void trim_buffer_lines(GtkTextBuffer *buffer, int max_lines);
+char* get_current_time_str();
+char *escape_json_string(const char *src);
+char *extract_sub_json_str(const char *json, const char *key);
+bool extract_sub_json_bool(const char *json, const char *key);
+
+// config storage
+void save_config();
+void load_config();
+
+// settings
+void on_proxy_configure(GtkWidget *widget, gpointer data);
+
+// rules section
+void on_proxy_rules_clicked(GtkWidget *widget, gpointer data);
+
+// Logs
+void lib_log_callback(const char *message);
+void lib_connection_callback(const char *process_name, uint32_t pid, const char *dest_ip, uint16_t dest_port, const char *proxy_info);
+void on_search_conn_changed(GtkSearchEntry *entry, gpointer user_data);
+void on_search_log_changed(GtkSearchEntry *entry, gpointer user_data);
+void on_clear_conn_clicked(GtkButton *button, gpointer user_data);
+void on_clear_log_clicked(GtkButton *button, gpointer user_data);
+
+#endif
diff --git a/Linux/gui/gui_config.c b/Linux/gui/gui_config.c
new file mode 100644
index 0000000..4030967
--- /dev/null
+++ b/Linux/gui/gui_config.c
@@ -0,0 +1,109 @@
+#include "gui.h"
+
+#define CONFIG_DIR "/etc/proxybridge"
+#define CONFIG_PATH "/etc/proxybridge/config.ini"
+
+// save config
+void save_config() {
+ struct stat st = {0};
+ if (stat(CONFIG_DIR, &st) == -1) {
+ if (mkdir(CONFIG_DIR, 0755) != 0) {
+ perror("failed to create config dir");
+ return;
+ }
+ }
+
+ FILE *f = fopen(CONFIG_PATH, "w");
+ if (!f) {
+ printf("failed to save config to %s\n", CONFIG_PATH);
+ return;
+ }
+
+ // settings section
+ fprintf(f, "[SETTINGS]\n");
+ fprintf(f, "ip=%s\n", g_proxy_ip);
+ fprintf(f, "port=%d\n", g_proxy_port);
+ fprintf(f, "type=%d\n", g_proxy_type);
+ fprintf(f, "user=%s\n", g_proxy_user);
+ fprintf(f, "pass=%s\n", g_proxy_pass);
+ fprintf(f, "logging=%d\n", g_chk_logging);
+ fprintf(f, "dns=%d\n", g_chk_dns);
+
+ // rules section
+ fprintf(f, "[RULES]\n");
+ for (GList *l = g_rules_list; l != NULL; l = l->next) {
+ RuleData *rule = (RuleData *)l->data;
+ // format: id|protocol|action|enabled|procname|hosts|ports
+ fprintf(f, "%u|%d|%d|%d|%s|%s|%s\n",
+ rule->id,
+ rule->protocol,
+ rule->action,
+ rule->enabled,
+ rule->process_name ? rule->process_name : "",
+ rule->target_hosts ? rule->target_hosts : "",
+ rule->target_ports ? rule->target_ports : ""
+ );
+ }
+
+ fclose(f);
+}
+
+// load settings from file
+void load_config() {
+ FILE *f = fopen(CONFIG_PATH, "r");
+ if (!f) return;
+
+ char line[2048];
+ int section = 0; // 0=none, 1=settings, 2=rules
+
+ while (fgets(line, sizeof(line), f)) {
+ // trim newline
+ line[strcspn(line, "\r\n")] = 0;
+
+ if (strlen(line) == 0 || line[0] == '#') continue;
+
+ if (strcmp(line, "[SETTINGS]") == 0) { section = 1; continue; }
+ if (strcmp(line, "[RULES]") == 0) { section = 2; continue; }
+
+ if (section == 1) {
+ char *val = strchr(line, '=');
+ if (!val) continue;
+ *val = 0; val++;
+
+ if (strcmp(line, "ip") == 0) strncpy(g_proxy_ip, val, sizeof(g_proxy_ip) - 1);
+ else if (strcmp(line, "port") == 0) g_proxy_port = atoi(val);
+ else if (strcmp(line, "type") == 0) g_proxy_type = atoi(val);
+ else if (strcmp(line, "user") == 0) strncpy(g_proxy_user, val, sizeof(g_proxy_user) - 1);
+ else if (strcmp(line, "pass") == 0) strncpy(g_proxy_pass, val, sizeof(g_proxy_pass) - 1);
+ else if (strcmp(line, "logging") == 0) g_chk_logging = atoi(val);
+ else if (strcmp(line, "dns") == 0) g_chk_dns = atoi(val);
+ }
+ else if (section == 2) {
+ // parse rule line
+ RuleData *rule = g_malloc0(sizeof(RuleData));
+ char *p = line;
+ char *token;
+ int idx = 0;
+
+ while ((token = strsep(&p, "|")) != NULL) {
+ switch(idx) {
+ case 0: rule->id = atoi(token); break;
+ case 1: rule->protocol = atoi(token); break;
+ case 2: rule->action = atoi(token); break;
+ case 3: rule->enabled = atoi(token); break;
+ case 4: rule->process_name = g_strdup(token); break;
+ case 5: rule->target_hosts = g_strdup(token); break;
+ case 6: rule->target_ports = g_strdup(token); break;
+ }
+ idx++;
+ }
+
+ if (idx >= 4) {
+ g_rules_list = g_list_append(g_rules_list, rule);
+ } else {
+ g_free(rule);
+ }
+ }
+ }
+ fclose(f);
+}
\ No newline at end of file
diff --git a/Linux/gui/gui_logs.c b/Linux/gui/gui_logs.c
new file mode 100644
index 0000000..8fc8d2f
--- /dev/null
+++ b/Linux/gui/gui_logs.c
@@ -0,0 +1,126 @@
+#include "gui.h"
+
+// filter logs based on search
+static void filter_text_view(GtkTextBuffer *buffer, const char *text) {
+ if (!buffer) return;
+
+ GtkTextIter start, end;
+ gtk_text_buffer_get_bounds(buffer, &start, &end);
+ gtk_text_buffer_remove_tag_by_name(buffer, "hidden", &start, &end);
+
+ if (!text || strlen(text) == 0) return;
+
+ GtkTextIter line_start = start;
+ while (!gtk_text_iter_is_end(&line_start)) {
+ GtkTextIter line_end = line_start;
+ if (!gtk_text_iter_ends_line(&line_end))
+ gtk_text_iter_forward_to_line_end(&line_end);
+
+ char *line_text = gtk_text_buffer_get_text(buffer, &line_start, &line_end, FALSE);
+
+ // search case insensitive
+ char *lower_line = g_utf8_strdown(line_text, -1);
+ char *lower_search = g_utf8_strdown(text, -1);
+
+ if (!strstr(lower_line, lower_search)) {
+ GtkTextIter next_line = line_end;
+ gtk_text_iter_forward_char(&next_line); // include newline
+ gtk_text_buffer_apply_tag_by_name(buffer, "hidden", &line_start, &next_line);
+ }
+
+ g_free(lower_line);
+ g_free(lower_search);
+ g_free(line_text);
+
+ gtk_text_iter_forward_line(&line_start);
+ }
+}
+
+void on_search_conn_changed(GtkSearchEntry *entry, gpointer user_data) {
+ const char *text = gtk_entry_get_text(GTK_ENTRY(entry));
+ filter_text_view(conn_buffer, text);
+}
+
+void on_search_log_changed(GtkSearchEntry *entry, gpointer user_data) {
+ const char *text = gtk_entry_get_text(GTK_ENTRY(entry));
+ filter_text_view(log_buffer, text);
+}
+
+void on_clear_conn_clicked(GtkButton *button, gpointer user_data) {
+ if (conn_buffer) gtk_text_buffer_set_text(conn_buffer, "", 0);
+}
+
+void on_clear_log_clicked(GtkButton *button, gpointer user_data) {
+ if (log_buffer) gtk_text_buffer_set_text(log_buffer, "", 0);
+}
+
+static void free_connection_data(ConnectionData *data) {
+ if (data) {
+ free(data->process_name);
+ free(data->dest_ip);
+ free(data->proxy_info);
+ free(data->timestamp);
+ free(data);
+ }
+}
+
+// update logs from main thread
+static gboolean update_log_gui(gpointer user_data) {
+ LogData *data = (LogData *)user_data;
+ if (!data) return FALSE;
+
+ GtkTextIter end;
+ gtk_text_buffer_get_end_iter(log_buffer, &end);
+
+ char *time_str = get_current_time_str();
+ char full_msg[1200];
+ snprintf(full_msg, sizeof(full_msg), "%s %s\n", time_str, data->message);
+ free(time_str);
+
+ gtk_text_buffer_insert(log_buffer, &end, full_msg, -1);
+
+ trim_buffer_lines(log_buffer, 100);
+
+ free(data->message);
+ free(data);
+ return FALSE; // done
+}
+
+// update conn info
+static gboolean update_connection_gui_append(gpointer user_data) {
+ ConnectionData *data = (ConnectionData *)user_data;
+ if (!data) return FALSE;
+
+ if (conn_buffer) {
+ GtkTextIter end;
+ gtk_text_buffer_get_end_iter(conn_buffer, &end);
+
+ char line_buffer[1024];
+ snprintf(line_buffer, sizeof(line_buffer), "%s %s (PID:%u) -> %s:%u via %s\n",
+ data->timestamp, data->process_name, data->pid, data->dest_ip, data->dest_port, data->proxy_info);
+
+ gtk_text_buffer_insert(conn_buffer, &end, line_buffer, -1);
+
+ trim_buffer_lines(conn_buffer, 100);
+ }
+
+ free_connection_data(data);
+ return FALSE;
+}
+
+void lib_log_callback(const char *message) {
+ LogData *data = malloc(sizeof(LogData));
+ data->message = strdup(message);
+ g_idle_add(update_log_gui, data);
+}
+
+void lib_connection_callback(const char *process_name, uint32_t pid, const char *dest_ip, uint16_t dest_port, const char *proxy_info) {
+ ConnectionData *data = malloc(sizeof(ConnectionData));
+ data->process_name = strdup(process_name);
+ data->pid = pid;
+ data->dest_ip = strdup(dest_ip);
+ data->dest_port = dest_port;
+ data->proxy_info = strdup(proxy_info);
+ data->timestamp = get_current_time_str();
+ g_idle_add(update_connection_gui_append, data);
+}
diff --git a/Linux/gui/gui_rules.c b/Linux/gui/gui_rules.c
new file mode 100644
index 0000000..df625ed
--- /dev/null
+++ b/Linux/gui/gui_rules.c
@@ -0,0 +1,469 @@
+#include "gui.h"
+
+static GtkWidget *rules_list_box = NULL;
+static GtkWidget *btn_select_all_header = NULL;
+
+// forward
+static void refresh_rules_ui();
+
+static void free_rule_data(RuleData *rule) {
+ if (rule) {
+ if (rule->process_name) free(rule->process_name);
+ if (rule->target_hosts) free(rule->target_hosts);
+ if (rule->target_ports) free(rule->target_ports);
+ free(rule);
+ }
+}
+
+static void on_rule_delete(GtkWidget *widget, gpointer data) {
+ RuleData *rule = (RuleData *)data;
+ ProxyBridge_DeleteRule(rule->id);
+ g_rules_list = g_list_remove(g_rules_list, rule);
+ free_rule_data(rule);
+ save_config();
+ refresh_rules_ui();
+}
+
+static void on_rule_toggle(GtkToggleButton *btn, gpointer data) {
+ RuleData *rule = (RuleData *)data;
+ rule->enabled = gtk_toggle_button_get_active(btn);
+ if (rule->enabled) ProxyBridge_EnableRule(rule->id);
+ else ProxyBridge_DisableRule(rule->id);
+ save_config();
+}
+
+static void on_save_rule(GtkWidget *widget, gpointer data) {
+ GtkWidget **widgets = (GtkWidget **)data;
+ GtkWidget *dialog = widgets[0];
+ RuleData *edit_rule = (RuleData *)widgets[6]; // existing rule if present
+
+ const char *proc = gtk_entry_get_text(GTK_ENTRY(widgets[1]));
+ const char *hosts = gtk_entry_get_text(GTK_ENTRY(widgets[2]));
+ const char *ports = gtk_entry_get_text(GTK_ENTRY(widgets[3]));
+ RuleProtocol proto = gtk_combo_box_get_active(GTK_COMBO_BOX(widgets[4]));
+ RuleAction action = gtk_combo_box_get_active(GTK_COMBO_BOX(widgets[5]));
+
+ if (strlen(proc) == 0) return; // need process name
+
+ if (edit_rule) {
+ // update backend and local copy
+ ProxyBridge_EditRule(edit_rule->id, proc, hosts, ports, proto, action);
+
+ free(edit_rule->process_name); edit_rule->process_name = strdup(proc);
+ free(edit_rule->target_hosts); edit_rule->target_hosts = strdup(hosts);
+ free(edit_rule->target_ports); edit_rule->target_ports = strdup(ports);
+ edit_rule->protocol = proto;
+ edit_rule->action = action;
+ } else {
+ // add new rule
+ uint32_t new_id = ProxyBridge_AddRule(proc, hosts, ports, proto, action);
+ RuleData *new_rule = malloc(sizeof(RuleData));
+ new_rule->id = new_id;
+ new_rule->process_name = strdup(proc);
+ new_rule->target_hosts = strdup(hosts);
+ new_rule->target_ports = strdup(ports);
+ new_rule->protocol = proto;
+ new_rule->action = action;
+ new_rule->enabled = true;
+ new_rule->selected = false;
+ g_rules_list = g_list_append(g_rules_list, new_rule);
+ }
+ save_config();
+ refresh_rules_ui();
+ gtk_widget_destroy(dialog);
+ free(widgets);
+}
+
+static void on_browse_clicked(GtkWidget *widget, gpointer data) {
+ GtkWidget *entry = (GtkWidget *)data;
+ GtkWidget *dialog = gtk_file_chooser_dialog_new("Select Application",
+ NULL,
+ GTK_FILE_CHOOSER_ACTION_OPEN,
+ "_Cancel", GTK_RESPONSE_CANCEL,
+ "_Select", GTK_RESPONSE_ACCEPT,
+ NULL);
+ if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
+ char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
+ char *base = g_path_get_basename(filename);
+ gtk_entry_set_text(GTK_ENTRY(entry), base);
+ g_free(base);
+ g_free(filename);
+ }
+ gtk_widget_destroy(dialog);
+}
+
+static void open_rule_dialog(RuleData *rule) {
+ GtkWidget *dialog = gtk_dialog_new();
+ gtk_window_set_title(GTK_WINDOW(dialog), rule ? "Edit Rule" : "Add Rule");
+ gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(window));
+ gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
+ gtk_window_set_default_size(GTK_WINDOW(dialog), 500, 400);
+
+ GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
+ GtkWidget *grid = gtk_grid_new();
+ gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
+ gtk_grid_set_column_spacing(GTK_GRID(grid), 10);
+ gtk_container_set_border_width(GTK_CONTAINER(grid), 15);
+
+ // proc input
+ GtkWidget *proc_entry = gtk_entry_new();
+ GtkWidget *browse_btn = gtk_button_new_with_label("Browse...");
+ g_signal_connect(browse_btn, "clicked", G_CALLBACK(on_browse_clicked), proc_entry);
+ GtkWidget *proc_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
+ gtk_box_pack_start(GTK_BOX(proc_box), proc_entry, TRUE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(proc_box), browse_btn, FALSE, FALSE, 0);
+
+ GtkWidget *host_entry = gtk_entry_new();
+ GtkWidget *port_entry = gtk_entry_new();
+
+ // protocol list
+ GtkWidget *proto_combo = gtk_combo_box_text_new();
+ gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(proto_combo), "TCP");
+ gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(proto_combo), "UDP");
+ gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(proto_combo), "BOTH");
+
+ // action list
+ GtkWidget *action_combo = gtk_combo_box_text_new();
+ gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(action_combo), "PROXY");
+ gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(action_combo), "DIRECT");
+ gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(action_combo), "BLOCK");
+
+ if (rule) {
+ gtk_entry_set_text(GTK_ENTRY(proc_entry), rule->process_name);
+ gtk_entry_set_text(GTK_ENTRY(host_entry), rule->target_hosts);
+ gtk_entry_set_text(GTK_ENTRY(port_entry), rule->target_ports);
+ gtk_combo_box_set_active(GTK_COMBO_BOX(proto_combo), rule->protocol);
+ gtk_combo_box_set_active(GTK_COMBO_BOX(action_combo), rule->action);
+ } else {
+ gtk_entry_set_text(GTK_ENTRY(port_entry), "*");
+ gtk_entry_set_text(GTK_ENTRY(host_entry), "*");
+ gtk_combo_box_set_active(GTK_COMBO_BOX(proto_combo), 2); // BOTH default
+ gtk_combo_box_set_active(GTK_COMBO_BOX(action_combo), 0); // PROXY default
+ }
+
+ int row = 0;
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Process Name:"), 0, row, 1, 1);
+ gtk_grid_attach(GTK_GRID(grid), proc_box, 1, row, 1, 1); row++;
+
+ GtkWidget *proc_hint = gtk_label_new("Example: firefox; chrome; /usr/bin/wget");
+ gtk_style_context_add_class(gtk_widget_get_style_context(proc_hint), "dim-label");
+ gtk_widget_set_halign(proc_hint, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), proc_hint, 1, row, 1, 1); row++;
+
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Target Host:"), 0, row, 1, 1);
+ gtk_grid_attach(GTK_GRID(grid), host_entry, 1, row, 1, 1); row++;
+
+ GtkWidget *host_hint = gtk_label_new("Example: 192.168.1.*; 10.0.0.1-50; *");
+ gtk_style_context_add_class(gtk_widget_get_style_context(host_hint), "dim-label");
+ gtk_widget_set_halign(host_hint, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), host_hint, 1, row, 1, 1); row++;
+
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Target Port:"), 0, row, 1, 1);
+ gtk_grid_attach(GTK_GRID(grid), port_entry, 1, row, 1, 1); row++;
+
+ GtkWidget *port_hint = gtk_label_new("Example: 80; 443; 8000-8080; *");
+ gtk_style_context_add_class(gtk_widget_get_style_context(port_hint), "dim-label");
+ gtk_widget_set_halign(port_hint, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), port_hint, 1, row, 1, 1); row++;
+
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Protocol:"), 0, row, 1, 1);
+ gtk_grid_attach(GTK_GRID(grid), proto_combo, 1, row, 1, 1); row++;
+
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Action:"), 0, row, 1, 1);
+ gtk_grid_attach(GTK_GRID(grid), action_combo, 1, row, 1, 1); row++;
+
+ gtk_container_add(GTK_CONTAINER(content), grid);
+
+ GtkWidget *save_btn = gtk_button_new_with_label("Save");
+ GtkWidget *cancel_btn = gtk_button_new_with_label("Cancel");
+ gtk_dialog_add_action_widget(GTK_DIALOG(dialog), cancel_btn, GTK_RESPONSE_CANCEL);
+ gtk_dialog_add_action_widget(GTK_DIALOG(dialog), save_btn, GTK_RESPONSE_ACCEPT);
+
+ GtkWidget **data = malloc(7 * sizeof(GtkWidget*));
+ data[0] = dialog;
+ data[1] = proc_entry;
+ data[2] = host_entry;
+ data[3] = port_entry;
+ data[4] = proto_combo;
+ data[5] = action_combo;
+ data[6] = (GtkWidget*)rule;
+
+ g_signal_connect(save_btn, "clicked", G_CALLBACK(on_save_rule), data);
+ g_signal_connect(cancel_btn, "clicked", G_CALLBACK(gtk_widget_destroy), NULL);
+ gtk_widget_show_all(dialog);
+}
+
+static void on_rule_edit(GtkWidget *widget, gpointer data) {
+ on_proxy_rules_clicked(NULL, NULL); // Re-open if closed? Usually modal.
+ // Wait, on_rule_edit clicked from rules list already
+ open_rule_dialog((RuleData *)data);
+}
+
+static void on_rule_add_clicked(GtkWidget *widget, gpointer data) {
+ open_rule_dialog(NULL);
+}
+
+static void on_rule_select_toggle(GtkToggleButton *btn, gpointer data) {
+ RuleData *rule = (RuleData *)data;
+ rule->selected = gtk_toggle_button_get_active(btn);
+ if (btn_select_all_header) {
+ bool all_selected = (g_rules_list != NULL);
+ if (g_rules_list == NULL) all_selected = false;
+ for (GList *l = g_rules_list; l != NULL; l = l->next) {
+ RuleData *r = (RuleData *)l->data;
+ if (!r->selected) {
+ all_selected = false;
+ break;
+ }
+ }
+ gtk_button_set_label(GTK_BUTTON(btn_select_all_header), all_selected ? "Deselect All" : "Select All");
+ }
+}
+
+static void on_rule_export_clicked(GtkWidget *widget, gpointer data) {
+ if (!g_rules_list) return;
+ bool any_selected = false;
+ for (GList *l = g_rules_list; l != NULL; l = l->next) {
+ if (((RuleData *)l->data)->selected) { any_selected = true; break; }
+ }
+ if (!any_selected) {
+ show_message(NULL, GTK_MESSAGE_WARNING, "Please select at least one rule to export.");
+ return;
+ }
+ GtkWidget *dialog = gtk_file_chooser_dialog_new("Export Rules", GTK_WINDOW(window), GTK_FILE_CHOOSER_ACTION_SAVE, "_Cancel", GTK_RESPONSE_CANCEL, "_Save", GTK_RESPONSE_ACCEPT, NULL);
+ gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), TRUE);
+ gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), "proxy_rules.json");
+ if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
+ char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
+ FILE *f = fopen(filename, "w");
+ if (f) {
+ fprintf(f, "[\n");
+ bool first = true;
+ for (GList *l = g_rules_list; l != NULL; l = l->next) {
+ RuleData *r = (RuleData *)l->data;
+ if (!r->selected) continue;
+ if (!first) fprintf(f, ",\n");
+ char *proc = escape_json_string(r->process_name);
+ char *host = escape_json_string(r->target_hosts);
+ char *port = escape_json_string(r->target_ports);
+ const char *proto = (r->protocol == RULE_PROTOCOL_TCP) ? "TCP" : (r->protocol == RULE_PROTOCOL_UDP ? "UDP" : "BOTH");
+ const char *act = (r->action == RULE_ACTION_PROXY) ? "PROXY" : (r->action == RULE_ACTION_DIRECT ? "DIRECT" : "BLOCK");
+ fprintf(f, " {\n \"processNames\": \"%s\",\n \"targetHosts\": \"%s\",\n \"targetPorts\": \"%s\",\n \"protocol\": \"%s\",\n \"action\": \"%s\",\n \"enabled\": %s\n }", proc, host, port, proto, act, r->enabled ? "true" : "false");
+ g_free(proc); g_free(host); g_free(port);
+ first = false;
+ }
+ fprintf(f, "\n]\n");
+ fclose(f);
+ show_message(NULL, GTK_MESSAGE_INFO, "Rules exported successfully.");
+ }
+ g_free(filename);
+ }
+ gtk_widget_destroy(dialog);
+}
+
+static void on_rule_import_clicked(GtkWidget *widget, gpointer data) {
+ GtkWidget *dialog = gtk_file_chooser_dialog_new("Import Rules", GTK_WINDOW(window), GTK_FILE_CHOOSER_ACTION_OPEN, "_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL);
+ if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
+ char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
+ char *content = NULL;
+ gsize len;
+ if (g_file_get_contents(filename, &content, &len, NULL)) {
+ char *curr = content;
+ int imported = 0;
+ while ((curr = strchr(curr, '{')) != NULL) {
+ char *end = strchr(curr, '}');
+ if (!end) break;
+ char saved = *end; *end = '\0';
+
+ char *proc = extract_sub_json_str(curr, "processNames");
+ char *host = extract_sub_json_str(curr, "targetHosts");
+ char *port = extract_sub_json_str(curr, "targetPorts");
+ char *proto_s = extract_sub_json_str(curr, "protocol");
+ char *act_s = extract_sub_json_str(curr, "action");
+ bool en = extract_sub_json_bool(curr, "enabled");
+
+ if (proc && host && port && proto_s && act_s) {
+ RuleProtocol p = RULE_PROTOCOL_BOTH;
+ if (strcmp(proto_s, "TCP") == 0) p = RULE_PROTOCOL_TCP;
+ else if (strcmp(proto_s, "UDP") == 0) p = RULE_PROTOCOL_UDP;
+ RuleAction a = RULE_ACTION_PROXY;
+ if (strcmp(act_s, "DIRECT") == 0) a = RULE_ACTION_DIRECT;
+ else if (strcmp(act_s, "BLOCK") == 0) a = RULE_ACTION_BLOCK;
+
+ uint32_t nid = ProxyBridge_AddRule(proc, host, port, p, a);
+ if (!en) ProxyBridge_DisableRule(nid);
+ RuleData *nd = malloc(sizeof(RuleData));
+ nd->id = nid; nd->process_name = strdup(proc); nd->target_hosts = strdup(host);
+ nd->target_ports = strdup(port); nd->protocol = p; nd->action = a; nd->enabled = en; nd->selected = false;
+ g_rules_list = g_list_append(g_rules_list, nd);
+ imported++;
+ }
+ g_free(proc); g_free(host); g_free(port); g_free(proto_s); g_free(act_s);
+ *end = saved; curr = end + 1;
+ }
+ g_free(content);
+ if (imported > 0) { refresh_rules_ui(); show_message(NULL, GTK_MESSAGE_INFO, "Imported %d rules.", imported); }
+ }
+ g_free(filename);
+ }
+ gtk_widget_destroy(dialog);
+}
+
+static void on_bulk_delete_clicked(GtkWidget *widget, gpointer data) {
+ if (!g_rules_list) return;
+ GList *iter = g_rules_list;
+ GList *to_delete = NULL;
+ while (iter != NULL) {
+ RuleData *rule = (RuleData *)iter->data;
+ if (rule->selected) to_delete = g_list_append(to_delete, rule);
+ iter = iter->next;
+ }
+ if (!to_delete) return;
+ for (GList *d = to_delete; d != NULL; d = d->next) {
+ RuleData *rule = (RuleData *)d->data;
+ ProxyBridge_DeleteRule(rule->id);
+ g_rules_list = g_list_remove(g_rules_list, rule);
+ free_rule_data(rule);
+ }
+ g_list_free(to_delete);
+ refresh_rules_ui();
+}
+
+static void on_select_all_clicked(GtkWidget *widget, gpointer data) {
+ if (!g_rules_list) return;
+ bool all_selected = true;
+ for (GList *l = g_rules_list; l != NULL; l = l->next) {
+ if (!((RuleData *)l->data)->selected) { all_selected = false; break; }
+ }
+ bool new_state = !all_selected;
+ for (GList *l = g_rules_list; l != NULL; l = l->next) ((RuleData *)l->data)->selected = new_state;
+ refresh_rules_ui();
+}
+
+static void refresh_rules_ui() {
+ if (!rules_list_box) return;
+ GList *children = gtk_container_get_children(GTK_CONTAINER(rules_list_box));
+ for(GList *iter = children; iter != NULL; iter = g_list_next(iter)) gtk_widget_destroy(GTK_WIDGET(iter->data));
+ g_list_free(children);
+
+ if (btn_select_all_header) {
+ bool all_selected = (g_rules_list != NULL);
+ if (g_rules_list == NULL) all_selected = false;
+ for (GList *l = g_rules_list; l != NULL; l = l->next) {
+ if (!((RuleData *)l->data)->selected) { all_selected = false; break; }
+ }
+ gtk_button_set_label(GTK_BUTTON(btn_select_all_header), all_selected ? "Deselect All" : "Select All");
+ }
+
+ GtkWidget *grid = gtk_grid_new();
+ gtk_grid_set_row_spacing(GTK_GRID(grid), 10);
+ gtk_grid_set_column_spacing(GTK_GRID(grid), 15);
+ gtk_container_add(GTK_CONTAINER(rules_list_box), grid);
+
+ int row = 0;
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new(" "), 0, row, 1, 1);
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Enable"), 1, row, 1, 1);
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Actions"), 2, row, 1, 1);
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new("SR"), 3, row, 1, 1);
+ GtkWidget *h_proc = gtk_label_new("Process"); gtk_widget_set_halign(h_proc, GTK_ALIGN_START);
+ gtk_widget_set_hexpand(h_proc, TRUE); gtk_grid_attach(GTK_GRID(grid), h_proc, 4, row, 1, 1);
+ GtkWidget *h_host = gtk_label_new("Target Hosts"); gtk_widget_set_halign(h_host, GTK_ALIGN_START);
+ gtk_widget_set_hexpand(h_host, TRUE); gtk_grid_attach(GTK_GRID(grid), h_host, 5, row, 1, 1);
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Protocol"), 6, row, 1, 1);
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Action"), 7, row, 1, 1);
+ row++;
+ gtk_grid_attach(GTK_GRID(grid), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row, 8, 1); row++;
+
+ int sr_counter = 1;
+ for (GList *l = g_rules_list; l != NULL; l = l->next) {
+ RuleData *r = (RuleData *)l->data;
+ GtkWidget *chk_sel = gtk_check_button_new();
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(chk_sel), r->selected);
+ g_signal_connect(chk_sel, "toggled", G_CALLBACK(on_rule_select_toggle), r);
+ gtk_grid_attach(GTK_GRID(grid), chk_sel, 0, row, 1, 1);
+
+ GtkWidget *chk = gtk_check_button_new();
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(chk), r->enabled);
+ g_signal_connect(chk, "toggled", G_CALLBACK(on_rule_toggle), r);
+ gtk_grid_attach(GTK_GRID(grid), chk, 1, row, 1, 1);
+
+ GtkWidget *act_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2);
+ GtkWidget *btn_edit = gtk_button_new_with_label("Edit");
+ g_signal_connect(btn_edit, "clicked", G_CALLBACK(on_rule_edit), r);
+ GtkWidget *btn_del = gtk_button_new_with_label("Delete");
+ g_signal_connect(btn_del, "clicked", G_CALLBACK(on_rule_delete), r);
+ gtk_box_pack_start(GTK_BOX(act_box), btn_edit, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(act_box), btn_del, FALSE, FALSE, 0);
+ gtk_grid_attach(GTK_GRID(grid), act_box, 2, row, 1, 1);
+
+ char sr_str[16]; snprintf(sr_str, sizeof(sr_str), "%d", sr_counter++);
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new(sr_str), 3, row, 1, 1);
+
+ GtkWidget *l_proc = gtk_label_new(r->process_name); gtk_widget_set_halign(l_proc, GTK_ALIGN_START);
+ gtk_label_set_ellipsize(GTK_LABEL(l_proc), PANGO_ELLIPSIZE_END);
+ gtk_grid_attach(GTK_GRID(grid), l_proc, 4, row, 1, 1);
+
+ GtkWidget *l_host = gtk_label_new(r->target_hosts); gtk_widget_set_halign(l_host, GTK_ALIGN_START);
+ gtk_label_set_ellipsize(GTK_LABEL(l_host), PANGO_ELLIPSIZE_END);
+ gtk_grid_attach(GTK_GRID(grid), l_host, 5, row, 1, 1);
+
+ const char* proto_strs[] = {"TCP", "UDP", "BOTH"};
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new(proto_strs[r->protocol]), 6, row, 1, 1);
+
+ const char* action_strs[] = {"PROXY", "DIRECT", "BLOCK"};
+ GtkWidget *l_act = gtk_label_new(action_strs[r->action]);
+ GtkStyleContext *context = gtk_widget_get_style_context(l_act);
+ if (r->action == RULE_ACTION_PROXY) gtk_style_context_add_class(context, "success");
+ else if (r->action == RULE_ACTION_DIRECT) gtk_style_context_add_class(context, "info");
+ else gtk_style_context_add_class(context, "warning");
+ gtk_grid_attach(GTK_GRID(grid), l_act, 7, row, 1, 1);
+
+ row++;
+ gtk_grid_attach(GTK_GRID(grid), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, row, 8, 1); row++;
+ }
+ gtk_widget_show_all(rules_list_box);
+}
+
+void on_proxy_rules_clicked(GtkWidget *widget, gpointer data) {
+ GtkWidget *dialog = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_title(GTK_WINDOW(dialog), "Process Rules");
+ gtk_window_set_default_size(GTK_WINDOW(dialog), 800, 500);
+ gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(window));
+ GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
+ gtk_container_set_border_width(GTK_CONTAINER(vbox), 20);
+ gtk_container_add(GTK_CONTAINER(dialog), vbox);
+
+ GtkWidget *header_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ GtkWidget *title = gtk_label_new(NULL);
+ gtk_label_set_markup(GTK_LABEL(title), "Process Rules ");
+ GtkWidget *add_btn = gtk_button_new_with_label("+ Add Rule");
+ g_signal_connect(add_btn, "clicked", G_CALLBACK(on_rule_add_clicked), NULL);
+ btn_select_all_header = gtk_button_new_with_label("Select All");
+ g_signal_connect(btn_select_all_header, "clicked", G_CALLBACK(on_select_all_clicked), NULL);
+ GtkWidget *del_all_btn = gtk_button_new_with_label("Delete Selected");
+ g_signal_connect(del_all_btn, "clicked", G_CALLBACK(on_bulk_delete_clicked), NULL);
+ GtkWidget *import_btn = gtk_button_new_with_label("Import");
+ g_signal_connect(import_btn, "clicked", G_CALLBACK(on_rule_import_clicked), NULL);
+ GtkWidget *export_btn = gtk_button_new_with_label("Export");
+ g_signal_connect(export_btn, "clicked", G_CALLBACK(on_rule_export_clicked), NULL);
+
+ gtk_box_pack_start(GTK_BOX(header_box), title, FALSE, FALSE, 0);
+ GtkWidget *spacer = gtk_label_new("");
+ gtk_widget_set_hexpand(spacer, TRUE);
+ gtk_box_pack_start(GTK_BOX(header_box), spacer, TRUE, TRUE, 0);
+ gtk_box_pack_end(GTK_BOX(header_box), add_btn, FALSE, FALSE, 5);
+ gtk_box_pack_end(GTK_BOX(header_box), btn_select_all_header, FALSE, FALSE, 5);
+ gtk_box_pack_end(GTK_BOX(header_box), del_all_btn, FALSE, FALSE, 5);
+ gtk_box_pack_end(GTK_BOX(header_box), export_btn, FALSE, FALSE, 5);
+ gtk_box_pack_end(GTK_BOX(header_box), import_btn, FALSE, FALSE, 5);
+ gtk_box_pack_start(GTK_BOX(vbox), header_box, FALSE, FALSE, 0);
+
+ GtkWidget *scrolled = gtk_scrolled_window_new(NULL, NULL);
+ gtk_widget_set_vexpand(scrolled, TRUE);
+ rules_list_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
+ gtk_container_add(GTK_CONTAINER(scrolled), rules_list_box);
+ gtk_box_pack_start(GTK_BOX(vbox), scrolled, TRUE, TRUE, 0);
+
+ refresh_rules_ui();
+ gtk_widget_show_all(dialog);
+}
diff --git a/Linux/gui/gui_settings.c b/Linux/gui/gui_settings.c
new file mode 100644
index 0000000..3268f9c
--- /dev/null
+++ b/Linux/gui/gui_settings.c
@@ -0,0 +1,221 @@
+#include "gui.h"
+
+// settings stuff
+
+static gboolean on_test_done(gpointer user_data) {
+ TestResultData *data = (TestResultData *)user_data;
+ gtk_text_buffer_set_text(data->buffer, data->result_text, -1);
+ gtk_widget_set_sensitive(data->btn, TRUE);
+
+ free(data->result_text);
+ free(data);
+ return FALSE;
+}
+
+static gpointer run_test_thread(gpointer user_data) {
+ struct TestRunnerData *req = (struct TestRunnerData *)user_data;
+
+ char *buffer = malloc(4096);
+ memset(buffer, 0, 4096);
+
+ // run test
+ ProxyBridge_TestConnection(req->host, req->port, buffer, 4096);
+
+ TestResultData *res = malloc(sizeof(TestResultData));
+ res->result_text = buffer;
+ res->buffer = req->ui_info->output_buffer;
+ res->btn = req->ui_info->test_btn;
+
+ g_idle_add(on_test_done, res);
+
+ free(req->host);
+ free(req);
+ return NULL;
+}
+
+static void on_start_test_clicked(GtkWidget *widget, gpointer data) {
+ ConfigInfo *info = (ConfigInfo *)data;
+
+ // check inputs
+ const char *ip_text = gtk_entry_get_text(GTK_ENTRY(info->ip_entry));
+ const char *port_text = gtk_entry_get_text(GTK_ENTRY(info->port_entry));
+
+ if (!ip_text || strlen(ip_text) == 0 || strspn(port_text, "0123456789") != strlen(port_text) || strlen(port_text) == 0) {
+ gtk_text_buffer_set_text(info->output_buffer, "Error: Invalid Proxy IP or Port.", -1);
+ return;
+ }
+
+ // save config
+ ProxyType type = (gtk_combo_box_get_active(GTK_COMBO_BOX(info->type_combo)) == 0) ? PROXY_TYPE_HTTP : PROXY_TYPE_SOCKS5;
+ int port = (int)safe_strtol(port_text);
+ const char *user = gtk_entry_get_text(GTK_ENTRY(info->user_entry));
+ const char *pass = gtk_entry_get_text(GTK_ENTRY(info->pass_entry));
+
+ ProxyBridge_SetProxyConfig(type, ip_text, port, user, pass);
+
+ // get target
+ const char *t_host = gtk_entry_get_text(GTK_ENTRY(info->test_host));
+ const char *t_port_s = gtk_entry_get_text(GTK_ENTRY(info->test_port));
+
+ if (!t_host || strlen(t_host) == 0) t_host = "google.com";
+ int t_port = (int)safe_strtol(t_port_s);
+ if (t_port <= 0) t_port = 80;
+
+ // update gui
+ gtk_text_buffer_set_text(info->output_buffer, "Testing connection... Please wait...", -1);
+ gtk_widget_set_sensitive(info->test_btn, FALSE);
+
+ // start thread
+ struct TestRunnerData *req = malloc(sizeof(struct TestRunnerData));
+ req->host = strdup(t_host);
+ req->port = t_port;
+ req->ui_info = info;
+
+ GThread *thread = g_thread_new("test_conn", run_test_thread, req);
+ g_thread_unref(thread);
+}
+
+void on_proxy_configure(GtkWidget *widget, gpointer data) {
+ ConfigInfo info;
+
+ GtkWidget *content_area;
+ GtkWidget *grid;
+
+ info.dialog = gtk_dialog_new_with_buttons("Proxy Settings",
+ GTK_WINDOW(window),
+ GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
+ "Cancel", GTK_RESPONSE_CANCEL,
+ "Save", GTK_RESPONSE_ACCEPT,
+ NULL);
+ // wider window
+ gtk_window_set_default_size(GTK_WINDOW(info.dialog), 600, 500);
+
+ content_area = gtk_dialog_get_content_area(GTK_DIALOG(info.dialog));
+ grid = gtk_grid_new();
+ gtk_grid_set_row_spacing(GTK_GRID(grid), 5);
+ gtk_grid_set_column_spacing(GTK_GRID(grid), 10);
+ gtk_container_set_border_width(GTK_CONTAINER(grid), 10);
+ gtk_box_pack_start(GTK_BOX(content_area), grid, TRUE, TRUE, 0);
+
+ // proxy type dropdown
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Type:"), 0, 0, 1, 1);
+ info.type_combo = gtk_combo_box_text_new();
+ gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(info.type_combo), "HTTP");
+ gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(info.type_combo), "SOCKS5");
+ gtk_combo_box_set_active(GTK_COMBO_BOX(info.type_combo), g_proxy_type == PROXY_TYPE_HTTP ? 0 : 1);
+ gtk_grid_attach(GTK_GRID(grid), info.type_combo, 1, 0, 3, 1);
+
+ // ip field
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Host:"), 0, 1, 1, 1);
+ info.ip_entry = gtk_entry_new();
+ gtk_entry_set_text(GTK_ENTRY(info.ip_entry), g_proxy_ip);
+ gtk_widget_set_hexpand(info.ip_entry, TRUE);
+ gtk_grid_attach(GTK_GRID(grid), info.ip_entry, 1, 1, 3, 1);
+
+ // port field
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Port:"), 0, 2, 1, 1);
+ info.port_entry = gtk_entry_new();
+ if (g_proxy_port != 0) {
+ char port_str[16];
+ snprintf(port_str, sizeof(port_str), "%d", g_proxy_port);
+ gtk_entry_set_text(GTK_ENTRY(info.port_entry), port_str);
+ }
+ gtk_grid_attach(GTK_GRID(grid), info.port_entry, 1, 2, 3, 1);
+
+ // user field
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Username:"), 0, 3, 1, 1);
+ info.user_entry = gtk_entry_new();
+ gtk_entry_set_text(GTK_ENTRY(info.user_entry), g_proxy_user);
+ gtk_grid_attach(GTK_GRID(grid), info.user_entry, 1, 3, 3, 1);
+
+ // password
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Password:"), 0, 4, 1, 1);
+ info.pass_entry = gtk_entry_new();
+ gtk_entry_set_visibility(GTK_ENTRY(info.pass_entry), FALSE);
+ gtk_entry_set_text(GTK_ENTRY(info.pass_entry), g_proxy_pass);
+ gtk_grid_attach(GTK_GRID(grid), info.pass_entry, 1, 4, 3, 1);
+
+ // test section
+ gtk_grid_attach(GTK_GRID(grid), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), 0, 5, 4, 1);
+
+ GtkWidget *test_label = gtk_label_new("Test Connection ");
+ gtk_label_set_use_markup(GTK_LABEL(test_label), TRUE);
+ gtk_widget_set_halign(test_label, GTK_ALIGN_START);
+ gtk_grid_attach(GTK_GRID(grid), test_label, 0, 6, 4, 1);
+
+ // target
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Target:"), 0, 7, 1, 1);
+ info.test_host = gtk_entry_new();
+ gtk_entry_set_text(GTK_ENTRY(info.test_host), "google.com");
+ gtk_grid_attach(GTK_GRID(grid), info.test_host, 1, 7, 1, 1);
+
+ // target port
+ gtk_grid_attach(GTK_GRID(grid), gtk_label_new("Port:"), 2, 7, 1, 1);
+ info.test_port = gtk_entry_new();
+ gtk_entry_set_text(GTK_ENTRY(info.test_port), "80");
+ gtk_widget_set_size_request(info.test_port, 80, -1);
+ gtk_grid_attach(GTK_GRID(grid), info.test_port, 3, 7, 1, 1);
+
+ // test button
+ info.test_btn = gtk_button_new_with_label("Start Test");
+ g_signal_connect(info.test_btn, "clicked", G_CALLBACK(on_start_test_clicked), &info);
+ gtk_grid_attach(GTK_GRID(grid), info.test_btn, 0, 8, 4, 1);
+
+ // output log
+ GtkWidget *out_scroll = gtk_scrolled_window_new(NULL, NULL);
+ gtk_widget_set_size_request(out_scroll, -1, 150);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(out_scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
+
+ GtkWidget *out_view = gtk_text_view_new();
+ gtk_text_view_set_editable(GTK_TEXT_VIEW(out_view), FALSE);
+ gtk_text_view_set_monospace(GTK_TEXT_VIEW(out_view), TRUE);
+ info.output_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(out_view));
+
+ gtk_container_add(GTK_CONTAINER(out_scroll), out_view);
+ gtk_grid_attach(GTK_GRID(grid), out_scroll, 0, 9, 4, 1);
+
+
+ gtk_widget_show_all(info.dialog);
+
+ while (TRUE) {
+ if (gtk_dialog_run(GTK_DIALOG(info.dialog)) != GTK_RESPONSE_ACCEPT) break;
+
+ // check
+ const char *ip_text = gtk_entry_get_text(GTK_ENTRY(info.ip_entry));
+ const char *port_text = gtk_entry_get_text(GTK_ENTRY(info.port_entry));
+
+ if (!ip_text || strlen(ip_text) == 0) {
+ show_message(GTK_WINDOW(info.dialog), GTK_MESSAGE_ERROR, "Host (IP/Domain) cannot be empty.");
+ continue;
+ }
+
+ if (strspn(port_text, "0123456789") != strlen(port_text) || strlen(port_text) == 0) {
+ show_message(GTK_WINDOW(info.dialog), GTK_MESSAGE_ERROR, "Port must be a valid number.");
+ continue;
+ }
+
+ int p = (int)safe_strtol(port_text);
+ if (p < 1 || p > 65535) {
+ show_message(GTK_WINDOW(info.dialog), GTK_MESSAGE_ERROR, "Port must be between 1 and 65535.");
+ continue;
+ }
+
+ // save
+ g_proxy_type = (gtk_combo_box_get_active(GTK_COMBO_BOX(info.type_combo)) == 0) ? PROXY_TYPE_HTTP : PROXY_TYPE_SOCKS5;
+ g_strlcpy(g_proxy_ip, ip_text, sizeof(g_proxy_ip));
+ g_proxy_port = p;
+ g_strlcpy(g_proxy_user, gtk_entry_get_text(GTK_ENTRY(info.user_entry)), sizeof(g_proxy_user));
+ g_strlcpy(g_proxy_pass, gtk_entry_get_text(GTK_ENTRY(info.pass_entry)), sizeof(g_proxy_pass));
+
+ ProxyBridge_SetProxyConfig(g_proxy_type, g_proxy_ip, g_proxy_port, g_proxy_user, g_proxy_pass);
+
+ save_config();
+
+ char status_msg[512];
+ snprintf(status_msg, sizeof(status_msg), "Configuration updated: %s:%d", g_proxy_ip, g_proxy_port);
+ gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, status_msg);
+ break;
+ }
+
+ gtk_widget_destroy(info.dialog);
+}
diff --git a/Linux/gui/gui_utils.c b/Linux/gui/gui_utils.c
new file mode 100644
index 0000000..ae69d29
--- /dev/null
+++ b/Linux/gui/gui_utils.c
@@ -0,0 +1,91 @@
+#include "gui.h"
+
+// safer way to turn string to int
+long safe_strtol(const char *nptr) {
+ if (!nptr) return 0; // null check
+ char *endptr;
+ long val = strtol(nptr, &endptr, 10);
+ if (endptr == nptr) return 0; // bad input
+ return val;
+}
+
+// show popup msg
+void show_message(GtkWindow *parent, GtkMessageType type, const char *format, ...) {
+ va_list args;
+ va_start(args, format);
+ char *msg = g_strdup_vprintf(format, args);
+ va_end(args);
+
+ GtkWidget *dialog = gtk_message_dialog_new(parent,
+ GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
+ type,
+ GTK_BUTTONS_OK,
+ "%s", msg);
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ g_free(msg);
+}
+
+// keep log size under control so we dont use too much ram
+void trim_buffer_lines(GtkTextBuffer *buffer, int max_lines) {
+ if (gtk_text_buffer_get_line_count(buffer) > max_lines) {
+ GtkTextIter start, next;
+ gtk_text_buffer_get_start_iter(buffer, &start);
+ next = start;
+ gtk_text_iter_forward_line(&next);
+ gtk_text_buffer_delete(buffer, &start, &next);
+ }
+}
+
+char* get_current_time_str() {
+ time_t rawtime;
+ struct tm *timeinfo;
+ char *buffer = malloc(32);
+ time(&rawtime);
+ timeinfo = localtime(&rawtime);
+ strftime(buffer, 32, "[%H:%M:%S]", timeinfo);
+ return buffer;
+}
+
+// json stuff
+char *escape_json_string(const char *src) {
+ if (!src) return strdup("");
+ GString *str = g_string_new("");
+ for (const char *p = src; *p; p++) {
+ if (*p == '\\') g_string_append(str, "\\\\");
+ else if (*p == '"') g_string_append(str, "\\\"");
+ else if (*p == '\n') g_string_append(str, "\\n");
+ else g_string_append_c(str, *p);
+ }
+ return g_string_free(str, FALSE);
+}
+
+// basic parser, good enough for valid inputs
+char *extract_sub_json_str(const char *json, const char *key) {
+ char search_key[256];
+ snprintf(search_key, sizeof(search_key), "\"%s\"", key);
+ char *k = strstr(json, search_key);
+ if (!k) return NULL;
+ char *colon = strchr(k, ':');
+ if (!colon) return NULL;
+ char *val_start = strchr(colon, '"');
+ if (!val_start) return NULL;
+ val_start++;
+ char *val_end = strchr(val_start, '"');
+ if (!val_end) return NULL;
+ return g_strndup(val_start, val_end - val_start);
+}
+
+bool extract_sub_json_bool(const char *json, const char *key) {
+ char search_key[256];
+ snprintf(search_key, sizeof(search_key), "\"%s\"", key);
+ char *k = strstr(json, search_key);
+ if (!k) return false;
+ char *colon = strchr(k, ':');
+ if (!colon) return false;
+ // skip whitespace
+ char *v = colon + 1;
+ while(*v == ' ' || *v == '\t') v++;
+ if (strncmp(v, "true", 4) == 0) return true;
+ return false;
+}
diff --git a/Linux/gui/main.c b/Linux/gui/main.c
new file mode 100644
index 0000000..c51d5ce
--- /dev/null
+++ b/Linux/gui/main.c
@@ -0,0 +1,268 @@
+#include "gui.h"
+
+// widgets
+GtkWidget *window;
+GtkTextView *conn_view;
+GtkTextBuffer *conn_buffer;
+GtkTextView *log_view;
+GtkTextBuffer *log_buffer;
+GtkWidget *status_bar;
+guint status_context_id;
+
+// default config
+char g_proxy_ip[256] = "";
+uint16_t g_proxy_port = 0;
+ProxyType g_proxy_type = PROXY_TYPE_SOCKS5;
+char g_proxy_user[256] = "";
+char g_proxy_pass[256] = "";
+
+GList *g_rules_list = NULL;
+bool g_chk_logging = true;
+bool g_chk_dns = true;
+
+static void on_log_traffic_toggled(GtkCheckMenuItem *item, gpointer data) {
+ bool active = gtk_check_menu_item_get_active(item);
+ ProxyBridge_SetTrafficLoggingEnabled(active);
+ g_chk_logging = active;
+ save_config();
+}
+
+static void on_dns_proxy_toggled(GtkCheckMenuItem *item, gpointer data) {
+ bool active = gtk_check_menu_item_get_active(item);
+ ProxyBridge_SetDnsViaProxy(active);
+ g_chk_dns = active;
+ save_config();
+}
+
+static void on_create_update_script_and_run() {
+ ProxyBridge_Stop();
+ const char *script_url = "https://raw.githubusercontent.com/InterceptSuite/ProxyBridge/refs/heads/master/Linux/deploy.sh";
+ char tmp_dir_tpl[] = "/tmp/pb_update_XXXXXX";
+ char *tmp_dir = mkdtemp(tmp_dir_tpl);
+ if (!tmp_dir) { fprintf(stderr, "Failed to create temp directory for update.\n"); exit(1); }
+ char script_path[512];
+ snprintf(script_path, sizeof(script_path), "%s/deploy.sh", tmp_dir);
+
+ pid_t pid = fork();
+ if (pid == -1) { fprintf(stderr, "Fork failed.\n"); exit(1); }
+ else if (pid == 0) { execlp("curl", "curl", "-s", "-o", script_path, script_url, NULL); _exit(127); }
+ else { int status; waitpid(pid, &status, 0); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { fprintf(stderr, "Failed to download update script.\n"); exit(1); } }
+
+ if (chmod(script_path, S_IRWXU) != 0) { perror("chmod failed"); exit(1); }
+ execl("/bin/bash", "bash", script_path, NULL);
+ exit(0);
+}
+
+static void on_check_update(GtkWidget *widget, gpointer data) {
+ const char *url = "https://api.github.com/repos/InterceptSuite/ProxyBridge/releases/latest";
+ char *cmd = g_strdup_printf("curl -s -H \"User-Agent: ProxyBridge-Linux\" %s", url);
+ char *standard_output = NULL;
+ char *standard_error = NULL;
+ GError *error = NULL;
+ int exit_status = 0;
+
+ gboolean result = g_spawn_command_line_sync(cmd, &standard_output, &standard_error, &exit_status, &error);
+ g_free(cmd);
+ if (!result) { show_message(GTK_WINDOW(window), GTK_MESSAGE_ERROR, "Failed to launch release check: %s", error ? error->message : "Unknown"); if (error) g_error_free(error); return; }
+ if (exit_status != 0 || !standard_output || strlen(standard_output) == 0) { show_message(GTK_WINDOW(window), GTK_MESSAGE_ERROR, "Update check failed (Exit: %d).", exit_status); g_free(standard_output); g_free(standard_error); return; }
+
+ char *tag_name = extract_sub_json_str(standard_output, "tag_name");
+ g_free(standard_output); g_free(standard_error);
+
+ if (!tag_name) { show_message(GTK_WINDOW(window), GTK_MESSAGE_WARNING, "Could not parse version info."); return; }
+ char *current_tag = g_strdup_printf("v%s", PROXYBRIDGE_VERSION);
+
+ if (strcmp(tag_name, current_tag) == 0) { show_message(GTK_WINDOW(window), GTK_MESSAGE_INFO, "You are using the latest version (%s).", PROXYBRIDGE_VERSION); }
+ else {
+ GtkWidget *dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, "New version %s is available!\nCurrent: %s\n\nUpdate now?", tag_name, PROXYBRIDGE_VERSION);
+ gtk_dialog_add_button(GTK_DIALOG(dialog), "Download Now", GTK_RESPONSE_ACCEPT);
+ gtk_dialog_add_button(GTK_DIALOG(dialog), "Close", GTK_RESPONSE_CANCEL);
+ int resp = gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ if (resp == GTK_RESPONSE_ACCEPT) on_create_update_script_and_run();
+ }
+ g_free(current_tag); g_free(tag_name);
+}
+
+static void on_about(GtkWidget *widget, gpointer data) {
+ GtkWidget *dialog = gtk_dialog_new_with_buttons("About ProxyBridge", GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, "OK", GTK_RESPONSE_OK, NULL);
+ gtk_window_set_default_size(GTK_WINDOW(dialog), 400, 300);
+ GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
+ gtk_container_set_border_width(GTK_CONTAINER(content_area), 20);
+ char *markup = g_strdup_printf(
+ "ProxyBridge \n"
+ "Version %s \n\n"
+ "Universal proxy client for Linux applications\n\n"
+ "Author: Sourav Kalal / InterceptSuite\n\n"
+ "Website: interceptsuite.com \n"
+ "GitHub: InterceptSuite/ProxyBridge \n\n"
+ "License: MIT", PROXYBRIDGE_VERSION);
+ GtkWidget *label = gtk_label_new(NULL);
+ gtk_label_set_markup(GTK_LABEL(label), markup);
+ gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER);
+ g_free(markup);
+ gtk_box_pack_start(GTK_BOX(content_area), label, TRUE, TRUE, 0);
+ gtk_widget_show_all(dialog);
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+}
+
+static void signal_handler(int sig) {
+ fprintf(stderr, "\nSignal %d received. Stopping ProxyBridge...\n", sig);
+ ProxyBridge_Stop();
+ exit(sig);
+}
+
+static void on_window_destroy(GtkWidget *widget, gpointer data) {
+ ProxyBridge_Stop();
+ gtk_main_quit();
+}
+
+int main(int argc, char *argv[]) {
+ signal(SIGINT, signal_handler);
+ signal(SIGTERM, signal_handler);
+ signal(SIGSEGV, signal_handler);
+
+ if (getuid() != 0) { gtk_init(&argc, &argv); show_message(NULL, GTK_MESSAGE_ERROR, "ProxyBridge must be run as root (sudo)."); return 1; }
+ setenv("GSETTINGS_BACKEND", "memory", 1);
+
+ // load config from file
+ load_config();
+
+ gtk_init(&argc, &argv);
+
+ GtkSettings *settings = gtk_settings_get_default();
+ if (settings) g_object_set(settings, "gtk-application-prefer-dark-theme", TRUE, NULL);
+
+ window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_title(GTK_WINDOW(window), "ProxyBridge");
+ gtk_window_set_default_size(GTK_WINDOW(window), 900, 600);
+ g_signal_connect(window, "destroy", G_CALLBACK(on_window_destroy), NULL);
+
+ GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_container_add(GTK_CONTAINER(window), vbox);
+
+ // setup menu
+ GtkWidget *menubar = gtk_menu_bar_new();
+ GtkWidget *proxy_menu_item = gtk_menu_item_new_with_label("Proxy");
+ GtkWidget *proxy_menu = gtk_menu_new();
+ GtkWidget *config_item = gtk_menu_item_new_with_label("Proxy Settings");
+ GtkWidget *rules_item = gtk_menu_item_new_with_label("Proxy Rules");
+
+ GtkWidget *log_check_item = gtk_check_menu_item_new_with_label("Enable Traffic Logging");
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(log_check_item), g_chk_logging);
+ g_signal_connect(log_check_item, "toggled", G_CALLBACK(on_log_traffic_toggled), NULL);
+
+ GtkWidget *dns_check_item = gtk_check_menu_item_new_with_label("DNS via Proxy");
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(dns_check_item), g_chk_dns);
+ g_signal_connect(dns_check_item, "toggled", G_CALLBACK(on_dns_proxy_toggled), NULL);
+
+ GtkWidget *exit_item = gtk_menu_item_new_with_label("Exit");
+
+ g_signal_connect(config_item, "activate", G_CALLBACK(on_proxy_configure), NULL);
+ g_signal_connect(rules_item, "activate", G_CALLBACK(on_proxy_rules_clicked), NULL);
+ g_signal_connect(exit_item, "activate", G_CALLBACK(on_window_destroy), NULL);
+
+ gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), config_item);
+ gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), rules_item);
+ gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), gtk_separator_menu_item_new());
+ gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), log_check_item);
+ gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), dns_check_item);
+ gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), gtk_separator_menu_item_new());
+ gtk_menu_shell_append(GTK_MENU_SHELL(proxy_menu), exit_item);
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(proxy_menu_item), proxy_menu);
+
+ GtkWidget *about_menu_item = gtk_menu_item_new_with_label("About");
+ GtkWidget *about_menu = gtk_menu_new();
+ GtkWidget *about_child_item = gtk_menu_item_new_with_label("About");
+ g_signal_connect(about_child_item, "activate", G_CALLBACK(on_about), NULL);
+ GtkWidget *update_item = gtk_menu_item_new_with_label("Check for Updates");
+ g_signal_connect(update_item, "activate", G_CALLBACK(on_check_update), NULL);
+ gtk_menu_shell_append(GTK_MENU_SHELL(about_menu), about_child_item);
+ gtk_menu_shell_append(GTK_MENU_SHELL(about_menu), update_item);
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(about_menu_item), about_menu);
+
+ gtk_menu_shell_append(GTK_MENU_SHELL(menubar), proxy_menu_item);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menubar), about_menu_item);
+ gtk_box_pack_start(GTK_BOX(vbox), menubar, FALSE, FALSE, 0);
+
+ // tabs
+ GtkWidget *notebook = gtk_notebook_new();
+ gtk_box_pack_start(GTK_BOX(vbox), notebook, TRUE, TRUE, 0);
+
+ // connections tab
+ GtkWidget *conn_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
+ gtk_container_set_border_width(GTK_CONTAINER(conn_vbox), 5);
+ GtkWidget *conn_toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
+ GtkWidget *conn_search = gtk_search_entry_new();
+ gtk_entry_set_placeholder_text(GTK_ENTRY(conn_search), "Search connections...");
+ g_signal_connect(conn_search, "search-changed", G_CALLBACK(on_search_conn_changed), NULL);
+ GtkWidget *conn_clear_btn = gtk_button_new_with_label("Clear Logs");
+ g_signal_connect(conn_clear_btn, "clicked", G_CALLBACK(on_clear_conn_clicked), NULL);
+ gtk_box_pack_start(GTK_BOX(conn_toolbar), conn_search, TRUE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(conn_toolbar), conn_clear_btn, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(conn_vbox), conn_toolbar, FALSE, FALSE, 0);
+ conn_view = GTK_TEXT_VIEW(gtk_text_view_new());
+ gtk_text_view_set_editable(conn_view, FALSE);
+ gtk_text_view_set_cursor_visible(conn_view, FALSE);
+ conn_buffer = gtk_text_view_get_buffer(conn_view);
+ gtk_text_buffer_create_tag(conn_buffer, "hidden", "invisible", TRUE, NULL);
+ GtkWidget *scrolled_window = gtk_scrolled_window_new(NULL, NULL);
+ gtk_container_add(GTK_CONTAINER(scrolled_window), GTK_WIDGET(conn_view));
+ gtk_box_pack_start(GTK_BOX(conn_vbox), scrolled_window, TRUE, TRUE, 0);
+ gtk_notebook_append_page(GTK_NOTEBOOK(notebook), conn_vbox, gtk_label_new("Connections"));
+
+ // logs tab
+ GtkWidget *log_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
+ gtk_container_set_border_width(GTK_CONTAINER(log_vbox), 5);
+ GtkWidget *log_toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
+ GtkWidget *log_search = gtk_search_entry_new();
+ gtk_entry_set_placeholder_text(GTK_ENTRY(log_search), "Search logs...");
+ g_signal_connect(log_search, "search-changed", G_CALLBACK(on_search_log_changed), NULL);
+ GtkWidget *log_clear_btn = gtk_button_new_with_label("Clear Logs");
+ g_signal_connect(log_clear_btn, "clicked", G_CALLBACK(on_clear_log_clicked), NULL);
+ gtk_box_pack_start(GTK_BOX(log_toolbar), log_search, TRUE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(log_toolbar), log_clear_btn, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(log_vbox), log_toolbar, FALSE, FALSE, 0);
+ log_view = GTK_TEXT_VIEW(gtk_text_view_new());
+ gtk_text_view_set_editable(log_view, FALSE);
+ gtk_text_view_set_cursor_visible(log_view, FALSE);
+ log_buffer = gtk_text_view_get_buffer(log_view);
+ gtk_text_buffer_create_tag(log_buffer, "hidden", "invisible", TRUE, NULL);
+ GtkWidget *log_scroll = gtk_scrolled_window_new(NULL, NULL);
+ gtk_container_add(GTK_CONTAINER(log_scroll), GTK_WIDGET(log_view));
+ gtk_box_pack_start(GTK_BOX(log_vbox), log_scroll, TRUE, TRUE, 0);
+ gtk_notebook_append_page(GTK_NOTEBOOK(notebook), log_vbox, gtk_label_new("Activity Logs"));
+
+ // status bar
+ status_bar = gtk_statusbar_new();
+ status_context_id = gtk_statusbar_get_context_id(GTK_STATUSBAR(status_bar), "Status");
+ gtk_box_pack_start(GTK_BOX(vbox), status_bar, FALSE, FALSE, 0);
+
+ // start
+ ProxyBridge_SetLogCallback(lib_log_callback);
+ ProxyBridge_SetConnectionCallback(lib_connection_callback);
+ ProxyBridge_SetTrafficLoggingEnabled(g_chk_logging);
+ ProxyBridge_SetDnsViaProxy(g_chk_dns);
+
+ if (ProxyBridge_Start()) {
+ // apply config
+ ProxyBridge_SetProxyConfig(g_proxy_type, g_proxy_ip, g_proxy_port, g_proxy_user, g_proxy_pass);
+
+ // restore rules
+ for (GList *l = g_rules_list; l != NULL; l = l->next) {
+ RuleData *r = (RuleData *)l->data;
+ r->id = ProxyBridge_AddRule(r->process_name, r->target_hosts, r->target_ports, r->protocol, r->action);
+ if (!r->enabled) ProxyBridge_DisableRule(r->id);
+ }
+
+ gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, "ProxyBridge Service Started.");
+ } else {
+ gtk_statusbar_push(GTK_STATUSBAR(status_bar), status_context_id, "Failed to start ProxyBridge engine.");
+ }
+
+ gtk_widget_show_all(window);
+ gtk_main();
+
+ return 0;
+}
diff --git a/Linux/setup.sh b/Linux/setup.sh
new file mode 100644
index 0000000..a365a16
--- /dev/null
+++ b/Linux/setup.sh
@@ -0,0 +1,237 @@
+#!/bin/bash
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+echo ""
+echo "==================================="
+echo "ProxyBridge Setup Script"
+echo "==================================="
+echo ""
+
+# Check if running as root
+if [ "$EUID" -ne 0 ]; then
+ echo "ERROR: This script must be run as root"
+ echo "Please run: sudo ./setup.sh"
+ exit 1
+fi
+
+# Detect Linux distribution
+detect_distro() {
+ if [ -f /etc/os-release ]; then
+ . /etc/os-release
+ DISTRO=$ID
+ DISTRO_LIKE=$ID_LIKE
+ elif [ -f /etc/lsb-release ]; then
+ . /etc/lsb-release
+ DISTRO=$DISTRIB_ID
+ else
+ DISTRO=$(uname -s)
+ fi
+ echo "Detected distribution: $DISTRO"
+}
+
+# Install dependencies based on distribution
+install_dependencies() {
+ echo ""
+ echo "Checking and installing dependencies..."
+
+ # Normalize distro name using ID_LIKE fallback
+ local distro_family="$DISTRO"
+ if [ -n "$DISTRO_LIKE" ]; then
+ case "$DISTRO_LIKE" in
+ *ubuntu*|*debian*) distro_family="debian" ;;
+ *fedora*) distro_family="fedora" ;;
+ *rhel*|*centos*) distro_family="rhel" ;;
+ *arch*) distro_family="arch" ;;
+ *suse*) distro_family="opensuse" ;;
+ esac
+ fi
+
+ case "$distro_family" in
+ ubuntu|debian|linuxmint|pop|elementary|zorin|kali|raspbian|mx|antix|deepin|lmde)
+ echo "Using apt package manager..."
+ apt-get update -qq
+ # libgtk-3-0 is for runtime, libnetfilter-queue1 for functionality
+ apt-get install -y libnetfilter-queue1 libnfnetlink0 iptables libgtk-3-0
+ ;;
+ fedora)
+ echo "Using dnf package manager..."
+ dnf install -y libnetfilter_queue libnfnetlink iptables gtk3
+ ;;
+ rhel|centos|rocky|almalinux)
+ echo "Using yum package manager..."
+ yum install -y libnetfilter_queue libnfnetlink iptables gtk3
+ ;;
+ arch|manjaro|endeavouros|garuda)
+ echo "Using pacman package manager..."
+ pacman -Sy --noconfirm libnetfilter_queue libnfnetlink iptables gtk3
+ ;;
+ opensuse*|sles)
+ echo "Using zypper package manager..."
+ zypper install -y libnetfilter_queue1 libnfnetlink0 iptables gtk3
+ ;;
+ void)
+ echo "Using xbps package manager..."
+ xbps-install -Sy libnetfilter_queue libnfnetlink iptables gtk3
+ ;;
+ *)
+ echo "WARNING: Unknown distribution '$DISTRO' (family: '$DISTRO_LIKE')"
+ echo ""
+ echo "Please manually install the following packages:"
+ echo " Debian/Ubuntu: sudo apt install libnetfilter-queue1 libnfnetlink0 iptables libgtk-3-0"
+ echo " Fedora: sudo dnf install libnetfilter_queue libnfnetlink iptables gtk3"
+ echo " Arch: sudo pacman -S libnetfilter_queue libnfnetlink iptables gtk3"
+ echo ""
+ read -p "Continue anyway? (y/n) " -n 1 -r
+ echo
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ exit 1
+ fi
+ ;;
+ esac
+
+ echo "Dependencies installed"
+}
+
+# Use /usr/local/lib (matches RPATH in binary)
+detect_lib_path() {
+ LIB_PATH="/usr/local/lib"
+ echo "Library installation path: $LIB_PATH"
+}
+
+# Check if files exist in current directory
+check_files() {
+ echo ""
+ echo "Checking for required files..."
+
+ if [ ! -f "$SCRIPT_DIR/libproxybridge.so" ]; then
+ echo "ERROR: libproxybridge.so not found in $SCRIPT_DIR"
+ exit 1
+ fi
+
+ if [ ! -f "$SCRIPT_DIR/ProxyBridge" ]; then
+ echo "ERROR: ProxyBridge binary not found in $SCRIPT_DIR"
+ exit 1
+ fi
+
+ if [ ! -f "$SCRIPT_DIR/ProxyBridgeGUI" ]; then
+ echo "WARNING: ProxyBridgeGUI binary not found in $SCRIPT_DIR - GUI will not be installed"
+ fi
+
+ echo "All files present"
+}
+
+# Install files
+install_files() {
+ echo ""
+ echo "Installing ProxyBridge..."
+
+ # Create directories if they don't exist
+ mkdir -p "$LIB_PATH" /usr/local/bin /etc/proxybridge
+ chmod 755 /etc/proxybridge
+
+ # Copy library
+ echo "Installing libproxybridge.so to $LIB_PATH..."
+ cp "$SCRIPT_DIR/libproxybridge.so" "$LIB_PATH/"
+ chmod 755 "$LIB_PATH/libproxybridge.so"
+
+ # Copy binary
+ echo "Installing ProxyBridge to /usr/local/bin..."
+ cp "$SCRIPT_DIR/ProxyBridge" /usr/local/bin/
+ chmod 755 /usr/local/bin/ProxyBridge
+
+ if [ -f "$SCRIPT_DIR/ProxyBridgeGUI" ]; then
+ echo "Installing ProxyBridgeGUI to /usr/local/bin..."
+ cp "$SCRIPT_DIR/ProxyBridgeGUI" /usr/local/bin/
+ chmod 755 /usr/local/bin/ProxyBridgeGUI
+ fi
+
+ echo "Files installed"
+}
+
+# Update library cache
+update_ldconfig() {
+ echo ""
+ echo "Updating library cache..."
+
+ # Add /usr/local/lib to ld.so.conf if not already there
+ if [ -d /etc/ld.so.conf.d ]; then
+ if ! grep -q "^/usr/local/lib" /etc/ld.so.conf.d/* 2>/dev/null; then
+ echo "/usr/local/lib" > /etc/ld.so.conf.d/proxybridge.conf
+ if [ "$LIB_PATH" = "/usr/local/lib64" ]; then
+ echo "/usr/local/lib64" >> /etc/ld.so.conf.d/proxybridge.conf
+ fi
+ fi
+ fi
+
+ # Run ldconfig
+ if command -v ldconfig &> /dev/null; then
+ ldconfig 2>/dev/null || true
+ # Also ensure cache is regenerated
+ ldconfig -v 2>/dev/null | grep -q proxybridge || true
+ echo "Library cache updated"
+ else
+ echo "WARNING: ldconfig not found. You may need to reboot."
+ fi
+}
+
+# Verify installation
+verify_installation() {
+ echo ""
+ echo "Verifying installation..."
+
+ # Check if binary is in PATH
+ if command -v ProxyBridge &> /dev/null; then
+ echo "ProxyBridge binary found in PATH"
+ else
+ echo "ProxyBridge binary not found in PATH"
+ echo " You may need to add /usr/local/bin to your PATH"
+ fi
+
+ # Check if library is loadable
+ if ldd /usr/local/bin/ProxyBridge 2>/dev/null | grep -q "libproxybridge.so"; then
+ if ldd /usr/local/bin/ProxyBridge 2>/dev/null | grep "libproxybridge.so" | grep -q "not found"; then
+ echo "libproxybridge.so not loadable"
+ else
+ echo "libproxybridge.so is loadable"
+ fi
+ fi
+
+ # Final test - try to run --help
+ if /usr/local/bin/ProxyBridge --help &>/dev/null; then
+ echo "ProxyBridge executable is working"
+ else
+ echo "ProxyBridge may have issues - try: sudo ldconfig"
+ fi
+}
+
+# Main deployment
+main() {
+ detect_distro
+ check_files
+ install_dependencies
+ detect_lib_path
+ install_files
+ update_ldconfig
+ verify_installation
+
+ echo ""
+ echo "==================================="
+ echo "Installation Complete!"
+ echo "==================================="
+ echo ""
+ echo "You can now run ProxyBridge from anywhere:"
+ echo " sudo ProxyBridge --help"
+ if [ -f /usr/local/bin/ProxyBridgeGUI ]; then
+ echo " sudo ProxyBridgeGUI (Graphical Interface)"
+ fi
+ echo " sudo ProxyBridge --proxy socks5://IP:PORT --rule \"app:*:*:TCP:PROXY\""
+ echo ""
+ echo "For cleanup after crash:"
+ echo " sudo ProxyBridge --cleanup"
+ echo ""
+}
+
+main
diff --git a/Linux/src/Makefile b/Linux/src/Makefile
new file mode 100644
index 0000000..29effb0
--- /dev/null
+++ b/Linux/src/Makefile
@@ -0,0 +1,35 @@
+CC = gcc
+CFLAGS = -Wall -Wextra -O3 -fPIC -D_GNU_SOURCE \
+ -fstack-protector-strong \
+ -D_FORTIFY_SOURCE=2 \
+ -fPIE \
+ -Wformat -Wformat-security \
+ -Werror=format-security \
+ -fno-strict-overflow \
+ -fno-delete-null-pointer-checks \
+ -fwrapv
+LDFLAGS = -shared -Wl,-z,relro,-z,now -Wl,-z,noexecstack -s
+LIBS = -lpthread -lnetfilter_queue -lnfnetlink
+
+TARGET = libproxybridge.so
+SOURCES = ProxyBridge.c
+OBJECTS = $(SOURCES:.c=.o)
+
+all: $(TARGET)
+
+$(TARGET): $(OBJECTS)
+ $(CC) $(LDFLAGS) -o $@ $^ $(LIBS)
+ @echo "Stripping debug symbols..."
+ strip --strip-unneeded $@
+
+%.o: %.c
+ $(CC) $(CFLAGS) -c $< -o $@
+
+clean:
+ rm -f $(OBJECTS) $(TARGET)
+
+install: $(TARGET)
+ install -m 0755 $(TARGET) /usr/local/lib/
+ ldconfig
+
+.PHONY: all clean install
diff --git a/Linux/src/ProxyBridge.c b/Linux/src/ProxyBridge.c
new file mode 100644
index 0000000..0bed311
--- /dev/null
+++ b/Linux/src/ProxyBridge.c
@@ -0,0 +1,3049 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "ProxyBridge.h"
+
+#define LOCAL_PROXY_PORT 34010
+#define LOCAL_UDP_RELAY_PORT 34011
+#define MAX_PROCESS_NAME 256
+#define PID_CACHE_SIZE 1024
+#define PID_CACHE_TTL_MS 1000
+#define NUM_PACKET_THREADS 4
+#define CONNECTION_HASH_SIZE 256
+#define SOCKS5_BUFFER_SIZE 1024
+#define HTTP_BUFFER_SIZE 1024
+#define LOG_BUFFER_SIZE 1024
+
+// safe way to run commands without shell injection issues
+static int run_command_v(const char *cmd_path, char *const argv[]) {
+ pid_t pid = fork();
+ if (pid == -1) {
+ return -1;
+ } else if (pid == 0) {
+ // child process
+ // send output to /dev/null so it doesnt clutter
+ int fd = open("/dev/null", O_WRONLY);
+ if (fd >= 0) {
+ dup2(fd, STDOUT_FILENO);
+ dup2(fd, STDERR_FILENO);
+ close(fd);
+ }
+ execvp(cmd_path, argv);
+ _exit(127); // command not found or no perms
+ } else {
+ // parent waits for child
+ int status;
+ waitpid(pid, &status, 0);
+ if (WIFEXITED(status)) {
+ return WEXITSTATUS(status);
+ }
+ return -1;
+ }
+}
+
+// run iptables commands easier
+static int run_iptables_cmd(const char *arg1, const char *arg2, const char *arg3, const char *arg4, const char *arg5, const char *arg6, const char *arg7, const char *arg8, const char *arg9, const char *arg10, const char *arg11, const char *arg12, const char *arg13, const char *arg14) {
+ // build argv array skipping null args
+ const char *argv[17];
+ int i = 0;
+ argv[i++] = "iptables";
+ if (arg1) argv[i++] = arg1;
+ if (arg2) argv[i++] = arg2;
+ if (arg3) argv[i++] = arg3;
+ if (arg4) argv[i++] = arg4;
+ if (arg5) argv[i++] = arg5;
+ if (arg6) argv[i++] = arg6;
+ if (arg7) argv[i++] = arg7;
+ if (arg8) argv[i++] = arg8;
+ if (arg9) argv[i++] = arg9;
+ if (arg10) argv[i++] = arg10;
+ if (arg11) argv[i++] = arg11;
+ if (arg12) argv[i++] = arg12;
+ if (arg13) argv[i++] = arg13;
+ if (arg14) argv[i++] = arg14;
+ argv[i] = NULL;
+
+ return run_command_v("iptables", (char **)argv);
+}
+
+// convert string to int safely
+static int safe_atoi(const char *str) {
+ if (!str) return 0;
+ char *endptr;
+ long val = strtol(str, &endptr, 10);
+ if (endptr == str) return 0;
+ return (int)val;
+}
+
+
+typedef struct PROCESS_RULE {
+ uint32_t rule_id;
+ char process_name[MAX_PROCESS_NAME];
+ char *target_hosts;
+ char *target_ports;
+ RuleProtocol protocol;
+ RuleAction action;
+ bool enabled;
+ struct PROCESS_RULE *next;
+} PROCESS_RULE;
+
+#define SOCKS5_VERSION 0x05
+#define SOCKS5_CMD_CONNECT 0x01
+#define SOCKS5_CMD_UDP_ASSOCIATE 0x03
+#define SOCKS5_ATYP_IPV4 0x01
+#define SOCKS5_AUTH_NONE 0x00
+
+typedef struct CONNECTION_INFO {
+ uint16_t src_port;
+ uint32_t src_ip;
+ uint32_t orig_dest_ip;
+ uint16_t orig_dest_port;
+ bool is_tracked;
+ uint64_t last_activity;
+ struct CONNECTION_INFO *next;
+} CONNECTION_INFO;
+
+typedef struct LOGGED_CONNECTION {
+ uint32_t pid;
+ uint32_t dest_ip;
+ uint16_t dest_port;
+ RuleAction action;
+ struct LOGGED_CONNECTION *next;
+} LOGGED_CONNECTION;
+
+typedef struct PID_CACHE_ENTRY {
+ uint32_t src_ip;
+ uint16_t src_port;
+ uint32_t pid;
+ uint64_t timestamp;
+ bool is_udp;
+ struct PID_CACHE_ENTRY *next;
+} PID_CACHE_ENTRY;
+
+static CONNECTION_INFO *connection_hash_table[CONNECTION_HASH_SIZE] = {NULL};
+static LOGGED_CONNECTION *logged_connections = NULL;
+static PROCESS_RULE *rules_list = NULL;
+static uint32_t g_next_rule_id = 1;
+static pthread_rwlock_t conn_lock = PTHREAD_RWLOCK_INITIALIZER; // read-heavy connection hash
+static pthread_rwlock_t rules_lock = PTHREAD_RWLOCK_INITIALIZER; // read-heavy rules list
+static pthread_mutex_t pid_cache_lock = PTHREAD_MUTEX_INITIALIZER; // PID cache only
+static pthread_mutex_t log_lock = PTHREAD_MUTEX_INITIALIZER; // logged connections only
+
+typedef struct {
+ int client_socket;
+ uint32_t orig_dest_ip;
+ uint16_t orig_dest_port;
+} connection_config_t;
+
+typedef struct {
+ int from_socket;
+ int to_socket;
+} transfer_config_t;
+
+static struct nfq_handle *nfq_h = NULL;
+static struct nfq_q_handle *nfq_qh = NULL;
+static pthread_t packet_thread[NUM_PACKET_THREADS] = {0};
+static pthread_t proxy_thread = 0;
+static pthread_t udp_relay_thread = 0;
+static pthread_t cleanup_thread = 0;
+static PID_CACHE_ENTRY *pid_cache[PID_CACHE_SIZE] = {NULL};
+static bool g_has_active_rules = false;
+static bool running = false;
+static uint32_t g_current_process_id = 0;
+
+// udp relay stuff
+static int udp_relay_socket = -1;
+static int socks5_udp_control_socket = -1;
+static int socks5_udp_send_socket = -1;
+static struct sockaddr_in socks5_udp_relay_addr;
+static bool udp_associate_connected = false;
+static uint64_t last_udp_connect_attempt = 0;
+
+static bool g_traffic_logging_enabled = true;
+
+static char g_proxy_host[256] = "";
+static uint16_t g_proxy_port = 0;
+static uint16_t g_local_relay_port = LOCAL_PROXY_PORT;
+static ProxyType g_proxy_type = PROXY_TYPE_SOCKS5;
+static char g_proxy_username[256] = "";
+static char g_proxy_password[256] = "";
+static bool g_dns_via_proxy = true;
+static uint32_t g_proxy_ip_cached = 0; // Cached resolved proxy IP
+static LogCallback g_log_callback = NULL;
+static ConnectionCallback g_connection_callback = NULL;
+
+static void log_message(const char *msg, ...)
+{
+ if (g_log_callback == NULL) return;
+ char buffer[LOG_BUFFER_SIZE];
+ va_list args;
+ va_start(args, msg);
+ vsnprintf(buffer, sizeof(buffer), msg, args);
+ va_end(args);
+ g_log_callback(buffer);
+}
+
+static const char* extract_filename(const char* path)
+{
+ if (!path) return "";
+ const char* last_slash = strrchr(path, '/');
+ return last_slash ? (last_slash + 1) : path;
+}
+
+static inline char* skip_whitespace(char *str)
+{
+ while (*str == ' ' || *str == '\t')
+ str++;
+ return str;
+}
+
+static void format_ip_address(uint32_t ip, char *buffer, size_t size)
+{
+ snprintf(buffer, size, "%d.%d.%d.%d",
+ (ip >> 0) & 0xFF, (ip >> 8) & 0xFF,
+ (ip >> 16) & 0xFF, (ip >> 24) & 0xFF);
+}
+
+typedef bool (*token_match_func)(const char *token, const void *data);
+
+static bool parse_token_list(const char *list, const char *delimiters, token_match_func match_func, const void *match_data)
+{
+ if (list == NULL || list[0] == '\0' || strcmp(list, "*") == 0)
+ return true;
+
+ size_t len = strlen(list) + 1;
+ char *list_copy = malloc(len);
+ if (list_copy == NULL)
+ return false;
+
+ memcpy(list_copy, list, len); // copy including null terminator
+ bool matched = false;
+ char *saveptr = NULL;
+ char *token = strtok_r(list_copy, delimiters, &saveptr);
+ while (token != NULL)
+ {
+ token = skip_whitespace(token);
+
+ // remove spaces at end
+ size_t tlen = strlen(token);
+ while (tlen > 0 && (token[tlen - 1] == ' ' || token[tlen - 1] == '\t' || token[tlen-1] == '\r' || token[tlen-1] == '\n'))
+ {
+ token[tlen - 1] = '\0';
+ tlen--;
+ }
+
+ if (tlen > 0 && match_func(token, match_data))
+ {
+ matched = true;
+ break;
+ }
+ token = strtok_r(NULL, delimiters, &saveptr);
+ }
+ free(list_copy);
+ return matched;
+}
+
+static void configure_tcp_socket(int sock, int bufsize, int timeout_ms)
+{
+ int nodelay = 1;
+ setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
+ setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
+ setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
+ struct timeval timeout = {timeout_ms / 1000, (timeout_ms % 1000) * 1000};
+ setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
+ setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
+}
+
+static void configure_udp_socket(int sock, int bufsize, int timeout_ms)
+{
+ setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
+ setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
+ struct timeval timeout = {timeout_ms / 1000, (timeout_ms % 1000) * 1000};
+ setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
+ setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
+}
+
+static ssize_t send_all(int sock, const char *buf, size_t len)
+{
+ size_t sent = 0;
+ while (sent < len) {
+ ssize_t n = send(sock, buf + sent, len - sent, MSG_NOSIGNAL);
+ if (n < 0) return -1;
+ sent += n;
+ }
+ return sent;
+}
+
+static uint64_t get_monotonic_ms(void)
+{
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
+}
+
+static uint32_t parse_ipv4(const char *ip);
+static uint32_t resolve_hostname(const char *hostname);
+static int socks5_connect(int s, uint32_t dest_ip, uint16_t dest_port);
+static bool match_ip_pattern(const char *pattern, uint32_t ip);
+static bool match_port_pattern(const char *pattern, uint16_t port);
+static bool match_ip_list(const char *ip_list, uint32_t ip);
+static bool match_port_list(const char *port_list, uint16_t port);
+static bool match_process_pattern(const char *pattern, const char *process_name);
+static bool match_process_list(const char *process_list, const char *process_name);
+static int http_connect(int s, uint32_t dest_ip, uint16_t dest_port);
+static void* local_proxy_server(void *arg);
+static void* connection_handler(void *arg);
+static void* transfer_handler(void *arg);
+static void* packet_processor(void *arg);
+static uint32_t get_process_id_from_connection(uint32_t src_ip, uint16_t src_port, bool is_udp);
+static bool get_process_name_from_pid(uint32_t pid, char *name, size_t name_size);
+static RuleAction match_rule(const char *process_name, uint32_t dest_ip, uint16_t dest_port, bool is_udp);
+static RuleAction check_process_rule(uint32_t src_ip, uint16_t src_port, uint32_t dest_ip, uint16_t dest_port, bool is_udp, uint32_t *out_pid);
+static void add_connection(uint16_t src_port, uint32_t src_ip, uint32_t dest_ip, uint16_t dest_port);
+static bool get_connection(uint16_t src_port, uint32_t *dest_ip, uint16_t *dest_port);
+static bool is_connection_tracked(uint16_t src_port);
+static void cleanup_stale_connections(void);
+static bool is_connection_already_logged(uint32_t pid, uint32_t dest_ip, uint16_t dest_port, RuleAction action);
+static void add_logged_connection(uint32_t pid, uint32_t dest_ip, uint16_t dest_port, RuleAction action);
+static void clear_logged_connections(void);
+static bool is_broadcast_or_multicast(uint32_t ip);
+static uint32_t get_cached_pid(uint32_t src_ip, uint16_t src_port, bool is_udp);
+static void cache_pid(uint32_t src_ip, uint16_t src_port, uint32_t pid, bool is_udp);
+static void clear_pid_cache(void);
+static void update_has_active_rules(void);
+
+// find which process owns a socket by checking /proc
+// uses uid hint to skip processes we dont need to check
+static uint32_t find_pid_from_inode(unsigned long target_inode, uint32_t uid_hint)
+{
+ // build the socket string we're looking for
+ char expected[64];
+ int expected_len = snprintf(expected, sizeof(expected), "socket:[%lu]", target_inode);
+
+ DIR *proc_dir = opendir("/proc");
+ if (!proc_dir)
+ return 0;
+
+ uint32_t pid = 0;
+ struct dirent *proc_entry;
+
+ while ((proc_entry = readdir(proc_dir)) != NULL) {
+ // skip stuff that aint a pid folder
+ if (proc_entry->d_type != DT_DIR || !isdigit(proc_entry->d_name[0]))
+ continue;
+
+ // if we know the user id, skip other users processes
+ // makes this way faster cuz less folders to check
+ if (uid_hint != (uint32_t)-1) {
+ struct stat proc_stat;
+ char proc_path[280];
+ snprintf(proc_path, sizeof(proc_path), "/proc/%s", proc_entry->d_name);
+ if (stat(proc_path, &proc_stat) == 0 && proc_stat.st_uid != uid_hint)
+ continue;
+ }
+
+ char fd_path[280];
+ snprintf(fd_path, sizeof(fd_path), "/proc/%s/fd", proc_entry->d_name);
+ DIR *fd_dir = opendir(fd_path);
+ if (!fd_dir)
+ continue;
+
+ struct dirent *fd_entry;
+ while ((fd_entry = readdir(fd_dir)) != NULL) {
+ if (fd_entry->d_name[0] == '.')
+ continue;
+
+ char link_path[560];
+ snprintf(link_path, sizeof(link_path), "/proc/%s/fd/%s",
+ proc_entry->d_name, fd_entry->d_name);
+
+ char link_target[64];
+ ssize_t link_len = readlink(link_path, link_target, sizeof(link_target) - 1);
+ if (link_len == expected_len) {
+ link_target[link_len] = '\0';
+ if (memcmp(link_target, expected, expected_len) == 0) {
+ pid = (uint32_t)safe_atoi(proc_entry->d_name);
+ closedir(fd_dir);
+ closedir(proc_dir);
+ return pid;
+ }
+ }
+ }
+ closedir(fd_dir);
+ }
+ closedir(proc_dir);
+ return pid;
+}
+
+// fast pid lookup using netlink
+// tcp uses exact query, udp needs dump
+static uint32_t get_process_id_from_connection(uint32_t src_ip, uint16_t src_port, bool is_udp)
+{
+ uint32_t cached_pid = get_cached_pid(src_ip, src_port, is_udp);
+ if (cached_pid != 0)
+ return cached_pid;
+
+ int fd = socket(AF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_SOCK_DIAG);
+ if (fd < 0)
+ return 0;
+
+ // short timeout so we dont block packet flow
+ struct timeval tv = {0, 100000}; // 100ms
+ setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+
+ struct {
+ struct nlmsghdr nlh;
+ struct inet_diag_req_v2 r;
+ } req;
+
+ memset(&req, 0, sizeof(req));
+ req.nlh.nlmsg_len = sizeof(req);
+ req.nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY;
+ req.r.sdiag_family = AF_INET;
+ req.r.sdiag_protocol = is_udp ? IPPROTO_UDP : IPPROTO_TCP;
+
+ // udp needs dump cuz no connection state to match on
+ // tcp can filter to only syn_sent and established states
+ req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
+ if (!is_udp)
+ req.r.idiag_states = (1 << 2) | (1 << 3); // SYN_SENT(2) + ESTABLISHED(3) only
+ else
+ req.r.idiag_states = (uint32_t)-1; // All states for UDP
+ req.r.idiag_ext = 0;
+
+ struct sockaddr_nl sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.nl_family = AF_NETLINK;
+
+ if (sendto(fd, &req, sizeof(req), 0, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
+ close(fd);
+ return 0;
+ }
+
+ uint32_t pid = 0;
+ unsigned long target_inode = 0;
+ uint32_t target_uid = (uint32_t)-1;
+ bool found = false;
+ char buf[16384];
+ struct iovec iov = {buf, sizeof(buf)};
+ struct msghdr msg = {&sa, sizeof(sa), &iov, 1, NULL, 0, 0};
+
+ while (1) {
+ ssize_t len = recvmsg(fd, &msg, 0);
+ if (len <= 0)
+ break;
+
+ struct nlmsghdr *h = (struct nlmsghdr *)buf;
+ while (NLMSG_OK(h, (size_t)len)) {
+ if (h->nlmsg_type == NLMSG_DONE || h->nlmsg_type == NLMSG_ERROR)
+ goto nl_done;
+
+ if (h->nlmsg_type == SOCK_DIAG_BY_FAMILY) {
+ struct inet_diag_msg *r = NLMSG_DATA(h);
+
+ // check if this is our socket
+ if (r->id.idiag_src[0] == src_ip && ntohs(r->id.idiag_sport) == src_port) {
+ target_inode = r->idiag_inode;
+ target_uid = r->idiag_uid; // UID to narrow /proc scan
+ found = true;
+ goto nl_done;
+ }
+ }
+ h = NLMSG_NEXT(h, len);
+ }
+ }
+
+nl_done:
+ close(fd);
+
+ // if netlink found it, now find pid from inode
+ // no need to scan /proc/net/tcp since we got inode already
+ if (found && target_inode != 0) {
+ pid = find_pid_from_inode(target_inode, target_uid);
+ }
+
+ // fallback for udp if netlink didnt find it
+ // happens when app uses sendto without connecting socket first
+ if (!found && is_udp) {
+ const char* udp_files[] = {"/proc/net/udp", "/proc/net/udp6"};
+ for (int file_idx = 0; file_idx < 2 && pid == 0; file_idx++) {
+ FILE *fp = fopen(udp_files[file_idx], "r");
+ if (!fp)
+ continue;
+
+ char line[512];
+ if (!fgets(line, sizeof(line), fp)) { // skip header
+ fclose(fp);
+ continue;
+ }
+ while (fgets(line, sizeof(line), fp)) {
+ unsigned int local_addr, local_port;
+ unsigned long inode;
+ int uid_val;
+
+ if (sscanf(line, "%*d: %X:%X %*X:%*X %*X %*X:%*X %*X:%*X %*X %d %*d %lu",
+ &local_addr, &local_port, &uid_val, &inode) == 4) {
+ if (local_port == src_port && inode != 0) {
+ pid = find_pid_from_inode(inode, (uint32_t)uid_val);
+ break;
+ }
+ }
+ }
+ fclose(fp);
+ }
+ }
+
+ if (pid != 0)
+ cache_pid(src_ip, src_port, pid, is_udp);
+ return pid;
+}
+
+static bool get_process_name_from_pid(uint32_t pid, char *name, size_t name_size)
+{
+ if (pid == 0)
+ return false;
+
+ // pid 1 is always init/systemd
+ // kinda like windows pid 4 for system stuff
+ if (pid == 1)
+ {
+ snprintf(name, name_size, "systemd");
+ return true;
+ }
+
+ char path[64];
+ snprintf(path, sizeof(path), "/proc/%u/exe", pid);
+
+ ssize_t len = readlink(path, name, name_size - 1);
+ if (len < 0)
+ return false;
+
+ name[len] = '\0';
+ return true;
+}
+
+static bool match_ip_pattern(const char *pattern, uint32_t ip)
+{
+ if (pattern == NULL || strcmp(pattern, "*") == 0)
+ return true;
+
+ unsigned char ip_octets[4];
+ ip_octets[0] = (ip >> 0) & 0xFF;
+ ip_octets[1] = (ip >> 8) & 0xFF;
+ ip_octets[2] = (ip >> 16) & 0xFF;
+ ip_octets[3] = (ip >> 24) & 0xFF;
+
+ char pattern_copy[256];
+ strncpy(pattern_copy, pattern, sizeof(pattern_copy) - 1);
+ pattern_copy[sizeof(pattern_copy) - 1] = '\0';
+
+ char pattern_octets[4][16];
+ int octet_count = 0;
+ int char_idx = 0;
+
+ for (size_t i = 0; i <= strlen(pattern_copy) && octet_count < 4; i++)
+ {
+ if (pattern_copy[i] == '.' || pattern_copy[i] == '\0')
+ {
+ pattern_octets[octet_count][char_idx] = '\0';
+ octet_count++;
+ char_idx = 0;
+ if (pattern_copy[i] == '\0')
+ break;
+ }
+ else
+ {
+ if (char_idx < 15)
+ pattern_octets[octet_count][char_idx++] = pattern_copy[i];
+ }
+ }
+
+ if (octet_count != 4)
+ return false;
+
+ for (int i = 0; i < 4; i++)
+ {
+ if (strcmp(pattern_octets[i], "*") == 0)
+ continue;
+
+ char *dash = strchr(pattern_octets[i], '-');
+ if (dash != NULL)
+ {
+ int start = safe_atoi(pattern_octets[i]);
+ int end = safe_atoi(dash + 1);
+ if (ip_octets[i] < start || ip_octets[i] > end)
+ return false;
+ }
+ else
+ {
+ int pattern_val = safe_atoi(pattern_octets[i]);
+ if (pattern_val != ip_octets[i])
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool match_port_pattern(const char *pattern, uint16_t port)
+{
+ if (pattern == NULL || strcmp(pattern, "*") == 0)
+ return true;
+
+ char *dash = strchr(pattern, '-');
+ if (dash != NULL)
+ {
+ int start_port = safe_atoi(pattern);
+ int end_port = safe_atoi(dash + 1);
+ return (port >= start_port && port <= end_port);
+ }
+
+ return (port == safe_atoi(pattern));
+}
+
+static bool ip_match_wrapper(const char *token, const void *data)
+{
+ return match_ip_pattern(token, *(const uint32_t*)data);
+}
+
+static bool match_ip_list(const char *ip_list, uint32_t ip)
+{
+ return parse_token_list(ip_list, ";", ip_match_wrapper, &ip);
+}
+
+static bool port_match_wrapper(const char *token, const void *data)
+{
+ return match_port_pattern(token, *(const uint16_t*)data);
+}
+
+static bool match_port_list(const char *port_list, uint16_t port)
+{
+ return parse_token_list(port_list, ",;", port_match_wrapper, &port);
+}
+
+static bool match_process_pattern(const char *pattern, const char *process_full_path)
+{
+ if (pattern == NULL || strcmp(pattern, "*") == 0)
+ return true;
+
+ const char *filename = strrchr(process_full_path, '/');
+ if (filename != NULL)
+ filename++;
+ else
+ filename = process_full_path;
+
+ size_t pattern_len = strlen(pattern);
+ size_t name_len = strlen(filename);
+ size_t full_path_len = strlen(process_full_path);
+
+ bool is_full_path_pattern = (strchr(pattern, '/') != NULL);
+ const char *match_target = is_full_path_pattern ? process_full_path : filename;
+ size_t target_len = is_full_path_pattern ? full_path_len : name_len;
+
+ if (pattern_len > 0 && pattern[pattern_len - 1] == '*')
+ {
+ return strncasecmp(pattern, match_target, pattern_len - 1) == 0;
+ }
+
+ if (pattern_len > 1 && pattern[0] == '*')
+ {
+ const char *pattern_suffix = pattern + 1;
+ size_t suffix_len = pattern_len - 1;
+ if (target_len >= suffix_len)
+ {
+ return strcasecmp(match_target + target_len - suffix_len, pattern_suffix) == 0;
+ }
+ return false;
+ }
+
+ return strcasecmp(pattern, match_target) == 0;
+}
+
+typedef struct {
+ const char *process_name;
+} process_match_data;
+
+static bool process_match_wrapper(const char *token, const void *data)
+{
+ const process_match_data *pdata = data;
+ return match_process_pattern(token, pdata->process_name);
+}
+
+static bool match_process_list(const char *process_list, const char *process_name)
+{
+ process_match_data data = {process_name};
+ return parse_token_list(process_list, ";", process_match_wrapper, &data);
+}
+
+static uint32_t parse_ipv4(const char *ip)
+{
+ unsigned int a, b, c, d;
+ if (sscanf(ip, "%u.%u.%u.%u", &a, &b, &c, &d) != 4)
+ return 0;
+ if (a > 255 || b > 255 || c > 255 || d > 255)
+ return 0;
+ return (a << 0) | (b << 8) | (c << 16) | (d << 24);
+}
+
+static uint32_t resolve_hostname(const char *hostname)
+{
+ if (hostname == NULL || hostname[0] == '\0')
+ return 0;
+
+ uint32_t ip = parse_ipv4(hostname);
+ if (ip != 0)
+ return ip;
+
+ struct addrinfo hints, *result = NULL;
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_INET;
+ hints.ai_socktype = SOCK_STREAM;
+
+ if (getaddrinfo(hostname, NULL, &hints, &result) != 0)
+ {
+ log_message("failed to resolve hostname: %s", hostname);
+ return 0;
+ }
+
+ if (result == NULL || result->ai_family != AF_INET)
+ {
+ if (result != NULL)
+ freeaddrinfo(result);
+ log_message("no ipv4 address found for hostname: %s", hostname);
+ return 0;
+ }
+
+ struct sockaddr_in *addr = (struct sockaddr_in *)result->ai_addr;
+ uint32_t resolved_ip = addr->sin_addr.s_addr;
+ freeaddrinfo(result);
+
+ return resolved_ip;
+}
+
+static bool is_broadcast_or_multicast(uint32_t ip)
+{
+ // localhost
+ uint8_t first_octet = (ip >> 0) & 0xFF;
+ if (first_octet == 127)
+ return true;
+
+ // link-local apipa stuff
+ uint8_t second_octet = (ip >> 8) & 0xFF;
+ if (first_octet == 169 && second_octet == 254)
+ return true;
+
+ // broadcast
+ if (ip == 0xFFFFFFFF)
+ return true;
+
+ // subnet broadcast
+ if ((ip & 0xFF000000) == 0xFF000000)
+ return true;
+
+ // multicast range
+ if (first_octet >= 224 && first_octet <= 239)
+ return true;
+
+ return false;
+}
+
+static RuleAction match_rule(const char *process_name, uint32_t dest_ip, uint16_t dest_port, bool is_udp)
+{
+ PROCESS_RULE *rule = rules_list;
+ PROCESS_RULE *wildcard_rule = NULL;
+
+ while (rule != NULL)
+ {
+ if (!rule->enabled)
+ {
+ rule = rule->next;
+ continue;
+ }
+
+ if (rule->protocol != RULE_PROTOCOL_BOTH)
+ {
+ if (rule->protocol == RULE_PROTOCOL_TCP && is_udp)
+ {
+ rule = rule->next;
+ continue;
+ }
+ if (rule->protocol == RULE_PROTOCOL_UDP && !is_udp)
+ {
+ rule = rule->next;
+ continue;
+ }
+ }
+
+ bool is_wildcard_process = (strcmp(rule->process_name, "*") == 0 || strcasecmp(rule->process_name, "ANY") == 0);
+
+ if (is_wildcard_process)
+ {
+ bool has_ip_filter = (strcmp(rule->target_hosts, "*") != 0);
+ bool has_port_filter = (strcmp(rule->target_ports, "*") != 0);
+
+ if (has_ip_filter || has_port_filter)
+ {
+ if (match_ip_list(rule->target_hosts, dest_ip) &&
+ match_port_list(rule->target_ports, dest_port))
+ {
+ return rule->action;
+ }
+ rule = rule->next;
+ continue;
+ }
+
+ if (wildcard_rule == NULL)
+ {
+ wildcard_rule = rule;
+ }
+ rule = rule->next;
+ continue;
+ }
+
+ if (match_process_list(rule->process_name, process_name))
+ {
+ if (match_ip_list(rule->target_hosts, dest_ip) &&
+ match_port_list(rule->target_ports, dest_port))
+ {
+ return rule->action;
+ }
+ }
+
+ rule = rule->next;
+ }
+
+ if (wildcard_rule != NULL)
+ {
+ return wildcard_rule->action;
+ }
+
+ return RULE_ACTION_DIRECT;
+}
+
+static RuleAction check_process_rule(uint32_t src_ip, uint16_t src_port, uint32_t dest_ip, uint16_t dest_port, bool is_udp, uint32_t *out_pid)
+{
+ uint32_t pid;
+ char process_name[MAX_PROCESS_NAME];
+
+ pid = get_process_id_from_connection(src_ip, src_port, is_udp);
+
+ if (out_pid != NULL)
+ *out_pid = pid;
+
+ if (pid == 0)
+ return RULE_ACTION_DIRECT;
+
+ if (pid == g_current_process_id)
+ return RULE_ACTION_DIRECT;
+
+ if (!get_process_name_from_pid(pid, process_name, sizeof(process_name)))
+ return RULE_ACTION_DIRECT;
+
+ pthread_rwlock_rdlock(&rules_lock);
+ RuleAction action = match_rule(process_name, dest_ip, dest_port, is_udp);
+ pthread_rwlock_unlock(&rules_lock);
+
+ if (action == RULE_ACTION_PROXY && is_udp && g_proxy_type == PROXY_TYPE_HTTP)
+ {
+ return RULE_ACTION_DIRECT;
+ }
+ if (action == RULE_ACTION_PROXY && (g_proxy_host[0] == '\0' || g_proxy_port == 0))
+ {
+ return RULE_ACTION_DIRECT;
+ }
+
+ return action;
+}
+
+static int socks5_connect(int s, uint32_t dest_ip, uint16_t dest_port)
+{
+ unsigned char buf[SOCKS5_BUFFER_SIZE];
+ ssize_t len;
+ bool use_auth = (g_proxy_username[0] != '\0');
+
+ buf[0] = SOCKS5_VERSION;
+ if (use_auth)
+ {
+ buf[1] = 0x02;
+ buf[2] = SOCKS5_AUTH_NONE;
+ buf[3] = 0x02;
+ if (send(s, buf, 4, MSG_NOSIGNAL) != 4)
+ {
+ log_message("socks5 failed to send auth methods");
+ return -1;
+ }
+ }
+ else
+ {
+ buf[1] = 0x01;
+ buf[2] = SOCKS5_AUTH_NONE;
+ if (send(s, buf, 3, MSG_NOSIGNAL) != 3)
+ {
+ log_message("socks5 failed to send auth methods");
+ return -1;
+ }
+ }
+
+ len = recv(s, buf, 2, 0);
+ if (len != 2 || buf[0] != SOCKS5_VERSION)
+ {
+ log_message("socks5 invalid auth response");
+ return -1;
+ }
+
+ if (buf[1] == 0x02 && use_auth)
+ {
+ size_t ulen = strlen(g_proxy_username);
+ size_t plen = strlen(g_proxy_password);
+ buf[0] = 0x01;
+ buf[1] = (unsigned char)ulen;
+ memcpy(buf + 2, g_proxy_username, ulen);
+ buf[2 + ulen] = (unsigned char)plen;
+ memcpy(buf + 3 + ulen, g_proxy_password, plen);
+
+ if (send(s, buf, 3 + ulen + plen, MSG_NOSIGNAL) != (ssize_t)(3 + ulen + plen))
+ {
+ log_message("socks5 failed to send credentials");
+ return -1;
+ }
+
+ len = recv(s, buf, 2, 0);
+ if (len != 2 || buf[0] != 0x01 || buf[1] != 0x00)
+ {
+ log_message("socks5 authentication failed");
+ return -1;
+ }
+ }
+ else if (buf[1] != SOCKS5_AUTH_NONE)
+ {
+ log_message("socks5 unsupported auth method");
+ return -1;
+ }
+
+ buf[0] = SOCKS5_VERSION;
+ buf[1] = SOCKS5_CMD_CONNECT;
+ buf[2] = 0x00;
+ buf[3] = SOCKS5_ATYP_IPV4;
+ memcpy(buf + 4, &dest_ip, 4);
+ uint16_t port_net = htons(dest_port);
+ memcpy(buf + 8, &port_net, 2);
+
+ if (send(s, buf, 10, MSG_NOSIGNAL) != 10)
+ {
+ log_message("socks5 failed to send connect request");
+ return -1;
+ }
+
+ len = recv(s, buf, 10, 0);
+ if (len < 10 || buf[0] != SOCKS5_VERSION || buf[1] != 0x00)
+ {
+ log_message("socks5 connect failed status %d", len > 1 ? buf[1] : -1);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int http_connect(int s, uint32_t dest_ip, uint16_t dest_port)
+{
+ char buf[HTTP_BUFFER_SIZE];
+ char dest_ip_str[32];
+ format_ip_address(dest_ip, dest_ip_str, sizeof(dest_ip_str));
+
+ int len = snprintf(buf, sizeof(buf),
+ "CONNECT %s:%d HTTP/1.1\r\n"
+ "Host: %s:%d\r\n"
+ "Proxy-Connection: Keep-Alive\r\n",
+ dest_ip_str, dest_port, dest_ip_str, dest_port);
+
+ if (g_proxy_username[0] != '\0')
+ {
+ // encode user:pass in base64
+ static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ char auth_raw[512];
+ int auth_raw_len = snprintf(auth_raw, sizeof(auth_raw), "%s:%s", g_proxy_username, g_proxy_password);
+ char auth_b64[700];
+ int j = 0;
+ for (int i = 0; i < auth_raw_len; i += 3) {
+ unsigned int n = ((unsigned char)auth_raw[i]) << 16;
+ if (i + 1 < auth_raw_len) n |= ((unsigned char)auth_raw[i + 1]) << 8;
+ if (i + 2 < auth_raw_len) n |= ((unsigned char)auth_raw[i + 2]);
+ auth_b64[j++] = b64[(n >> 18) & 0x3F];
+ auth_b64[j++] = b64[(n >> 12) & 0x3F];
+ auth_b64[j++] = (i + 1 < auth_raw_len) ? b64[(n >> 6) & 0x3F] : '=';
+ auth_b64[j++] = (i + 2 < auth_raw_len) ? b64[n & 0x3F] : '=';
+ }
+ auth_b64[j] = '\0';
+ len += snprintf(buf + len, sizeof(buf) - len,
+ "Proxy-Authorization: Basic %s\r\n", auth_b64);
+ }
+
+ len += snprintf(buf + len, sizeof(buf) - len, "\r\n");
+
+ if (send_all(s, buf, len) < 0)
+ {
+ log_message("http failed to send connect");
+ return -1;
+ }
+
+ ssize_t recv_len = recv(s, buf, sizeof(buf) - 1, 0);
+ if (recv_len < 12)
+ {
+ log_message("http invalid response");
+ return -1;
+ }
+
+ buf[recv_len] = '\0';
+ if (strncmp(buf, "HTTP/1.", 7) != 0)
+ {
+ log_message("http invalid response");
+ return -1;
+ }
+
+ int status_code = 0;
+ if (sscanf(buf + 9, "%d", &status_code) != 1 || status_code != 200)
+ {
+ log_message("http connect failed status %d", status_code);
+ return -1;
+ }
+
+ return 0;
+}
+
+// relay functions for production use
+
+// connection handler like windows - blocks on connect then transfers data
+static void* connection_handler(void *arg)
+{
+ connection_config_t *config = (connection_config_t *)arg;
+ int client_sock = config->client_socket;
+ uint32_t dest_ip = config->orig_dest_ip;
+ uint16_t dest_port = config->orig_dest_port;
+ int proxy_sock;
+ struct sockaddr_in proxy_addr;
+ uint32_t proxy_ip;
+
+ free(config);
+
+ // use cached proxy ip we resolved earlier
+ proxy_ip = g_proxy_ip_cached;
+ if (proxy_ip == 0)
+ {
+ // try resolving again if cache failed
+ proxy_ip = resolve_hostname(g_proxy_host);
+ if (proxy_ip == 0)
+ {
+ close(client_sock);
+ return NULL;
+ }
+ g_proxy_ip_cached = proxy_ip;
+ }
+
+ proxy_sock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (proxy_sock < 0)
+ {
+ close(client_sock);
+ return NULL;
+ }
+
+ configure_tcp_socket(proxy_sock, 1048576, 60000);
+ configure_tcp_socket(client_sock, 1048576, 60000);
+
+ memset(&proxy_addr, 0, sizeof(proxy_addr));
+ proxy_addr.sin_family = AF_INET;
+ proxy_addr.sin_addr.s_addr = proxy_ip;
+ proxy_addr.sin_port = htons(g_proxy_port);
+
+ if (connect(proxy_sock, (struct sockaddr *)&proxy_addr, sizeof(proxy_addr)) < 0)
+ {
+ close(client_sock);
+ close(proxy_sock);
+ return NULL;
+ }
+
+ // do handshake blocking
+ if (g_proxy_type == PROXY_TYPE_SOCKS5)
+ {
+ if (socks5_connect(proxy_sock, dest_ip, dest_port) != 0)
+ {
+ close(client_sock);
+ close(proxy_sock);
+ return NULL;
+ }
+ }
+ else if (g_proxy_type == PROXY_TYPE_HTTP)
+ {
+ if (http_connect(proxy_sock, dest_ip, dest_port) != 0)
+ {
+ close(client_sock);
+ close(proxy_sock);
+ return NULL;
+ }
+ }
+
+ // setup transfer config
+ transfer_config_t *transfer_config = (transfer_config_t *)malloc(sizeof(transfer_config_t));
+ if (transfer_config == NULL)
+ {
+ close(client_sock);
+ close(proxy_sock);
+ return NULL;
+ }
+
+ transfer_config->from_socket = client_sock;
+ transfer_config->to_socket = proxy_sock;
+
+ // transfer data both ways in this thread
+ transfer_handler((void*)transfer_config);
+
+ return NULL;
+}
+
+// relay data both ways using splice for zero-copy
+// data goes kernel to kernel thru pipe, never hits userspace
+// way faster than copying thru userspace buffers
+static void* transfer_handler(void *arg)
+{
+ transfer_config_t *config = (transfer_config_t *)arg;
+ int sock1 = config->from_socket; // client socket
+ int sock2 = config->to_socket; // proxy socket
+ free(config);
+
+ // make 2 pipes for splice
+ // pipe_a: proxy to client (download)
+ // pipe_b: client to proxy (upload)
+ int pipe_a[2] = {-1, -1};
+ int pipe_b[2] = {-1, -1};
+
+ if (pipe2(pipe_a, O_CLOEXEC | O_NONBLOCK) < 0 ||
+ pipe2(pipe_b, O_CLOEXEC | O_NONBLOCK) < 0) {
+ // pipe failed, cleanup and fallback
+ if (pipe_a[0] >= 0) { close(pipe_a[0]); close(pipe_a[1]); }
+ if (pipe_b[0] >= 0) { close(pipe_b[0]); close(pipe_b[1]); }
+ goto fallback;
+ }
+
+ // make pipes bigger for better speed (64kb to 1mb)
+ fcntl(pipe_a[0], F_SETPIPE_SZ, 1048576);
+ fcntl(pipe_b[0], F_SETPIPE_SZ, 1048576);
+
+ // set sockets to nonblocking for splice
+ fcntl(sock1, F_SETFL, fcntl(sock1, F_GETFL, 0) | O_NONBLOCK);
+ fcntl(sock2, F_SETFL, fcntl(sock2, F_GETFL, 0) | O_NONBLOCK);
+
+ {
+ struct pollfd fds[2];
+ ssize_t pipe_a_bytes = 0; // bytes in download pipe (proxy→client)
+ ssize_t pipe_b_bytes = 0; // bytes in upload pipe (client→proxy)
+ bool sock1_done = false; // client EOF or error
+ bool sock2_done = false; // proxy EOF or error
+ bool shut_wr_sock1 = false; // already called shutdown(sock1, SHUT_WR)
+ bool shut_wr_sock2 = false; // already called shutdown(sock2, SHUT_WR)
+
+ while (1)
+ {
+ // build poll set, use fd=-1 to skip closed sockets
+ // important: poll reports POLLHUP even with events=0
+ // so use fd=-1 to actually skip it or we get busy loop
+ fds[0].fd = (!sock1_done || pipe_a_bytes > 0 || pipe_b_bytes > 0) ? sock1 : -1;
+ fds[1].fd = (!sock2_done || pipe_b_bytes > 0 || pipe_a_bytes > 0) ? sock2 : -1;
+ fds[0].events = 0;
+ fds[1].events = 0;
+ fds[0].revents = 0;
+ fds[1].revents = 0;
+
+ // download: proxy to client
+ if (!sock2_done && pipe_a_bytes == 0)
+ fds[1].events |= POLLIN; // read from proxy
+ if (pipe_a_bytes > 0)
+ fds[0].events |= POLLOUT; // write to client
+
+ // upload: client to proxy
+ if (!sock1_done && pipe_b_bytes == 0)
+ fds[0].events |= POLLIN; // read from client
+ if (pipe_b_bytes > 0)
+ fds[1].events |= POLLOUT; // write to proxy
+
+ // all done
+ if (fds[0].fd == -1 && fds[1].fd == -1)
+ break;
+ if (fds[0].events == 0 && fds[1].events == 0)
+ break;
+
+ int ready = poll(fds, 2, 60000);
+ if (ready < 0) {
+ if (errno == EINTR) continue;
+ break;
+ }
+ if (ready == 0)
+ break; // 60s idle timeout
+
+ // check for errors and hangups
+ if (fds[0].revents & POLLERR) break;
+ if (fds[1].revents & POLLERR) break;
+
+ // pollhup means other side closed
+ if ((fds[1].revents & POLLHUP) && !(fds[1].revents & POLLIN))
+ sock2_done = true;
+ if ((fds[0].revents & POLLHUP) && !(fds[0].revents & POLLIN))
+ sock1_done = true;
+
+ // download path
+
+ if (!sock2_done && pipe_a_bytes == 0 && (fds[1].revents & POLLIN)) {
+ ssize_t n = splice(sock2, NULL, pipe_a[1], NULL, 1048576,
+ SPLICE_F_MOVE | SPLICE_F_NONBLOCK);
+ if (n > 0) {
+ pipe_a_bytes = n;
+ } else if (n == 0) {
+ sock2_done = true;
+ } else if (errno != EAGAIN && errno != EWOULDBLOCK) {
+ sock2_done = true;
+ }
+ }
+
+ if (pipe_a_bytes > 0 && (fds[0].revents & POLLOUT)) {
+ ssize_t n = splice(pipe_a[0], NULL, sock1, NULL, pipe_a_bytes,
+ SPLICE_F_MOVE | SPLICE_F_NONBLOCK);
+ if (n > 0) {
+ pipe_a_bytes -= n;
+ } else if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
+ break;
+ }
+ }
+
+ // upload path
+
+ if (!sock1_done && pipe_b_bytes == 0 && (fds[0].revents & POLLIN)) {
+ ssize_t n = splice(sock1, NULL, pipe_b[1], NULL, 1048576,
+ SPLICE_F_MOVE | SPLICE_F_NONBLOCK);
+ if (n > 0) {
+ pipe_b_bytes = n;
+ } else if (n == 0) {
+ sock1_done = true;
+ } else if (errno != EAGAIN && errno != EWOULDBLOCK) {
+ sock1_done = true;
+ }
+ }
+
+ if (pipe_b_bytes > 0 && (fds[1].revents & POLLOUT)) {
+ ssize_t n = splice(pipe_b[0], NULL, sock2, NULL, pipe_b_bytes,
+ SPLICE_F_MOVE | SPLICE_F_NONBLOCK);
+ if (n > 0) {
+ pipe_b_bytes -= n;
+ } else if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
+ break;
+ }
+ }
+
+ // half-close when one side done and pipe empty
+ // tell other side with shutdown
+ if (sock2_done && pipe_a_bytes == 0 && !shut_wr_sock1) {
+ shutdown(sock1, SHUT_WR);
+ shut_wr_sock1 = true;
+ }
+ if (sock1_done && pipe_b_bytes == 0 && !shut_wr_sock2) {
+ shutdown(sock2, SHUT_WR);
+ shut_wr_sock2 = true;
+ }
+
+ // both sides finished and pipes empty
+ if (sock1_done && sock2_done && pipe_a_bytes == 0 && pipe_b_bytes == 0)
+ break;
+ }
+ }
+
+ close(pipe_a[0]); close(pipe_a[1]);
+ close(pipe_b[0]); close(pipe_b[1]);
+ goto cleanup;
+
+fallback:
+ // fallback to normal recv/send if pipes didnt work
+ {
+ char buf[131072];
+ struct pollfd fds[2];
+ fds[0].fd = sock1;
+ fds[0].events = POLLIN;
+ fds[1].fd = sock2;
+ fds[1].events = POLLIN;
+
+ while (1) {
+ int ready = poll(fds, 2, 60000);
+ if (ready <= 0) break;
+
+ if (fds[0].revents & POLLERR || fds[1].revents & POLLERR) break;
+
+ bool did_work = false;
+ if (fds[0].revents & POLLIN) {
+ ssize_t n = recv(sock1, buf, sizeof(buf), MSG_NOSIGNAL);
+ if (n <= 0) break;
+ if (send_all(sock2, buf, n) < 0) break;
+ did_work = true;
+ }
+ if (fds[1].revents & POLLIN) {
+ ssize_t n = recv(sock2, buf, sizeof(buf), MSG_NOSIGNAL);
+ if (n <= 0) break;
+ if (send_all(sock1, buf, n) < 0) break;
+ did_work = true;
+ }
+
+ // pollhup with no data means peer closed
+ if (!did_work) break;
+ }
+ }
+
+cleanup:
+ shutdown(sock1, SHUT_RDWR);
+ shutdown(sock2, SHUT_RDWR);
+ close(sock1);
+ close(sock2);
+ return NULL;
+}
+
+// proxy server accepts connections and spawns threads
+static void* local_proxy_server(void *arg)
+{
+ (void)arg;
+ struct sockaddr_in addr;
+ int listen_sock;
+ int on = 1;
+
+ listen_sock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (listen_sock < 0)
+ {
+ log_message("Socket creation failed");
+ return NULL;
+ }
+
+ setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
+ setsockopt(listen_sock, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on));
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_addr.s_addr = INADDR_ANY;
+ addr.sin_port = htons(g_local_relay_port);
+
+ if (bind(listen_sock, (struct sockaddr *)&addr, sizeof(addr)) < 0)
+ {
+ log_message("Bind failed");
+ close(listen_sock);
+ return NULL;
+ }
+
+ if (listen(listen_sock, SOMAXCONN) < 0)
+ {
+ log_message("Listen failed");
+ close(listen_sock);
+ return NULL;
+ }
+
+ // create thread attrs with small stack (256kb not 8mb)
+ // relay threads dont need big buffers anymore cuz splice
+ pthread_attr_t thread_attr;
+ pthread_attr_init(&thread_attr);
+ pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);
+ pthread_attr_setstacksize(&thread_attr, 262144); // 256KB stack
+
+ struct pollfd pfd;
+ pfd.fd = listen_sock;
+ pfd.events = POLLIN;
+
+ while (running)
+ {
+ int ready = poll(&pfd, 1, 1000); // 1s timeout
+ if (ready <= 0)
+ continue;
+
+ struct sockaddr_in client_addr;
+ socklen_t addr_len = sizeof(client_addr);
+ int client_sock = accept4(listen_sock, (struct sockaddr *)&client_addr, &addr_len, SOCK_CLOEXEC);
+
+ if (client_sock < 0)
+ continue;
+
+ connection_config_t *conn_config = (connection_config_t *)malloc(sizeof(connection_config_t));
+ if (conn_config == NULL)
+ {
+ close(client_sock);
+ continue;
+ }
+
+ conn_config->client_socket = client_sock;
+
+ uint16_t client_port = ntohs(client_addr.sin_port);
+ if (!get_connection(client_port, &conn_config->orig_dest_ip, &conn_config->orig_dest_port))
+ {
+ close(client_sock);
+ free(conn_config);
+ continue;
+ }
+
+ pthread_t conn_thread;
+ if (pthread_create(&conn_thread, &thread_attr, connection_handler, (void*)conn_config) != 0)
+ {
+ close(client_sock);
+ free(conn_config);
+ continue;
+ }
+ }
+
+ pthread_attr_destroy(&thread_attr);
+ close(listen_sock);
+ return NULL;
+}
+
+// socks5 udp associate
+static int socks5_udp_associate(int s, struct sockaddr_in *relay_addr)
+{
+ unsigned char buf[512];
+ ssize_t len;
+
+ // auth handshake
+ bool use_auth = (g_proxy_username[0] != '\0');
+ buf[0] = SOCKS5_VERSION;
+ buf[1] = use_auth ? 0x02 : 0x01;
+ buf[2] = SOCKS5_AUTH_NONE;
+ if (use_auth)
+ buf[3] = 0x02; // username/password auth
+
+ if (send(s, buf, use_auth ? 4 : 3, MSG_NOSIGNAL) != (use_auth ? 4 : 3))
+ return -1;
+
+ len = recv(s, buf, 2, 0);
+ if (len != 2 || buf[0] != SOCKS5_VERSION)
+ return -1;
+
+ if (buf[1] == 0x02 && use_auth)
+ {
+ size_t ulen = strlen(g_proxy_username);
+ size_t plen = strlen(g_proxy_password);
+ buf[0] = 0x01;
+ buf[1] = (unsigned char)ulen;
+ memcpy(buf + 2, g_proxy_username, ulen);
+ buf[2 + ulen] = (unsigned char)plen;
+ memcpy(buf + 3 + ulen, g_proxy_password, plen);
+
+ if (send(s, buf, 3 + ulen + plen, MSG_NOSIGNAL) != (ssize_t)(3 + ulen + plen))
+ return -1;
+
+ len = recv(s, buf, 2, 0);
+ if (len != 2 || buf[0] != 0x01 || buf[1] != 0x00)
+ return -1;
+ }
+ else if (buf[1] != SOCKS5_AUTH_NONE)
+ return -1;
+
+ // udp associate request
+ buf[0] = SOCKS5_VERSION;
+ buf[1] = SOCKS5_CMD_UDP_ASSOCIATE;
+ buf[2] = 0x00;
+ buf[3] = SOCKS5_ATYP_IPV4;
+ memset(buf + 4, 0, 4); // 0.0.0.0
+ memset(buf + 8, 0, 2); // port 0
+
+ if (send(s, buf, 10, MSG_NOSIGNAL) != 10)
+ return -1;
+
+ len = recv(s, buf, 512, 0);
+ if (len < 10 || buf[0] != SOCKS5_VERSION || buf[1] != 0x00)
+ return -1;
+
+ // get relay address
+ if (buf[3] == SOCKS5_ATYP_IPV4)
+ {
+ memset(relay_addr, 0, sizeof(*relay_addr));
+ relay_addr->sin_family = AF_INET;
+ memcpy(&relay_addr->sin_addr.s_addr, buf + 4, 4);
+ memcpy(&relay_addr->sin_port, buf + 8, 2);
+ return 0;
+ }
+
+ return -1;
+}
+
+static bool establish_udp_associate(void)
+{
+ uint64_t now = get_monotonic_ms();
+ if (now - last_udp_connect_attempt < 5000)
+ return false;
+
+ last_udp_connect_attempt = now;
+
+ if (socks5_udp_control_socket >= 0)
+ {
+ close(socks5_udp_control_socket);
+ socks5_udp_control_socket = -1;
+ }
+ if (socks5_udp_send_socket >= 0)
+ {
+ close(socks5_udp_send_socket);
+ socks5_udp_send_socket = -1;
+ }
+
+ int tcp_sock = socket(AF_INET, SOCK_STREAM, 0);
+ if (tcp_sock < 0)
+ return false;
+
+ configure_tcp_socket(tcp_sock, 262144, 3000);
+
+ uint32_t socks5_ip = resolve_hostname(g_proxy_host);
+ if (socks5_ip == 0)
+ {
+ close(tcp_sock);
+ return false;
+ }
+
+ struct sockaddr_in socks_addr;
+ memset(&socks_addr, 0, sizeof(socks_addr));
+ socks_addr.sin_family = AF_INET;
+ socks_addr.sin_addr.s_addr = socks5_ip;
+ socks_addr.sin_port = htons(g_proxy_port);
+
+ if (connect(tcp_sock, (struct sockaddr *)&socks_addr, sizeof(socks_addr)) < 0)
+ {
+ close(tcp_sock);
+ return false;
+ }
+
+ if (socks5_udp_associate(tcp_sock, &socks5_udp_relay_addr) != 0)
+ {
+ close(tcp_sock);
+ return false;
+ }
+
+ // rfc says if server gives 0.0.0.0 use proxy servers ip instead
+ if (socks5_udp_relay_addr.sin_addr.s_addr == INADDR_ANY)
+ socks5_udp_relay_addr.sin_addr.s_addr = socks5_ip;
+
+ socks5_udp_control_socket = tcp_sock;
+
+ socks5_udp_send_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+ if (socks5_udp_send_socket < 0)
+ {
+ close(socks5_udp_control_socket);
+ socks5_udp_control_socket = -1;
+ return false;
+ }
+
+ configure_udp_socket(socks5_udp_send_socket, 262144, 30000);
+
+ udp_associate_connected = true;
+ log_message("UDP ASSOCIATE established with SOCKS5 proxy");
+ return true;
+}
+
+// teardown udp associate so next packet reconnects
+static void teardown_udp_associate(void)
+{
+ udp_associate_connected = false;
+ if (socks5_udp_control_socket >= 0)
+ {
+ close(socks5_udp_control_socket);
+ socks5_udp_control_socket = -1;
+ }
+ if (socks5_udp_send_socket >= 0)
+ {
+ close(socks5_udp_send_socket);
+ socks5_udp_send_socket = -1;
+ }
+}
+
+static void* udp_relay_server(void *arg)
+{
+ (void)arg;
+ struct sockaddr_in local_addr, from_addr;
+ unsigned char recv_buf[65536];
+ unsigned char send_buf[65536];
+ socklen_t from_len;
+
+ udp_relay_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+ if (udp_relay_socket < 0)
+ return NULL;
+
+ int on = 1;
+ setsockopt(udp_relay_socket, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
+ configure_udp_socket(udp_relay_socket, 262144, 30000);
+
+ memset(&local_addr, 0, sizeof(local_addr));
+ local_addr.sin_family = AF_INET;
+ local_addr.sin_addr.s_addr = INADDR_ANY;
+ local_addr.sin_port = htons(LOCAL_UDP_RELAY_PORT);
+
+ if (bind(udp_relay_socket, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0)
+ {
+ close(udp_relay_socket);
+ udp_relay_socket = -1;
+ return NULL;
+ }
+
+ // try initial connect, not fatal if proxy not up yet
+ udp_associate_connected = establish_udp_associate();
+
+ while (running)
+ {
+ struct pollfd fds[3];
+ int nfds = 1;
+
+ // watch local relay socket for client packets
+ fds[0].fd = udp_relay_socket;
+ fds[0].events = POLLIN;
+ fds[0].revents = 0;
+
+ // watch socks5 udp socket for proxy responses
+ fds[1].fd = (udp_associate_connected && socks5_udp_send_socket >= 0) ? socks5_udp_send_socket : -1;
+ fds[1].events = POLLIN;
+ fds[1].revents = 0;
+
+ // watch socks5 tcp socket to detect if connection dies
+ fds[2].fd = (udp_associate_connected && socks5_udp_control_socket >= 0) ? socks5_udp_control_socket : -1;
+ fds[2].events = POLLIN;
+ fds[2].revents = 0;
+ nfds = 3;
+
+ int ready = poll(fds, nfds, 1000); // 1s timeout
+ if (ready <= 0)
+ continue;
+
+ // check if tcp control still alive
+ // if it dies the udp associate is dead too
+ if (fds[2].fd >= 0 && (fds[2].revents & (POLLIN | POLLHUP | POLLERR)))
+ {
+ char peek_buf[1];
+ ssize_t peek_len = recv(socks5_udp_control_socket, peek_buf, 1, MSG_PEEK | MSG_DONTWAIT);
+ if (peek_len == 0 || (peek_len < 0 && errno != EAGAIN && errno != EWOULDBLOCK))
+ {
+ // tcp died, proxy is gone
+ teardown_udp_associate();
+ continue;
+ }
+ }
+
+ // client sending to proxy
+ if (fds[0].revents & POLLIN)
+ {
+ from_len = sizeof(from_addr);
+ ssize_t recv_len = recvfrom(udp_relay_socket, recv_buf, sizeof(recv_buf), 0,
+ (struct sockaddr *)&from_addr, &from_len);
+ if (recv_len <= 0)
+ continue;
+
+ // try to connect if not connected yet
+ if (!udp_associate_connected)
+ {
+ if (!establish_udp_associate())
+ continue;
+ }
+
+ uint16_t client_port = ntohs(from_addr.sin_port);
+ uint32_t dest_ip;
+ uint16_t dest_port;
+
+ if (!get_connection(client_port, &dest_ip, &dest_port))
+ continue;
+
+ // make sure data fits with socks5 header
+ if (recv_len > (ssize_t)(sizeof(send_buf) - 10))
+ continue;
+
+ // build socks5 udp packet header
+ send_buf[0] = 0x00; // RSV
+ send_buf[1] = 0x00; // RSV
+ send_buf[2] = 0x00; // FRAG
+ send_buf[3] = SOCKS5_ATYP_IPV4;
+ memcpy(send_buf + 4, &dest_ip, 4);
+ uint16_t port_net = htons(dest_port);
+ memcpy(send_buf + 8, &port_net, 2);
+ memcpy(send_buf + 10, recv_buf, recv_len);
+
+ ssize_t sent = sendto(socks5_udp_send_socket, send_buf, 10 + recv_len, 0,
+ (struct sockaddr *)&socks5_udp_relay_addr, sizeof(socks5_udp_relay_addr));
+
+ // if send fails proxy died, teardown and retry later
+ if (sent < 0)
+ {
+ teardown_udp_associate();
+ }
+ }
+
+ // proxy sending back to client
+ if (fds[1].fd >= 0 && (fds[1].revents & POLLIN))
+ {
+ from_len = sizeof(from_addr);
+ ssize_t recv_len = recvfrom(socks5_udp_send_socket, recv_buf, sizeof(recv_buf), 0,
+ (struct sockaddr *)&from_addr, &from_len);
+
+ // socket error, proxy might be dead
+ if (recv_len < 0)
+ {
+ if (errno != EAGAIN && errno != EWOULDBLOCK)
+ teardown_udp_associate();
+ continue;
+ }
+ if (recv_len < 10)
+ continue;
+
+ // we dont support fragmented packets
+ if (recv_buf[2] != 0x00)
+ continue;
+
+ // parse socks5 udp packet
+ if (recv_buf[3] != SOCKS5_ATYP_IPV4)
+ continue;
+
+ uint32_t src_ip;
+ uint16_t src_port;
+ memcpy(&src_ip, recv_buf + 4, 4);
+ memcpy(&src_port, recv_buf + 8, 2);
+ src_port = ntohs(src_port);
+
+ // find which client sent packet to this destination
+ // loop thru hash table looking for dest match
+ pthread_rwlock_rdlock(&conn_lock);
+
+ struct sockaddr_in client_addr;
+ bool found_client = false;
+
+ for (int hash = 0; hash < CONNECTION_HASH_SIZE; hash++)
+ {
+ CONNECTION_INFO *conn = connection_hash_table[hash];
+ while (conn != NULL)
+ {
+ if (conn->orig_dest_ip == src_ip &&
+ conn->orig_dest_port == src_port)
+ {
+ // found it, send response back to original client port
+ memset(&client_addr, 0, sizeof(client_addr));
+ client_addr.sin_family = AF_INET;
+ client_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ client_addr.sin_port = htons(conn->src_port);
+ found_client = true;
+ break;
+ }
+ conn = conn->next;
+ }
+ if (found_client)
+ break;
+ }
+
+ pthread_rwlock_unlock(&conn_lock);
+
+ if (found_client)
+ {
+ // send unwrapped data back to client
+ ssize_t data_len = recv_len - 10;
+ sendto(udp_relay_socket, recv_buf + 10, data_len, 0,
+ (struct sockaddr *)&client_addr, sizeof(client_addr));
+ }
+ }
+
+ // handle errors on udp send socket
+ if (fds[1].fd >= 0 && (fds[1].revents & (POLLHUP | POLLERR)))
+ {
+ teardown_udp_associate();
+ }
+ }
+
+ teardown_udp_associate();
+ if (udp_relay_socket >= 0)
+ close(udp_relay_socket);
+
+ return NULL;
+}
+
+// nfqueue callback for packets
+static int packet_callback(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg, struct nfq_data *nfad, void *data)
+{
+ (void)nfmsg;
+ (void)data;
+
+ struct nfqnl_msg_packet_hdr *ph = nfq_get_msg_packet_hdr(nfad);
+ if (!ph) return nfq_set_verdict(qh, 0, NF_ACCEPT, 0, NULL);
+
+ uint32_t id = ntohl(ph->packet_id);
+
+ unsigned char *payload;
+ int payload_len = nfq_get_payload(nfad, &payload);
+ if (payload_len < (int)sizeof(struct iphdr))
+ return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
+
+ struct iphdr *iph = (struct iphdr *)payload;
+
+ // fast path when no rules
+ if (!g_has_active_rules && g_connection_callback == NULL)
+ return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
+
+ uint32_t src_ip = iph->saddr;
+ uint32_t dest_ip = iph->daddr;
+ uint16_t src_port = 0;
+ uint16_t dest_port = 0;
+ RuleAction action = RULE_ACTION_DIRECT;
+ uint32_t pid = 0;
+
+ if (iph->protocol == IPPROTO_TCP)
+ {
+ if (payload_len < (int)(iph->ihl * 4 + sizeof(struct tcphdr)))
+ return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
+
+ struct tcphdr *tcph = (struct tcphdr *)(payload + iph->ihl * 4);
+ src_port = ntohs(tcph->source);
+ dest_port = ntohs(tcph->dest);
+
+ // skip our own packets from local relay
+ if (src_port == g_local_relay_port)
+ return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
+
+ if (is_connection_tracked(src_port))
+ return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
+
+ // only look at syn packets for new connections
+ if (!(tcph->syn && !tcph->ack))
+ return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
+
+ if (dest_port == 53 && !g_dns_via_proxy)
+ action = RULE_ACTION_DIRECT;
+ else
+ action = check_process_rule(src_ip, src_port, dest_ip, dest_port, false, &pid);
+
+ if (action == RULE_ACTION_PROXY && is_broadcast_or_multicast(dest_ip))
+ action = RULE_ACTION_DIRECT;
+
+ // log it if not from our own process
+ if (g_traffic_logging_enabled && g_connection_callback != NULL && (tcph->syn && !tcph->ack) && pid > 0 && pid != g_current_process_id)
+ {
+ char process_name[MAX_PROCESS_NAME];
+ if (get_process_name_from_pid(pid, process_name, sizeof(process_name)))
+ {
+ if (!is_connection_already_logged(pid, dest_ip, dest_port, action))
+ {
+ char dest_ip_str[32];
+ format_ip_address(dest_ip, dest_ip_str, sizeof(dest_ip_str));
+
+ char proxy_info[300];
+ if (action == RULE_ACTION_PROXY)
+ {
+ snprintf(proxy_info, sizeof(proxy_info), "proxy %s://%s:%d tcp",
+ g_proxy_type == PROXY_TYPE_HTTP ? "http" : "socks5",
+ g_proxy_host, g_proxy_port);
+ }
+ else if (action == RULE_ACTION_DIRECT)
+ {
+ snprintf(proxy_info, sizeof(proxy_info), "direct tcp");
+ }
+ else if (action == RULE_ACTION_BLOCK)
+ {
+ snprintf(proxy_info, sizeof(proxy_info), "blocked tcp");
+ }
+
+ const char* display_name = extract_filename(process_name);
+ g_connection_callback(display_name, pid, dest_ip_str, dest_port, proxy_info);
+
+ add_logged_connection(pid, dest_ip, dest_port, action);
+ }
+ }
+ }
+
+ if (action == RULE_ACTION_DIRECT)
+ return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
+ else if (action == RULE_ACTION_BLOCK)
+ return nfq_set_verdict(qh, id, NF_DROP, 0, NULL);
+ else if (action == RULE_ACTION_PROXY)
+ {
+ // store connection info
+ add_connection(src_port, src_ip, dest_ip, dest_port);
+
+ // mark packet so nat table REDIRECT rule will catch it
+ uint32_t mark = 1;
+ return nfq_set_verdict2(qh, id, NF_ACCEPT, mark, 0, NULL);
+ }
+ }
+ else if (iph->protocol == IPPROTO_UDP)
+ {
+ if (payload_len < (int)(iph->ihl * 4 + sizeof(struct udphdr)))
+ return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
+
+ struct udphdr *udph = (struct udphdr *)(payload + iph->ihl * 4);
+ src_port = ntohs(udph->source);
+ dest_port = ntohs(udph->dest);
+
+ if (src_port == LOCAL_UDP_RELAY_PORT)
+ return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
+
+ if (is_connection_tracked(src_port))
+ return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
+
+ if (dest_port == 53 && !g_dns_via_proxy)
+ action = RULE_ACTION_DIRECT;
+ else
+ action = check_process_rule(src_ip, src_port, dest_ip, dest_port, true, &pid);
+
+ if (action == RULE_ACTION_PROXY && is_broadcast_or_multicast(dest_ip))
+ action = RULE_ACTION_DIRECT;
+
+ if (action == RULE_ACTION_PROXY && (dest_port == 67 || dest_port == 68))
+ action = RULE_ACTION_DIRECT;
+
+ // UDP proxy only works with SOCKS5, not HTTP
+ if (action == RULE_ACTION_PROXY && g_proxy_type != PROXY_TYPE_SOCKS5)
+ action = RULE_ACTION_DIRECT;
+
+ // log (skip our own process, log even without PID for ephemeral UDP sockets)
+ if (g_traffic_logging_enabled && g_connection_callback != NULL && pid != g_current_process_id)
+ {
+ char process_name[MAX_PROCESS_NAME];
+ uint32_t log_pid = (pid == 0) ? 1 : pid; // Use PID 1 for unknown processes
+
+ if (pid > 0 && get_process_name_from_pid(pid, process_name, sizeof(process_name)))
+ {
+ // Got process name from PID
+ }
+ else
+ {
+ // UDP socket not found - ephemeral or timing issue
+ snprintf(process_name, sizeof(process_name), "unknown");
+ }
+
+ if (!is_connection_already_logged(log_pid, dest_ip, dest_port, action))
+ {
+ char dest_ip_str[32];
+ format_ip_address(dest_ip, dest_ip_str, sizeof(dest_ip_str));
+
+ char proxy_info[300];
+ if (action == RULE_ACTION_PROXY)
+ {
+ snprintf(proxy_info, sizeof(proxy_info), "proxy socks5://%s:%d udp",
+ g_proxy_host, g_proxy_port);
+ }
+ else if (action == RULE_ACTION_DIRECT)
+ {
+ snprintf(proxy_info, sizeof(proxy_info), "direct udp");
+ }
+ else if (action == RULE_ACTION_BLOCK)
+ {
+ snprintf(proxy_info, sizeof(proxy_info), "blocked udp");
+ }
+
+ const char* display_name = extract_filename(process_name);
+ g_connection_callback(display_name, log_pid, dest_ip_str, dest_port, proxy_info);
+
+ add_logged_connection(log_pid, dest_ip, dest_port, action);
+ }
+ }
+
+ if (action == RULE_ACTION_DIRECT)
+ return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
+ else if (action == RULE_ACTION_BLOCK)
+ return nfq_set_verdict(qh, id, NF_DROP, 0, NULL);
+ else if (action == RULE_ACTION_PROXY)
+ {
+ // UDP proxy via SOCKS5 UDP ASSOCIATE
+ add_connection(src_port, src_ip, dest_ip, dest_port);
+
+ // Mark UDP packet for redirect to local UDP relay (port 34011)
+ uint32_t mark = 2; // Use mark=2 for UDP (mark=1 is for TCP)
+ return nfq_set_verdict2(qh, id, NF_ACCEPT, mark, 0, NULL);
+ }
+ }
+
+ return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
+}
+
+static void* packet_processor(void *arg)
+{
+ (void)arg;
+ char buf[4096] __attribute__((aligned));
+ int fd = nfq_fd(nfq_h);
+ ssize_t rv;
+
+ while (running)
+ {
+ rv = recv(fd, buf, sizeof(buf), 0);
+ if (rv >= 0)
+ nfq_handle_packet(nfq_h, buf, rv);
+ // On error: ENOBUFS = kernel queue full (normal under load),
+ // EINTR = signal, other = transient. Always continue -
+ // stopping the thread would break all network traffic.
+ }
+
+ return NULL;
+}
+
+static inline uint32_t connection_hash(uint16_t port)
+{
+ return port % CONNECTION_HASH_SIZE;
+}
+
+static void add_connection(uint16_t src_port, uint32_t src_ip, uint32_t dest_ip, uint16_t dest_port)
+{
+ uint32_t hash = connection_hash(src_port);
+ pthread_rwlock_wrlock(&conn_lock);
+
+ CONNECTION_INFO *conn = connection_hash_table[hash];
+ while (conn != NULL)
+ {
+ if (conn->src_port == src_port)
+ {
+ conn->orig_dest_ip = dest_ip;
+ conn->orig_dest_port = dest_port;
+ conn->src_ip = src_ip;
+ conn->is_tracked = true;
+ conn->last_activity = get_monotonic_ms();
+ pthread_rwlock_unlock(&conn_lock);
+ return;
+ }
+ conn = conn->next;
+ }
+
+ CONNECTION_INFO *new_conn = malloc(sizeof(CONNECTION_INFO));
+ if (new_conn != NULL)
+ {
+ new_conn->src_port = src_port;
+ new_conn->src_ip = src_ip;
+ new_conn->orig_dest_ip = dest_ip;
+ new_conn->orig_dest_port = dest_port;
+ new_conn->is_tracked = true;
+ new_conn->last_activity = get_monotonic_ms();
+ new_conn->next = connection_hash_table[hash];
+ connection_hash_table[hash] = new_conn;
+ }
+
+ pthread_rwlock_unlock(&conn_lock);
+}
+
+static bool get_connection(uint16_t src_port, uint32_t *dest_ip, uint16_t *dest_port)
+{
+ uint32_t hash = connection_hash(src_port);
+ pthread_rwlock_rdlock(&conn_lock);
+
+ CONNECTION_INFO *conn = connection_hash_table[hash];
+ while (conn != NULL)
+ {
+ if (conn->src_port == src_port && conn->is_tracked)
+ {
+ *dest_ip = conn->orig_dest_ip;
+ *dest_port = conn->orig_dest_port;
+ conn->last_activity = get_monotonic_ms(); // benign race on timestamp
+ pthread_rwlock_unlock(&conn_lock);
+ return true;
+ }
+ conn = conn->next;
+ }
+
+ pthread_rwlock_unlock(&conn_lock);
+ return false;
+}
+
+static bool is_connection_tracked(uint16_t src_port)
+{
+ uint32_t hash = connection_hash(src_port);
+ pthread_rwlock_rdlock(&conn_lock);
+
+ CONNECTION_INFO *conn = connection_hash_table[hash];
+ while (conn != NULL)
+ {
+ if (conn->src_port == src_port && conn->is_tracked)
+ {
+ pthread_rwlock_unlock(&conn_lock);
+ return true;
+ }
+ conn = conn->next;
+ }
+
+ pthread_rwlock_unlock(&conn_lock);
+ return false;
+}
+
+static void __attribute__((unused)) remove_connection(uint16_t src_port)
+{
+ uint32_t hash = connection_hash(src_port);
+ pthread_rwlock_wrlock(&conn_lock);
+
+ CONNECTION_INFO **conn_ptr = &connection_hash_table[hash];
+ while (*conn_ptr != NULL)
+ {
+ if ((*conn_ptr)->src_port == src_port)
+ {
+ CONNECTION_INFO *to_free = *conn_ptr;
+ *conn_ptr = (*conn_ptr)->next;
+ free(to_free);
+ pthread_rwlock_unlock(&conn_lock);
+ return;
+ }
+ conn_ptr = &(*conn_ptr)->next;
+ }
+
+ pthread_rwlock_unlock(&conn_lock);
+}
+
+static void cleanup_stale_connections(void)
+{
+ uint64_t now = get_monotonic_ms();
+
+ // Cleanup connection hash table
+ for (int i = 0; i < CONNECTION_HASH_SIZE; i++)
+ {
+ pthread_rwlock_wrlock(&conn_lock);
+ CONNECTION_INFO **conn_ptr = &connection_hash_table[i];
+
+ while (*conn_ptr != NULL)
+ {
+ if (now - (*conn_ptr)->last_activity > 60000) // 60 sec timeout
+ {
+ CONNECTION_INFO *to_free = *conn_ptr;
+ *conn_ptr = (*conn_ptr)->next;
+ free(to_free);
+ }
+ else
+ {
+ conn_ptr = &(*conn_ptr)->next;
+ }
+ }
+ pthread_rwlock_unlock(&conn_lock);
+ }
+
+ // Cleanup PID cache (separate lock - no contention with connection lookups)
+ uint64_t now_cache = get_monotonic_ms();
+ for (int i = 0; i < PID_CACHE_SIZE; i++)
+ {
+ pthread_mutex_lock(&pid_cache_lock);
+ PID_CACHE_ENTRY **entry_ptr = &pid_cache[i];
+ while (*entry_ptr != NULL)
+ {
+ if (now_cache - (*entry_ptr)->timestamp > 10000) // 10 sec cache TTL
+ {
+ PID_CACHE_ENTRY *to_free = *entry_ptr;
+ *entry_ptr = (*entry_ptr)->next;
+ free(to_free);
+ }
+ else
+ {
+ entry_ptr = &(*entry_ptr)->next;
+ }
+ }
+ pthread_mutex_unlock(&pid_cache_lock);
+ }
+
+ // Keep only last 100 logged connections
+ pthread_mutex_lock(&log_lock);
+ int logged_count = 0;
+ LOGGED_CONNECTION *temp = logged_connections;
+ while (temp != NULL)
+ {
+ logged_count++;
+ temp = temp->next;
+ }
+
+ if (logged_count > 100)
+ {
+ temp = logged_connections;
+ for (int i = 0; i < 99 && temp != NULL; i++)
+ {
+ temp = temp->next;
+ }
+ if (temp != NULL && temp->next != NULL)
+ {
+ LOGGED_CONNECTION *to_free = temp->next;
+ temp->next = NULL;
+ while (to_free != NULL)
+ {
+ LOGGED_CONNECTION *next = to_free->next;
+ free(to_free);
+ to_free = next;
+ }
+ }
+ }
+ pthread_mutex_unlock(&log_lock);
+}
+
+static bool is_connection_already_logged(uint32_t pid, uint32_t dest_ip, uint16_t dest_port, RuleAction action)
+{
+ pthread_mutex_lock(&log_lock);
+
+ LOGGED_CONNECTION *logged = logged_connections;
+ while (logged != NULL)
+ {
+ if (logged->pid == pid && logged->dest_ip == dest_ip &&
+ logged->dest_port == dest_port && logged->action == action)
+ {
+ pthread_mutex_unlock(&log_lock);
+ return true;
+ }
+ logged = logged->next;
+ }
+
+ pthread_mutex_unlock(&log_lock);
+ return false;
+}
+
+static void add_logged_connection(uint32_t pid, uint32_t dest_ip, uint16_t dest_port, RuleAction action)
+{
+ pthread_mutex_lock(&log_lock);
+
+ // keep only last 100 entries to avoid memory growth
+ int count = 0;
+ LOGGED_CONNECTION *temp = logged_connections;
+ while (temp != NULL && count < 100)
+ {
+ count++;
+ temp = temp->next;
+ }
+
+ if (count >= 100)
+ {
+ temp = logged_connections;
+ for (int i = 0; i < 98 && temp != NULL; i++)
+ {
+ temp = temp->next;
+ }
+
+ if (temp != NULL && temp->next != NULL)
+ {
+ LOGGED_CONNECTION *to_free_list = temp->next;
+ temp->next = NULL;
+
+ // Free excess entries (still under log_lock, but this is rare)
+ while (to_free_list != NULL)
+ {
+ LOGGED_CONNECTION *next = to_free_list->next;
+ free(to_free_list);
+ to_free_list = next;
+ }
+ }
+ }
+
+ LOGGED_CONNECTION *logged = malloc(sizeof(LOGGED_CONNECTION));
+ if (logged != NULL)
+ {
+ logged->pid = pid;
+ logged->dest_ip = dest_ip;
+ logged->dest_port = dest_port;
+ logged->action = action;
+ logged->next = logged_connections;
+ logged_connections = logged;
+ }
+
+ pthread_mutex_unlock(&log_lock);
+}
+
+static void clear_logged_connections(void)
+{
+ pthread_mutex_lock(&log_lock);
+
+ while (logged_connections != NULL)
+ {
+ LOGGED_CONNECTION *to_free = logged_connections;
+ logged_connections = logged_connections->next;
+ free(to_free);
+ }
+
+ pthread_mutex_unlock(&log_lock);
+}
+
+static uint32_t pid_cache_hash(uint32_t src_ip, uint16_t src_port, bool is_udp)
+{
+ uint32_t hash = src_ip ^ ((uint32_t)src_port << 16) ^ (is_udp ? 0x80000000 : 0);
+ return hash % PID_CACHE_SIZE;
+}
+
+static uint32_t get_cached_pid(uint32_t src_ip, uint16_t src_port, bool is_udp)
+{
+ uint32_t hash = pid_cache_hash(src_ip, src_port, is_udp);
+ uint64_t current_time = get_monotonic_ms();
+ uint32_t pid = 0;
+
+ pthread_mutex_lock(&pid_cache_lock);
+
+ PID_CACHE_ENTRY *entry = pid_cache[hash];
+ while (entry != NULL)
+ {
+ if (entry->src_ip == src_ip &&
+ entry->src_port == src_port &&
+ entry->is_udp == is_udp)
+ {
+ if (current_time - entry->timestamp < PID_CACHE_TTL_MS)
+ {
+ pid = entry->pid;
+ break;
+ }
+ else
+ {
+ break;
+ }
+ }
+ entry = entry->next;
+ }
+
+ pthread_mutex_unlock(&pid_cache_lock);
+ return pid;
+}
+
+static void cache_pid(uint32_t src_ip, uint16_t src_port, uint32_t pid, bool is_udp)
+{
+ uint32_t hash = pid_cache_hash(src_ip, src_port, is_udp);
+ uint64_t current_time = get_monotonic_ms();
+
+ pthread_mutex_lock(&pid_cache_lock);
+
+ PID_CACHE_ENTRY *entry = pid_cache[hash];
+ while (entry != NULL)
+ {
+ if (entry->src_ip == src_ip &&
+ entry->src_port == src_port &&
+ entry->is_udp == is_udp)
+ {
+ entry->pid = pid;
+ entry->timestamp = current_time;
+ pthread_mutex_unlock(&pid_cache_lock);
+ return;
+ }
+ entry = entry->next;
+ }
+
+ PID_CACHE_ENTRY *new_entry = malloc(sizeof(PID_CACHE_ENTRY));
+ if (new_entry != NULL)
+ {
+ new_entry->src_ip = src_ip;
+ new_entry->src_port = src_port;
+ new_entry->pid = pid;
+ new_entry->timestamp = current_time;
+ new_entry->is_udp = is_udp;
+ new_entry->next = pid_cache[hash];
+ pid_cache[hash] = new_entry;
+ }
+
+ pthread_mutex_unlock(&pid_cache_lock);
+}
+
+static void clear_pid_cache(void)
+{
+ pthread_mutex_lock(&pid_cache_lock);
+
+ for (int i = 0; i < PID_CACHE_SIZE; i++)
+ {
+ while (pid_cache[i] != NULL)
+ {
+ PID_CACHE_ENTRY *to_free = pid_cache[i];
+ pid_cache[i] = pid_cache[i]->next;
+ free(to_free);
+ }
+ }
+
+ pthread_mutex_unlock(&pid_cache_lock);
+}
+
+static void* cleanup_worker(void *arg)
+{
+ (void)arg;
+ while (running)
+ {
+ sleep(30); // 30 seconds
+ if (running)
+ {
+ cleanup_stale_connections();
+ }
+ }
+ return NULL;
+}
+
+static void update_has_active_rules(void)
+{
+ g_has_active_rules = false;
+ PROCESS_RULE *rule = rules_list;
+ while (rule != NULL)
+ {
+ if (rule->enabled)
+ {
+ g_has_active_rules = true;
+ break;
+ }
+ rule = rule->next;
+ }
+}
+
+uint32_t ProxyBridge_AddRule(const char* process_name, const char* target_hosts, const char* target_ports, RuleProtocol protocol, RuleAction action)
+{
+ if (process_name == NULL || process_name[0] == '\0')
+ return 0;
+
+ PROCESS_RULE *rule = malloc(sizeof(PROCESS_RULE));
+ if (rule == NULL)
+ return 0;
+
+ rule->rule_id = g_next_rule_id++;
+ strncpy(rule->process_name, process_name, MAX_PROCESS_NAME - 1);
+ rule->process_name[MAX_PROCESS_NAME - 1] = '\0';
+ rule->protocol = protocol;
+
+ if (target_hosts != NULL && target_hosts[0] != '\0')
+ {
+ rule->target_hosts = strdup(target_hosts);
+ if (rule->target_hosts == NULL)
+ {
+ free(rule);
+ return 0;
+ }
+ }
+ else
+ {
+ rule->target_hosts = strdup("*");
+ if (rule->target_hosts == NULL)
+ {
+ free(rule);
+ return 0;
+ }
+ }
+
+ if (target_ports != NULL && target_ports[0] != '\0')
+ {
+ rule->target_ports = strdup(target_ports);
+ if (rule->target_ports == NULL)
+ {
+ free(rule->target_hosts);
+ free(rule);
+ return 0;
+ }
+ }
+ else
+ {
+ rule->target_ports = strdup("*");
+ if (rule->target_ports == NULL)
+ {
+ free(rule->target_hosts);
+ free(rule);
+ return 0;
+ }
+ }
+
+ rule->action = action;
+ rule->enabled = true;
+
+ pthread_rwlock_wrlock(&rules_lock);
+ rule->next = rules_list;
+ rules_list = rule;
+ update_has_active_rules();
+ pthread_rwlock_unlock(&rules_lock);
+
+ log_message("added rule id %u for process %s protocol %d action %d", rule->rule_id, process_name, protocol, action);
+
+ return rule->rule_id;
+}
+
+bool ProxyBridge_EnableRule(uint32_t rule_id)
+{
+ if (rule_id == 0)
+ return false;
+
+ pthread_rwlock_wrlock(&rules_lock);
+ PROCESS_RULE *rule = rules_list;
+ while (rule != NULL)
+ {
+ if (rule->rule_id == rule_id)
+ {
+ rule->enabled = true;
+ update_has_active_rules();
+ pthread_rwlock_unlock(&rules_lock);
+ log_message("enabled rule id %u", rule_id);
+ return true;
+ }
+ rule = rule->next;
+ }
+ pthread_rwlock_unlock(&rules_lock);
+ return false;
+}
+
+bool ProxyBridge_DisableRule(uint32_t rule_id)
+{
+ if (rule_id == 0)
+ return false;
+
+ pthread_rwlock_wrlock(&rules_lock);
+ PROCESS_RULE *rule = rules_list;
+ while (rule != NULL)
+ {
+ if (rule->rule_id == rule_id)
+ {
+ rule->enabled = false;
+ update_has_active_rules();
+ pthread_rwlock_unlock(&rules_lock);
+ log_message("disabled rule id %u", rule_id);
+ return true;
+ }
+ rule = rule->next;
+ }
+ pthread_rwlock_unlock(&rules_lock);
+ return false;
+}
+
+bool ProxyBridge_DeleteRule(uint32_t rule_id)
+{
+ if (rule_id == 0)
+ return false;
+
+ pthread_rwlock_wrlock(&rules_lock);
+ PROCESS_RULE *rule = rules_list;
+ PROCESS_RULE *prev = NULL;
+
+ while (rule != NULL)
+ {
+ if (rule->rule_id == rule_id)
+ {
+ if (prev == NULL)
+ rules_list = rule->next;
+ else
+ prev->next = rule->next;
+
+ update_has_active_rules();
+ pthread_rwlock_unlock(&rules_lock);
+
+ if (rule->target_hosts != NULL)
+ free(rule->target_hosts);
+ if (rule->target_ports != NULL)
+ free(rule->target_ports);
+ free(rule);
+
+ log_message("deleted rule id %u", rule_id);
+ return true;
+ }
+ prev = rule;
+ rule = rule->next;
+ }
+ pthread_rwlock_unlock(&rules_lock);
+ return false;
+}
+
+bool ProxyBridge_EditRule(uint32_t rule_id, const char* process_name, const char* target_hosts, const char* target_ports, RuleProtocol protocol, RuleAction action)
+{
+ if (rule_id == 0 || process_name == NULL || target_hosts == NULL || target_ports == NULL)
+ return false;
+
+ // Pre-allocate new strings before taking lock to minimize hold time
+ char *new_hosts = strdup(target_hosts);
+ char *new_ports = strdup(target_ports);
+ if (new_hosts == NULL || new_ports == NULL)
+ {
+ free(new_hosts);
+ free(new_ports);
+ return false;
+ }
+
+ pthread_rwlock_wrlock(&rules_lock);
+ PROCESS_RULE *rule = rules_list;
+ while (rule != NULL)
+ {
+ if (rule->rule_id == rule_id)
+ {
+ strncpy(rule->process_name, process_name, MAX_PROCESS_NAME - 1);
+ rule->process_name[MAX_PROCESS_NAME - 1] = '\0';
+
+ free(rule->target_hosts);
+ rule->target_hosts = new_hosts;
+
+ free(rule->target_ports);
+ rule->target_ports = new_ports;
+
+ rule->protocol = protocol;
+ rule->action = action;
+
+ update_has_active_rules();
+ pthread_rwlock_unlock(&rules_lock);
+ log_message("updated rule id %u", rule_id);
+ return true;
+ }
+ rule = rule->next;
+ }
+ pthread_rwlock_unlock(&rules_lock);
+
+ // Rule not found - free pre-allocated strings
+ free(new_hosts);
+ free(new_ports);
+ return false;
+}
+
+bool ProxyBridge_SetProxyConfig(ProxyType type, const char* proxy_ip, uint16_t proxy_port, const char* username, const char* password)
+{
+ if (proxy_ip == NULL || proxy_ip[0] == '\0' || proxy_port == 0)
+ return false;
+
+ g_proxy_ip_cached = resolve_hostname(proxy_ip);
+ if (g_proxy_ip_cached == 0)
+ return false;
+
+ strncpy(g_proxy_host, proxy_ip, sizeof(g_proxy_host) - 1);
+ g_proxy_host[sizeof(g_proxy_host) - 1] = '\0';
+ g_proxy_port = proxy_port;
+ g_proxy_type = (type == PROXY_TYPE_HTTP) ? PROXY_TYPE_HTTP : PROXY_TYPE_SOCKS5;
+
+ if (username != NULL)
+ {
+ strncpy(g_proxy_username, username, sizeof(g_proxy_username) - 1);
+ g_proxy_username[sizeof(g_proxy_username) - 1] = '\0';
+ }
+ else
+ {
+ g_proxy_username[0] = '\0';
+ }
+
+ if (password != NULL)
+ {
+ strncpy(g_proxy_password, password, sizeof(g_proxy_password) - 1);
+ g_proxy_password[sizeof(g_proxy_password) - 1] = '\0';
+ }
+ else
+ {
+ g_proxy_password[0] = '\0';
+ }
+
+ log_message("proxy configured %s %s:%d", type == PROXY_TYPE_HTTP ? "http" : "socks5", proxy_ip, proxy_port);
+ return true;
+}
+
+void ProxyBridge_SetDnsViaProxy(bool enable)
+{
+ g_dns_via_proxy = enable;
+ log_message("dns via proxy %s", enable ? "enabled" : "disabled");
+}
+
+void ProxyBridge_SetLogCallback(LogCallback callback)
+{
+ g_log_callback = callback;
+}
+
+void ProxyBridge_SetConnectionCallback(ConnectionCallback callback)
+{
+ g_connection_callback = callback;
+}
+
+void ProxyBridge_SetTrafficLoggingEnabled(bool enable)
+{
+ g_traffic_logging_enabled = enable;
+}
+
+void ProxyBridge_ClearConnectionLogs(void)
+{
+ clear_logged_connections();
+}
+
+bool ProxyBridge_Start(void)
+{
+ if (running)
+ return false;
+
+ running = true;
+ g_current_process_id = getpid();
+
+ // Ignore SIGPIPE - send() on a closed socket must return EPIPE, not kill the process
+ signal(SIGPIPE, SIG_IGN);
+
+ // Raise system socket buffer limits for high throughput (requires root)
+ // Default rmem_max/wmem_max is usually 208KB, far too small for >100Mbps
+ FILE *fp;
+ fp = fopen("/proc/sys/net/core/rmem_max", "w");
+ if (fp) { fprintf(fp, "4194304"); fclose(fp); } // 4MB
+ fp = fopen("/proc/sys/net/core/wmem_max", "w");
+ if (fp) { fprintf(fp, "4194304"); fclose(fp); } // 4MB
+
+ if (pthread_create(&proxy_thread, NULL, local_proxy_server, NULL) != 0)
+ {
+ running = false;
+ return false;
+ }
+
+ if (pthread_create(&cleanup_thread, NULL, cleanup_worker, NULL) != 0)
+ {
+ running = false;
+ pthread_cancel(proxy_thread);
+ pthread_join(proxy_thread, NULL);
+ proxy_thread = 0;
+ return false;
+ }
+
+ // Start UDP relay server if SOCKS5 proxy
+ if (g_proxy_type == PROXY_TYPE_SOCKS5)
+ {
+ if (pthread_create(&udp_relay_thread, NULL, udp_relay_server, NULL) != 0)
+ {
+ log_message("failed to create UDP relay thread");
+ }
+ }
+
+ nfq_h = nfq_open();
+ if (!nfq_h)
+ {
+ log_message("nfq_open failed");
+ goto start_fail;
+ }
+
+ if (nfq_unbind_pf(nfq_h, AF_INET) < 0)
+ {
+ log_message("nfq_unbind_pf failed");
+ }
+
+ if (nfq_bind_pf(nfq_h, AF_INET) < 0)
+ {
+ log_message("nfq_bind_pf failed");
+ nfq_close(nfq_h);
+ nfq_h = NULL;
+ goto start_fail;
+ }
+
+ nfq_qh = nfq_create_queue(nfq_h, 0, &packet_callback, NULL);
+ if (!nfq_qh)
+ {
+ log_message("nfq_create_queue failed");
+ nfq_close(nfq_h);
+ nfq_h = NULL;
+ goto start_fail;
+ }
+
+ if (nfq_set_mode(nfq_qh, NFQNL_COPY_PACKET, 0xffff) < 0)
+ {
+ log_message("nfq_set_mode failed");
+ nfq_destroy_queue(nfq_qh);
+ nfq_qh = NULL;
+ nfq_close(nfq_h);
+ nfq_h = NULL;
+ goto start_fail;
+ }
+
+ // Set larger queue length for better performance (16384 like Windows)
+ nfq_set_queue_maxlen(nfq_qh, 16384);
+
+ // setup iptables rules for packet interception - USE MANGLE table so it runs BEFORE nat
+ log_message("setting up iptables rules");
+ // mangle table runs before nat, so we can mark packets there
+ int ret1 = run_iptables_cmd("-t", "mangle", "-A", "OUTPUT", "-p", "tcp", "-j", "NFQUEUE", "--queue-num", "0", NULL, NULL, NULL, NULL);
+ int ret2 = run_iptables_cmd("-t", "mangle", "-A", "OUTPUT", "-p", "udp", "-j", "NFQUEUE", "--queue-num", "0", NULL, NULL, NULL, NULL);
+
+ if (ret1 != 0 || ret2 != 0) {
+ log_message("failed to add iptables rules ret1=%d ret2=%d", ret1, ret2);
+ } else {
+ log_message("iptables nfqueue rules added successfully");
+ }
+
+ // setup nat redirect for marked packets
+ int ret3 = run_iptables_cmd("-t", "nat", "-A", "OUTPUT", "-p", "tcp", "-m", "mark", "--mark", "1", "-j", "REDIRECT", "--to-port", "34010");
+ int ret4 = run_iptables_cmd("-t", "nat", "-A", "OUTPUT", "-p", "udp", "-m", "mark", "--mark", "2", "-j", "REDIRECT", "--to-port", "34011");
+ if (ret3 != 0 || ret4 != 0) {
+ log_message("failed to add nat redirect rules");
+ }
+
+ (void)ret3;
+ (void)ret4;
+
+ for (int i = 0; i < NUM_PACKET_THREADS; i++)
+ {
+ if (pthread_create(&packet_thread[i], NULL, packet_processor, NULL) != 0)
+ {
+ log_message("failed to create packet thread %d", i);
+ }
+ }
+
+ log_message("proxybridge started");
+ return true;
+
+start_fail:
+ running = false;
+ if (proxy_thread != 0) { pthread_cancel(proxy_thread); pthread_join(proxy_thread, NULL); proxy_thread = 0; }
+ if (cleanup_thread != 0) { pthread_cancel(cleanup_thread); pthread_join(cleanup_thread, NULL); cleanup_thread = 0; }
+ if (udp_relay_thread != 0) { pthread_cancel(udp_relay_thread); pthread_join(udp_relay_thread, NULL); udp_relay_thread = 0; }
+ return false;
+}
+
+bool ProxyBridge_Stop(void)
+{
+ if (!running)
+ return false;
+
+ running = false;
+
+ // cleanup iptables
+ int ret1 = run_iptables_cmd("-t", "mangle", "-D", "OUTPUT", "-p", "tcp", "-j", "NFQUEUE", "--queue-num", "0", NULL, NULL, NULL, NULL);
+ int ret2 = run_iptables_cmd("-t", "mangle", "-D", "OUTPUT", "-p", "udp", "-j", "NFQUEUE", "--queue-num", "0", NULL, NULL, NULL, NULL);
+ int ret3 = run_iptables_cmd("-t", "nat", "-D", "OUTPUT", "-p", "tcp", "-m", "mark", "--mark", "1", "-j", "REDIRECT", "--to-port", "34010");
+ int ret4 = run_iptables_cmd("-t", "nat", "-D", "OUTPUT", "-p", "udp", "-m", "mark", "--mark", "2", "-j", "REDIRECT", "--to-port", "34011");
+ (void)ret1;
+ (void)ret2;
+ (void)ret3;
+ (void)ret4;
+
+ for (int i = 0; i < NUM_PACKET_THREADS; i++)
+ {
+ if (packet_thread[i] != 0)
+ {
+ pthread_cancel(packet_thread[i]);
+ pthread_join(packet_thread[i], NULL);
+ packet_thread[i] = 0;
+ }
+ }
+
+ if (nfq_qh)
+ {
+ nfq_destroy_queue(nfq_qh);
+ nfq_qh = NULL;
+ }
+
+ if (nfq_h)
+ {
+ nfq_close(nfq_h);
+ nfq_h = NULL;
+ }
+
+ if (proxy_thread != 0)
+ {
+ pthread_cancel(proxy_thread);
+ pthread_join(proxy_thread, NULL);
+ proxy_thread = 0;
+ }
+
+ if (udp_relay_thread != 0)
+ {
+ pthread_cancel(udp_relay_thread);
+ pthread_join(udp_relay_thread, NULL);
+ udp_relay_thread = 0;
+ }
+
+ if (cleanup_thread != 0)
+ {
+ pthread_cancel(cleanup_thread);
+ pthread_join(cleanup_thread, NULL);
+ cleanup_thread = 0;
+ }
+
+ // Free all connections in hash table
+ pthread_rwlock_wrlock(&conn_lock);
+ for (int i = 0; i < CONNECTION_HASH_SIZE; i++)
+ {
+ while (connection_hash_table[i] != NULL)
+ {
+ CONNECTION_INFO *to_free = connection_hash_table[i];
+ connection_hash_table[i] = connection_hash_table[i]->next;
+ free(to_free);
+ }
+ }
+ pthread_rwlock_unlock(&conn_lock);
+
+ // Free all rules
+ pthread_rwlock_wrlock(&rules_lock);
+ while (rules_list != NULL)
+ {
+ PROCESS_RULE *to_free = rules_list;
+ rules_list = rules_list->next;
+ free(to_free->target_hosts);
+ free(to_free->target_ports);
+ free(to_free);
+ }
+ g_has_active_rules = false;
+ g_next_rule_id = 1;
+ pthread_rwlock_unlock(&rules_lock);
+
+ clear_logged_connections();
+ clear_pid_cache();
+
+ log_message("proxybridge stopped");
+ return true;
+}
+
+int ProxyBridge_TestConnection(const char* target_host, uint16_t target_port, char* result_buffer, size_t buffer_size)
+{
+ int test_sock = -1;
+ struct sockaddr_in proxy_addr;
+ uint32_t target_ip;
+ int ret = -1;
+ char temp_buffer[512];
+
+ if (g_proxy_host[0] == '\0' || g_proxy_port == 0)
+ {
+ snprintf(result_buffer, buffer_size, "error no proxy configured");
+ return -1;
+ }
+
+ if (target_host == NULL || target_host[0] == '\0')
+ {
+ snprintf(result_buffer, buffer_size, "error invalid target host");
+ return -1;
+ }
+
+ snprintf(temp_buffer, sizeof(temp_buffer), "testing connection to %s:%d via %s proxy %s:%d\n",
+ target_host, target_port,
+ g_proxy_type == PROXY_TYPE_HTTP ? "http" : "socks5",
+ g_proxy_host, g_proxy_port);
+ strncpy(result_buffer, temp_buffer, buffer_size - 1);
+ result_buffer[buffer_size - 1] = '\0';
+
+ struct addrinfo hints, *res;
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_INET;
+ hints.ai_socktype = SOCK_STREAM;
+ int gai_ret = getaddrinfo(target_host, NULL, &hints, &res);
+ if (gai_ret != 0)
+ {
+ snprintf(temp_buffer, sizeof(temp_buffer), "error failed to resolve hostname %s: %s\n",
+ target_host, gai_strerror(gai_ret));
+ strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1);
+ return -1;
+ }
+ target_ip = ((struct sockaddr_in *)res->ai_addr)->sin_addr.s_addr;
+ freeaddrinfo(res);
+
+ snprintf(temp_buffer, sizeof(temp_buffer), "resolved %s to %d.%d.%d.%d\n",
+ target_host,
+ (target_ip >> 0) & 0xFF, (target_ip >> 8) & 0xFF,
+ (target_ip >> 16) & 0xFF, (target_ip >> 24) & 0xFF);
+ strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1);
+
+ test_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+ if (test_sock < 0)
+ {
+ snprintf(temp_buffer, sizeof(temp_buffer), "error socket creation failed\n");
+ strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1);
+ return -1;
+ }
+
+ configure_tcp_socket(test_sock, 65536, 10000);
+
+ memset(&proxy_addr, 0, sizeof(proxy_addr));
+ proxy_addr.sin_family = AF_INET;
+ proxy_addr.sin_addr.s_addr = resolve_hostname(g_proxy_host);
+ proxy_addr.sin_port = htons(g_proxy_port);
+
+ snprintf(temp_buffer, sizeof(temp_buffer), "connecting to proxy %s:%d\n", g_proxy_host, g_proxy_port);
+ strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1);
+
+ if (connect(test_sock, (struct sockaddr*)&proxy_addr, sizeof(proxy_addr)) < 0)
+ {
+ snprintf(temp_buffer, sizeof(temp_buffer), "error failed to connect to proxy\n");
+ strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1);
+ close(test_sock);
+ return -1;
+ }
+
+ strncat(result_buffer, "connected to proxy server\n", buffer_size - strlen(result_buffer) - 1);
+
+ if (g_proxy_type == PROXY_TYPE_SOCKS5)
+ {
+ if (socks5_connect(test_sock, target_ip, target_port) != 0)
+ {
+ snprintf(temp_buffer, sizeof(temp_buffer), "error socks5 handshake failed\n");
+ strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1);
+ close(test_sock);
+ return -1;
+ }
+ strncat(result_buffer, "socks5 handshake successful\n", buffer_size - strlen(result_buffer) - 1);
+ }
+ else
+ {
+ if (http_connect(test_sock, target_ip, target_port) != 0)
+ {
+ snprintf(temp_buffer, sizeof(temp_buffer), "error http connect failed\n");
+ strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1);
+ close(test_sock);
+ return -1;
+ }
+ strncat(result_buffer, "http connect successful\n", buffer_size - strlen(result_buffer) - 1);
+ }
+
+ char http_request[512];
+ snprintf(http_request, sizeof(http_request),
+ "GET / HTTP/1.1\r\n"
+ "Host: %s\r\n"
+ "Connection: close\r\n"
+ "User-Agent: ProxyBridge/1.0\r\n"
+ "\r\n", target_host);
+
+ if (send_all(test_sock, http_request, strlen(http_request)) < 0)
+ {
+ snprintf(temp_buffer, sizeof(temp_buffer), "error failed to send test request\n");
+ strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1);
+ close(test_sock);
+ return -1;
+ }
+
+ strncat(result_buffer, "sent http get request\n", buffer_size - strlen(result_buffer) - 1);
+ char response[1024];
+ ssize_t bytes_received = recv(test_sock, response, sizeof(response) - 1, 0);
+ if (bytes_received > 0)
+ {
+ response[bytes_received] = '\0';
+
+ if (strstr(response, "HTTP/") != NULL)
+ {
+ char* status_line = strstr(response, "HTTP/");
+ int status_code = 0;
+ if (status_line != NULL)
+ {
+ sscanf(status_line, "HTTP/%*s %d", &status_code);
+ }
+
+ snprintf(temp_buffer, sizeof(temp_buffer), "success received http %d response %ld bytes\n", status_code, (long)bytes_received);
+ strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1);
+ ret = 0;
+ }
+ else
+ {
+ snprintf(temp_buffer, sizeof(temp_buffer), "error received data but not valid http response\n");
+ strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1);
+ ret = -1;
+ }
+ }
+ else
+ {
+ snprintf(temp_buffer, sizeof(temp_buffer), "error failed to receive response\n");
+ strncat(result_buffer, temp_buffer, buffer_size - strlen(result_buffer) - 1);
+ ret = -1;
+ }
+
+ close(test_sock);
+
+ if (ret == 0)
+ {
+ strncat(result_buffer, "\nproxy connection test passed\n", buffer_size - strlen(result_buffer) - 1);
+ }
+ else
+ {
+ strncat(result_buffer, "\nproxy connection test failed\n", buffer_size - strlen(result_buffer) - 1);
+ }
+
+ return ret;
+}
+
+// Library destructor - automatically cleanup when library is unloaded
+__attribute__((destructor))
+static void library_cleanup(void)
+{
+ if (running)
+ {
+ log_message("library unloading - cleaning up automatically");
+ ProxyBridge_Stop();
+ }
+ else
+ {
+ // Even if not running, ensure iptables rules are removed
+ // This handles cases where the app crashed before calling Stop
+ run_iptables_cmd("-t", "mangle", "-D", "OUTPUT", "-p", "tcp", "-j", "NFQUEUE", "--queue-num", "0", NULL, NULL, NULL, NULL);
+ run_iptables_cmd("-t", "mangle", "-D", "OUTPUT", "-p", "udp", "-j", "NFQUEUE", "--queue-num", "0", NULL, NULL, NULL, NULL);
+ run_iptables_cmd("-t", "nat", "-D", "OUTPUT", "-p", "tcp", "-m", "mark", "--mark", "1", "-j", "REDIRECT", "--to-port", "34010");
+ run_iptables_cmd("-t", "nat", "-D", "OUTPUT", "-p", "udp", "-m", "mark", "--mark", "2", "-j", "REDIRECT", "--to-port", "34011");
+ }
+}
diff --git a/Linux/src/ProxyBridge.h b/Linux/src/ProxyBridge.h
new file mode 100644
index 0000000..bd80583
--- /dev/null
+++ b/Linux/src/ProxyBridge.h
@@ -0,0 +1,52 @@
+#ifndef PROXYBRIDGE_H
+#define PROXYBRIDGE_H
+
+#define PROXYBRIDGE_VERSION "3.2.0"
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef void (*LogCallback)(const char* message);
+typedef void (*ConnectionCallback)(const char* process_name, uint32_t pid, const char* dest_ip, uint16_t dest_port, const char* proxy_info);
+
+typedef enum {
+ PROXY_TYPE_HTTP = 0,
+ PROXY_TYPE_SOCKS5 = 1
+} ProxyType;
+
+typedef enum {
+ RULE_ACTION_PROXY = 0,
+ RULE_ACTION_DIRECT = 1,
+ RULE_ACTION_BLOCK = 2
+} RuleAction;
+
+typedef enum {
+ RULE_PROTOCOL_TCP = 0,
+ RULE_PROTOCOL_UDP = 1,
+ RULE_PROTOCOL_BOTH = 2
+} RuleProtocol;
+
+uint32_t ProxyBridge_AddRule(const char* process_name, const char* target_hosts, const char* target_ports, RuleProtocol protocol, RuleAction action);
+bool ProxyBridge_EnableRule(uint32_t rule_id);
+bool ProxyBridge_DisableRule(uint32_t rule_id);
+bool ProxyBridge_DeleteRule(uint32_t rule_id);
+bool ProxyBridge_EditRule(uint32_t rule_id, const char* process_name, const char* target_hosts, const char* target_ports, RuleProtocol protocol, RuleAction action);
+bool ProxyBridge_SetProxyConfig(ProxyType type, const char* proxy_ip, uint16_t proxy_port, const char* username, const char* password);
+void ProxyBridge_SetDnsViaProxy(bool enable);
+void ProxyBridge_SetLogCallback(LogCallback callback);
+void ProxyBridge_SetConnectionCallback(ConnectionCallback callback);
+void ProxyBridge_SetTrafficLoggingEnabled(bool enable);
+void ProxyBridge_ClearConnectionLogs(void);
+bool ProxyBridge_Start(void);
+bool ProxyBridge_Stop(void);
+int ProxyBridge_TestConnection(const char* target_host, uint16_t target_port, char* result_buffer, size_t buffer_size);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/MacOS/ProxyBridge/ProxyBridge.xcodeproj/project.pbxproj b/MacOS/ProxyBridge/ProxyBridge.xcodeproj/project.pbxproj
index 26ead15..d761d8c 100644
--- a/MacOS/ProxyBridge/ProxyBridge.xcodeproj/project.pbxproj
+++ b/MacOS/ProxyBridge/ProxyBridge.xcodeproj/project.pbxproj
@@ -9,14 +9,6 @@
/* Begin PBXBuildFile section */
8466A1E92EC63C5600A8C2E5 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8466A1E82EC63C5600A8C2E5 /* NetworkExtension.framework */; };
8466A1F32EC63C5600A8C2E5 /* com.interceptsuite.ProxyBridge.extension.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 8466A1E62EC63C5600A8C2E5 /* com.interceptsuite.ProxyBridge.extension.systemextension */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
- 846F78F92F14204F00C94754 /* Signing-Config-ext.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 846F78F82F14204F00C94754 /* Signing-Config-ext.xcconfig */; };
- 846F78FA2F14204F00C94754 /* Signing-Config-ext.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 846F78F82F14204F00C94754 /* Signing-Config-ext.xcconfig */; };
- 84AFF4672F23CAC8007CF0B0 /* Signing-Config-app.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 84AFF4662F23CAC8007CF0B0 /* Signing-Config-app.xcconfig */; };
- 84AFF4682F23CAC8007CF0B0 /* Signing-Config-app.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 84AFF4662F23CAC8007CF0B0 /* Signing-Config-app.xcconfig */; };
- 84AFF46A2F23CAD5007CF0B0 /* Signing-Config-ext.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 84AFF4692F23CAD5007CF0B0 /* Signing-Config-ext.xcconfig */; };
- 84AFF46B2F23CAD5007CF0B0 /* Signing-Config-ext.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 84AFF4692F23CAD5007CF0B0 /* Signing-Config-ext.xcconfig */; };
- 84B1D8012F141B2300D0E39B /* Signing-Config-app.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 84B1D8002F141B2300D0E39B /* Signing-Config-app.xcconfig */; };
- 84B1D8022F141B2300D0E39B /* Signing-Config-app.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 84B1D8002F141B2300D0E39B /* Signing-Config-app.xcconfig */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -44,6 +36,8 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 1B7672902F421AB400B323D6 /* Signing-Config-app.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Signing-Config-app.xcconfig"; sourceTree = ""; };
+ 1B7672932F421ABA00B323D6 /* Signing-Config-ext.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Signing-Config-ext.xcconfig"; sourceTree = ""; };
8466A1D32EC63B9400A8C2E5 /* ProxyBridge.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ProxyBridge.app; sourceTree = BUILT_PRODUCTS_DIR; };
8466A1E62EC63C5600A8C2E5 /* com.interceptsuite.ProxyBridge.extension.systemextension */ = {isa = PBXFileReference; explicitFileType = "wrapper.system-extension"; includeInIndex = 0; path = com.interceptsuite.ProxyBridge.extension.systemextension; sourceTree = BUILT_PRODUCTS_DIR; };
8466A1E82EC63C5600A8C2E5 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
@@ -109,6 +103,8 @@
8466A1D42EC63B9400A8C2E5 /* Products */,
84AFF4662F23CAC8007CF0B0 /* Signing-Config-app.xcconfig */,
84AFF4692F23CAD5007CF0B0 /* Signing-Config-ext.xcconfig */,
+ 1B7672902F421AB400B323D6 /* Signing-Config-app.xcconfig */,
+ 1B7672932F421ABA00B323D6 /* Signing-Config-ext.xcconfig */,
);
sourceTree = "";
};
@@ -221,10 +217,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 846F78FA2F14204F00C94754 /* Signing-Config-ext.xcconfig in Resources */,
- 84AFF46B2F23CAD5007CF0B0 /* Signing-Config-ext.xcconfig in Resources */,
- 84AFF4672F23CAC8007CF0B0 /* Signing-Config-app.xcconfig in Resources */,
- 84B1D8012F141B2300D0E39B /* Signing-Config-app.xcconfig in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -232,10 +224,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 846F78F92F14204F00C94754 /* Signing-Config-ext.xcconfig in Resources */,
- 84B1D8022F141B2300D0E39B /* Signing-Config-app.xcconfig in Resources */,
- 84AFF46A2F23CAD5007CF0B0 /* Signing-Config-ext.xcconfig in Resources */,
- 84AFF4682F23CAC8007CF0B0 /* Signing-Config-app.xcconfig in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -382,7 +370,8 @@
MACOSX_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
- ONLY_ACTIVE_ARCH = YES;
+ ONLY_ACTIVE_ARCH = NO;
+ ARCHS = "arm64 x86_64";
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
};
@@ -398,7 +387,6 @@
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = "";
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
@@ -422,7 +410,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
- MARKETING_VERSION = 3.1;
+ MARKETING_VERSION = 3.2.0;
PRODUCT_BUNDLE_IDENTIFIER = com.interceptsuite.ProxyBridge;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -438,7 +426,7 @@
};
8466A1E02EC63B9500A8C2E5 /* Release */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 84AFF4662F23CAC8007CF0B0 /* Signing-Config-app.xcconfig */;
+ baseConfigurationReference = 1B7672902F421AB400B323D6 /* Signing-Config-app.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
@@ -467,7 +455,8 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
- MARKETING_VERSION = 3.1;
+ MARKETING_VERSION = 3.2.0;
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "ProxyBridge Prod";
REGISTER_APP_GROUPS = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
@@ -507,7 +496,7 @@
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
- MARKETING_VERSION = 3.1;
+ MARKETING_VERSION = 3.2.0;
PRODUCT_BUNDLE_IDENTIFIER = com.interceptsuite.ProxyBridge.extension;
PRODUCT_NAME = "$(inherited)";
REGISTER_APP_GROUPS = YES;
@@ -522,7 +511,7 @@
};
8466A1F72EC63C5600A8C2E5 /* Release */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 84AFF4692F23CAD5007CF0B0 /* Signing-Config-ext.xcconfig */;
+ baseConfigurationReference = 1B7672932F421ABA00B323D6 /* Signing-Config-ext.xcconfig */;
buildSettings = {
CURRENT_PROJECT_VERSION = 1;
ENABLE_APP_SANDBOX = YES;
@@ -546,7 +535,8 @@
"@executable_path/../Frameworks",
"@executable_path/../../../../Frameworks",
);
- MARKETING_VERSION = 3.1;
+ MARKETING_VERSION = 3.2.0;
+ "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "ProxyBridge Extension Prod";
REGISTER_APP_GROUPS = YES;
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
diff --git a/MacOS/ProxyBridge/ProxyBridge/ProxyBridgeViewModel.swift b/MacOS/ProxyBridge/ProxyBridge/ProxyBridgeViewModel.swift
index 6ae2285..44b938e 100644
--- a/MacOS/ProxyBridge/ProxyBridge/ProxyBridgeViewModel.swift
+++ b/MacOS/ProxyBridge/ProxyBridge/ProxyBridgeViewModel.swift
@@ -14,8 +14,20 @@ class ProxyBridgeViewModel: NSObject, ObservableObject {
private(set) var proxyConfig: ProxyConfig?
private let maxLogEntries = 1000
+ // trim to 80% when limit hit to avoid trimming on each entry
+ private let trimToEntries = 800
private let logPollingInterval = 1.0
private let extensionIdentifier = "com.interceptsuite.ProxyBridge.extension"
+ // reuse formatter
+ // saves memory about 2% and speed up the ui
+ private let timestampFormatter: DateFormatter = {
+ let f = DateFormatter()
+ f.dateFormat = "HH:mm:ss"
+ return f
+ }()
+ // removed uuid and use int - memory usage and speed improved due to size
+ private var connectionIdCounter: Int = 0
+ private var activityIdCounter: Int = 0
struct ProxyConfig {
let type: String
@@ -26,7 +38,7 @@ class ProxyBridgeViewModel: NSObject, ObservableObject {
}
struct ConnectionLog: Identifiable {
- let id = UUID()
+ let id: Int
let timestamp: String
let connectionProtocol: String
let process: String
@@ -36,7 +48,7 @@ class ProxyBridgeViewModel: NSObject, ObservableObject {
}
struct ActivityLog: Identifiable {
- let id = UUID()
+ let id: Int
let timestamp: String
let level: String
let message: String
@@ -115,6 +127,26 @@ class ProxyBridgeViewModel: NSObject, ObservableObject {
}
private func installAndStartProxy() {
+ // Stop any existing tunnel first so macOS replaces the running extension
+ // binary with the newly installed one instead of reusing the old cached process.
+ NETransparentProxyManager.loadAllFromPreferences { [weak self] managers, error in
+ guard let self = self else { return }
+
+ if let existing = managers?.first,
+ let session = existing.connection as? NETunnelProviderSession,
+ session.status != .disconnected && session.status != .invalid {
+ session.stopTunnel()
+ // Brief pause to let the old extension fully terminate
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
+ self.submitExtensionActivationRequest()
+ }
+ } else {
+ self.submitExtensionActivationRequest()
+ }
+ }
+ }
+
+ private func submitExtensionActivationRequest() {
let request = OSSystemExtensionRequest.activationRequest(
forExtensionWithIdentifier: extensionIdentifier,
queue: .main
@@ -296,7 +328,9 @@ class ProxyBridgeViewModel: NSObject, ObservableObject {
return
}
+ connectionIdCounter &+= 1
let connectionLog = ConnectionLog(
+ id: connectionIdCounter,
timestamp: getCurrentTimestamp(),
connectionProtocol: proto,
process: process,
@@ -306,8 +340,9 @@ class ProxyBridgeViewModel: NSObject, ObservableObject {
)
connections.append(connectionLog)
+ // Trim in bulk to avoid O(n) shift on every entry at the limit
if connections.count > maxLogEntries {
- connections.removeFirst()
+ connections.removeFirst(connections.count - trimToEntries)
}
}
@@ -318,7 +353,9 @@ class ProxyBridgeViewModel: NSObject, ObservableObject {
return
}
+ activityIdCounter &+= 1
let activityLog = ActivityLog(
+ id: activityIdCounter,
timestamp: timestamp,
level: level,
message: message
@@ -326,7 +363,7 @@ class ProxyBridgeViewModel: NSObject, ObservableObject {
activityLogs.append(activityLog)
if activityLogs.count > maxLogEntries {
- activityLogs.removeFirst()
+ activityLogs.removeFirst(activityLogs.count - trimToEntries)
}
}
@@ -382,7 +419,9 @@ class ProxyBridgeViewModel: NSObject, ObservableObject {
}
private func addLog(_ level: String, _ message: String) {
+ activityIdCounter &+= 1
let log = ActivityLog(
+ id: activityIdCounter,
timestamp: getCurrentTimestamp(),
level: level,
message: message
@@ -390,14 +429,12 @@ class ProxyBridgeViewModel: NSObject, ObservableObject {
activityLogs.append(log)
if activityLogs.count > maxLogEntries {
- activityLogs.removeFirst()
+ activityLogs.removeFirst(activityLogs.count - trimToEntries)
}
}
private func getCurrentTimestamp() -> String {
- let formatter = DateFormatter()
- formatter.dateFormat = "HH:mm:ss"
- return formatter.string(from: Date())
+ return timestampFormatter.string(from: Date())
}
deinit {
diff --git a/MacOS/ProxyBridge/ProxyBridge/ProxyRulesView.swift b/MacOS/ProxyBridge/ProxyBridge/ProxyRulesView.swift
index c6609b9..617237a 100644
--- a/MacOS/ProxyBridge/ProxyBridge/ProxyRulesView.swift
+++ b/MacOS/ProxyBridge/ProxyBridge/ProxyRulesView.swift
@@ -410,7 +410,7 @@ struct RuleEditorView: View {
label: "Target hosts",
placeholder: "*",
text: $targetHosts,
- hint: "Example: 127.0.0.1; *.example.com; 192.168.1.*; 10.1.0.0-10.5.255.255"
+ hint: "Example: 127.0.0.1; 192.168.1.*; 10.0.0.1-10.0.0.254"
)
formField(
diff --git a/MacOS/ProxyBridge/ProxyBridge/UpdateService.swift b/MacOS/ProxyBridge/ProxyBridge/UpdateService.swift
index 529937b..ac828f3 100644
--- a/MacOS/ProxyBridge/ProxyBridge/UpdateService.swift
+++ b/MacOS/ProxyBridge/ProxyBridge/UpdateService.swift
@@ -63,10 +63,13 @@ class UpdateService {
asset.name.lowercased().contains("installer"))
}
+ // a macOS pkg installer in the release is valid update
+ let isUpdateAvailable = isNewerVersion(latestVersion, currentVersion) && pkgAsset != nil
+
return VersionInfo(
currentVersion: currentVersion,
latestVersion: release.tagName,
- isUpdateAvailable: isNewerVersion(latestVersion, currentVersion),
+ isUpdateAvailable: isUpdateAvailable,
downloadUrl: pkgAsset?.browserDownloadUrl,
fileName: pkgAsset?.name,
error: nil
diff --git a/MacOS/ProxyBridge/build.sh b/MacOS/ProxyBridge/build.sh
index 94fe978..52628e5 100755
--- a/MacOS/ProxyBridge/build.sh
+++ b/MacOS/ProxyBridge/build.sh
@@ -3,7 +3,7 @@
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-VERSION="3.1.0"
+VERSION="3.2.0"
if [ -f "$SCRIPT_DIR/.env" ]; then
source "$SCRIPT_DIR/.env"
@@ -26,7 +26,7 @@ echo "Creating installer package..."
pkgbuild \
--root build/component \
--identifier com.interceptsuite.ProxyBridge \
- --version 3.1.0 \
+ --version 3.2.0 \
--install-location /Applications \
build/temp.pkg
@@ -45,7 +45,7 @@ cat > build/distribution.xml << 'EOF'
- temp.pkg
+ temp.pkg
EOF
diff --git a/MacOS/ProxyBridge/extension/AppProxyProvider.swift b/MacOS/ProxyBridge/extension/AppProxyProvider.swift
index ac6cd18..b4f61bf 100644
--- a/MacOS/ProxyBridge/extension/AppProxyProvider.swift
+++ b/MacOS/ProxyBridge/extension/AppProxyProvider.swift
@@ -1,4 +1,5 @@
import NetworkExtension
+import Foundation
enum RuleProtocol: String, Codable {
case tcp = "TCP"
@@ -52,8 +53,18 @@ struct ProxyRule: Codable {
self.enabled = enabled
}
- func matchesProcess(_ processPath: String) -> Bool {
- return Self.matchProcessList(processNames, processPath: processPath)
+ func matchesProcess(bundleId: String, processName: String?) -> Bool {
+ if Self.matchProcessList(processNames, processPath: bundleId) {
+ return true
+ }
+
+ if let procName = processName {
+ if Self.matchProcessList(processNames, processPath: procName) {
+ return true
+ }
+ }
+
+ return false
}
func matchesIP(_ ipString: String) -> Bool {
@@ -124,11 +135,40 @@ struct ProxyRule: Codable {
return false
}
+ private static func ipToInteger(_ ipString: String) -> UInt32? {
+ let octets = ipString.components(separatedBy: ".")
+ guard octets.count == 4 else { return nil }
+
+ var result: UInt32 = 0
+ for octet in octets {
+ guard let value = UInt8(octet) else { return nil }
+ result = (result << 8) | UInt32(value)
+ }
+ return result
+ }
+
private static func matchIPPattern(_ pattern: String, ipString: String) -> Bool {
if pattern.isEmpty || pattern == "*" {
return true
}
+ // Check for IP range (e.g., 192.168.1.1-192.168.1.254)
+ if pattern.contains("-") {
+ let parts = pattern.components(separatedBy: "-")
+ if parts.count == 2 {
+ let startIP = parts[0].trimmingCharacters(in: .whitespaces)
+ let endIP = parts[1].trimmingCharacters(in: .whitespaces)
+
+ if let startInt = ipToInteger(startIP),
+ let endInt = ipToInteger(endIP),
+ let targetInt = ipToInteger(ipString) {
+ return targetInt >= startInt && targetInt <= endInt
+ }
+ }
+ return false
+ }
+
+ // Wildcard matching (e.g., 192.168.1.*)
let patternOctets = pattern.components(separatedBy: ".")
let ipOctets = ipString.components(separatedBy: ".")
@@ -186,10 +226,69 @@ struct ProxyRule: Codable {
class AppProxyProvider: NETransparentProxyProvider {
- private var logQueue: [[String: String]] = []
+ // Circular buffer for log queue - avoids O(n) removeFirst() on array
+ private static let logCapacity = 1000
+ private var logBuffer: [[String: String]] = Array(repeating: [:], count: AppProxyProvider.logCapacity)
+ private var logHead = 0 // next read position
+ private var logTail = 0 // next write position
+ private var logCount = 0
private let logQueueLock = NSLock()
+ private let dateFormatter: ISO8601DateFormatter = ISO8601DateFormatter()
+
+ // cache results so each process only instead of per-connection.
+ private var pidCache: [pid_t: String] = [:]
+ private let pidCacheLock = NSLock()
+ private static let pidCacheMaxSize = 256
- private var trafficLoggingEnabled = true
+ private func getProcessName(from metaData: NEFlowMetaData) -> String? {
+ guard let auditTokenData = metaData.sourceAppAuditToken else {
+ return nil
+ }
+ guard auditTokenData.count == MemoryLayout.size else {
+ return nil
+ }
+
+ let pid = auditTokenData.withUnsafeBytes { ptr -> pid_t in
+ guard let baseAddress = ptr.baseAddress else { return 0 }
+ let token = baseAddress.assumingMemoryBound(to: UInt32.self)
+ return pid_t(token[5])
+ }
+
+ guard pid > 0 else { return nil }
+
+ // Check cache first
+ pidCacheLock.lock()
+ if let cached = pidCache[pid] {
+ pidCacheLock.unlock()
+ return cached
+ }
+ pidCacheLock.unlock()
+
+ // Cache miss - call proc_pidpath()
+ var pathBuffer = [Int8](repeating: 0, count: Int(MAXPATHLEN))
+ guard proc_pidpath(pid, &pathBuffer, UInt32(MAXPATHLEN)) > 0 else {
+ return nil
+ }
+
+ let fullPath = String(cString: pathBuffer)
+ let processName = (fullPath as NSString).lastPathComponent
+
+ // Store in cache (evict all if too large - processes rarely exceed this)
+ pidCacheLock.lock()
+ if pidCache.count >= AppProxyProvider.pidCacheMaxSize {
+ pidCache.removeAll(keepingCapacity: true)
+ }
+ pidCache[pid] = processName
+ pidCacheLock.unlock()
+
+ return processName
+ }
+
+ private var _trafficLoggingEnabled: Int32 = 1 // atomic: 1=enabled, 0=disabled
+ private var trafficLoggingEnabled: Bool {
+ get { return OSAtomicAdd32(0, &_trafficLoggingEnabled) != 0 }
+ set { OSAtomicCompareAndSwap32(newValue ? 0 : 1, newValue ? 1 : 0, &_trafficLoggingEnabled) }
+ }
private var rules: [ProxyRule] = []
private let rulesLock = NSLock()
@@ -203,17 +302,19 @@ class AppProxyProvider: NETransparentProxyProvider {
private let proxyLock = NSLock()
private func log(_ message: String, level: String = "INFO") {
- let timestamp = ISO8601DateFormatter().string(from: Date())
let logEntry: [String: String] = [
- "timestamp": timestamp,
+ "timestamp": dateFormatter.string(from: Date()),
"level": level,
"message": message
]
-
logQueueLock.lock()
- logQueue.append(logEntry)
- if logQueue.count > 1000 {
- logQueue.removeFirst()
+ logBuffer[logTail] = logEntry
+ logTail = (logTail + 1) % AppProxyProvider.logCapacity
+ if logCount < AppProxyProvider.logCapacity {
+ logCount += 1
+ } else {
+ // Buffer full: advance head to overwrite oldest entry
+ logHead = (logHead + 1) % AppProxyProvider.logCapacity
}
logQueueLock.unlock()
}
@@ -254,9 +355,15 @@ class AppProxyProvider: NETransparentProxyProvider {
switch action {
case "getLogs":
logQueueLock.lock()
- if !logQueue.isEmpty {
- let logsToSend = Array(logQueue.prefix(min(100, logQueue.count)))
- logQueue.removeFirst(logsToSend.count)
+ if logCount > 0 {
+ let batchSize = min(100, logCount)
+ var logsToSend: [[String: String]] = []
+ logsToSend.reserveCapacity(batchSize)
+ for _ in 0.. Bool {
+ let metaData = flow.metaData
+ let processPath = metaData.sourceAppSigningIdentifier
+
+ // early exit for own app traffic before any other work
+ if processPath == "com.interceptsuite.ProxyBridge" || processPath == "com.interceptsuite.ProxyBridge.extension" {
+ return false
+ }
+
let remoteEndpoint = flow.remoteEndpoint
var destination = ""
var portNum: UInt16 = 0
@@ -447,30 +562,24 @@ class AppProxyProvider: NETransparentProxyProvider {
portStr = "unknown"
}
- var processPath = "unknown"
- if let metaData = flow.metaData as? NEFlowMetaData {
- processPath = metaData.sourceAppSigningIdentifier
- }
-
- if processPath == "com.interceptsuite.ProxyBridge" || processPath == "com.interceptsuite.ProxyBridge.extension" {
- return false
- }
+ let processName = getProcessName(from: metaData)
+ let displayName = processName ?? processPath
proxyLock.lock()
let hasProxyConfig = (proxyHost != nil && proxyPort != nil)
proxyLock.unlock()
if !hasProxyConfig {
- sendLogToApp(protocol: "TCP", process: processPath, destination: destination, port: portStr, proxy: "Direct")
+ sendLogToApp(protocol: "TCP", process: displayName, destination: destination, port: portStr, proxy: "Direct")
return false
}
- let matchedRule = findMatchingRule(processPath: processPath, destination: destination, port: portNum, connectionProtocol: .tcp, checkIpPort: true)
+ let matchedRule = findMatchingRule(bundleId: processPath, processName: processName, destination: destination, port: portNum, connectionProtocol: .tcp, checkIpPort: true)
if let rule = matchedRule {
let action = rule.action.rawValue
- sendLogToApp(protocol: "TCP", process: processPath, destination: destination, port: portStr, proxy: action)
+ sendLogToApp(protocol: "TCP", process: displayName, destination: destination, port: portStr, proxy: action)
switch rule.action {
case .direct:
@@ -484,17 +593,21 @@ class AppProxyProvider: NETransparentProxyProvider {
return true
}
} else {
- sendLogToApp(protocol: "TCP", process: processPath, destination: destination, port: portStr, proxy: "Direct")
+ sendLogToApp(protocol: "TCP", process: displayName, destination: destination, port: portStr, proxy: "Direct")
return false
}
}
private func handleUDPFlow(_ flow: NEAppProxyUDPFlow) -> Bool {
var processPath = "unknown"
+ var processName: String?
if let metaData = flow.metaData as? NEFlowMetaData {
processPath = metaData.sourceAppSigningIdentifier
+ processName = getProcessName(from: metaData)
}
+ let displayName = processName ?? processPath
+
if processPath == "com.interceptsuite.ProxyBridge" || processPath == "com.interceptsuite.ProxyBridge.extension" {
return false
}
@@ -509,16 +622,15 @@ class AppProxyProvider: NETransparentProxyProvider {
return false
}
- let matchedRule = findMatchingRule(processPath: processPath, destination: "", port: 0, connectionProtocol: .udp, checkIpPort: false)
+ let matchedRule = findMatchingRule(bundleId: processPath, processName: processName, destination: "", port: 0, connectionProtocol: .udp, checkIpPort: false)
if let rule = matchedRule {
- // We don't have access to UDP dest ip and port when os handles it in (apple proxy API limitation), we log with unknown ip and port to know specific package is using UDP
switch rule.action {
case .direct:
- sendLogToApp(protocol: "UDP", process: processPath, destination: "unknown", port: "unknown", proxy: "Direct")
+ sendLogToApp(protocol: "UDP", process: displayName, destination: "unknown", port: "unknown", proxy: "Direct")
return false
case .block:
- sendLogToApp(protocol: "UDP", process: processPath, destination: "unknown", port: "unknown", proxy: "BLOCK")
+ sendLogToApp(protocol: "UDP", process: displayName, destination: "unknown", port: "unknown", proxy: "BLOCK")
return true
case .proxy:
flow.open(withLocalEndpoint: nil) { [weak self] error in
@@ -536,8 +648,7 @@ class AppProxyProvider: NETransparentProxyProvider {
return true
}
} else {
- // No rule matched let OS handle it, but log it so user knows this process is using UDP
- sendLogToApp(protocol: "UDP", process: processPath, destination: "unknown", port: "unknown", proxy: "Direct")
+ sendLogToApp(protocol: "UDP", process: displayName, destination: "unknown", port: "unknown", proxy: "Direct")
return false
}
}
@@ -1097,7 +1208,11 @@ class AppProxyProvider: NETransparentProxyProvider {
private func relayClientToProxy(clientFlow: NEAppProxyTCPFlow, proxyConnection: NWTCPConnection) {
clientFlow.readData { [weak self] data, error in
if let error = error {
- self?.log("Client read error: \(error.localizedDescription)", level: "ERROR")
+ // ignore expected errors
+ let code = (error as NSError).code
+ if code != 57 && code != 54 && code != 89 {
+ self?.log("Client read error: \(error.localizedDescription)", level: "ERROR")
+ }
proxyConnection.cancel()
return
}
@@ -1123,9 +1238,12 @@ class AppProxyProvider: NETransparentProxyProvider {
private func relayProxyToClient(clientFlow: NEAppProxyTCPFlow, proxyConnection: NWTCPConnection) {
proxyConnection.readMinimumLength(1, maximumLength: 65536) { [weak self] data, error in
if let error = error {
- self?.log("Proxy read error: \(error.localizedDescription)", level: "ERROR")
- clientFlow.closeReadWithError(error)
- clientFlow.closeWriteWithError(error)
+ let code = (error as NSError).code
+ if code != 57 && code != 54 && code != 89 {
+ self?.log("Proxy read error: \(error.localizedDescription)", level: "ERROR")
+ }
+ clientFlow.closeReadWithError(nil)
+ clientFlow.closeWriteWithError(nil)
return
}
@@ -1150,32 +1268,65 @@ class AppProxyProvider: NETransparentProxyProvider {
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
}
- private func findMatchingRule(processPath: String, destination: String, port: UInt16, connectionProtocol: RuleProtocol, checkIpPort: Bool) -> ProxyRule? {
+ private func findMatchingRule(bundleId: String, processName: String?, destination: String, port: UInt16, connectionProtocol: RuleProtocol, checkIpPort: Bool) -> ProxyRule? {
rulesLock.lock()
- defer { rulesLock.unlock() }
+ let currentRules = rules
+ rulesLock.unlock()
+
+ var wildcardRule: ProxyRule? = nil
- for rule in rules {
+ for rule in currentRules {
guard rule.enabled else { continue }
+ // check protocol firstß
if rule.ruleProtocol != .both && rule.ruleProtocol != connectionProtocol {
continue
}
- if !rule.matchesProcess(processPath) {
- continue
- }
+ // if this is a wildcard process rule
+ let isWildcardProcess = (rule.processNames == "*" || rule.processNames.isEmpty)
- if checkIpPort {
- if !rule.matchesIP(destination) {
+ if isWildcardProcess {
+ // wildcard has specific filters
+ let hasIpFilter = (rule.targetHosts != "*" && !rule.targetHosts.isEmpty)
+ let hasPortFilter = (rule.targetPorts != "*" && !rule.targetPorts.isEmpty)
+
+ if hasIpFilter || hasPortFilter {
+ // wildcard with filters - check immediately
+ if checkIpPort {
+ if rule.matchesIP(destination) && rule.matchesPort(port) {
+ return rule
+ }
+ } else {
+ // for UDP without destination info, skip filtered wildcards
+ }
continue
}
- if !rule.matchesPort(port) {
- continue
+ // Fully wildcard rule - defer it (save first one only)
+ if wildcardRule == nil {
+ wildcardRule = rule
}
+ continue
}
- return rule
+ // Specific process rule - check if it matches
+ if rule.matchesProcess(bundleId: bundleId, processName: processName) {
+ // Process matched! Check IP and port filters
+ if checkIpPort {
+ if rule.matchesIP(destination) && rule.matchesPort(port) {
+ return rule
+ }
+ } else {
+ // For UDP without destination info, match on process only
+ return rule
+ }
+ }
+ }
+
+ // No specific rule matched, use deferred wildcard if available
+ if let wildcardRule = wildcardRule {
+ return wildcardRule
}
return nil
@@ -1194,9 +1345,12 @@ class AppProxyProvider: NETransparentProxyProvider {
]
logQueueLock.lock()
- logQueue.append(logData)
- if logQueue.count > 1000 {
- logQueue.removeFirst()
+ logBuffer[logTail] = logData
+ logTail = (logTail + 1) % AppProxyProvider.logCapacity
+ if logCount < AppProxyProvider.logCapacity {
+ logCount += 1
+ } else {
+ logHead = (logHead + 1) % AppProxyProvider.logCapacity
}
logQueueLock.unlock()
}
diff --git a/MacOS/ProxyBridge/proxybridge-app.xcconfig b/MacOS/ProxyBridge/proxybridge-app.xcconfig
index 5d88cfe..7064149 100644
--- a/MacOS/ProxyBridge/proxybridge-app.xcconfig
+++ b/MacOS/ProxyBridge/proxybridge-app.xcconfig
@@ -24,3 +24,7 @@ PROVISIONING_PROFILE_SPECIFIER = ProxyBridge Prod
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO
ENABLE_HARDENED_RUNTIME = YES
+// MARK: - Architecture (Universal Binary: Intel + Apple Silicon)
+ARCHS = arm64 x86_64
+ONLY_ACTIVE_ARCH = NO
+
diff --git a/MacOS/ProxyBridge/proxybridge-ext.xcconfig b/MacOS/ProxyBridge/proxybridge-ext.xcconfig
index f552f14..2c46ec9 100644
--- a/MacOS/ProxyBridge/proxybridge-ext.xcconfig
+++ b/MacOS/ProxyBridge/proxybridge-ext.xcconfig
@@ -23,3 +23,7 @@ PROVISIONING_PROFILE_SPECIFIER = ProxyBridge Extension Prod
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO
ENABLE_HARDENED_RUNTIME = YES
+// MARK: - Architecture (Universal Binary: Intel + Apple Silicon)
+ARCHS = arm64 x86_64
+ONLY_ACTIVE_ARCH = NO
+
diff --git a/README.md b/README.md
index a5e03ed..cdea538 100644
--- a/README.md
+++ b/README.md
@@ -4,9 +4,10 @@
-ProxyBridge is a lightweight, open-source universal proxy client (Proxifier alternative) that provides transparent proxy routing for applications on **Windows** and **macOS**. It redirects TCP and UDP traffic from specific processes through SOCKS5 or HTTP proxies, with the ability to route, block, or allow traffic on a per-application basis. ProxyBridge fully supports both TCP and UDP proxy routing and works at the system level, making it compatible with proxy-unaware applications without requiring any configuration changes.
+ProxyBridge is a lightweight, open-source universal proxy client (Proxifier alternative) that provides transparent proxy routing for applications on **Windows**, **macOS**, and **Linux**. It redirects TCP and UDP traffic from specific processes through SOCKS5 or HTTP proxies, with the ability to route, block, or allow traffic on a per-application basis. ProxyBridge fully supports both TCP and UDP proxy routing and works at the system level, making it compatible with proxy-unaware applications without requiring any configuration changes.
-🚀 **Need advanced traffic analysis?** Check out [**InterceptSuite**](https://github.com/InterceptSuite/InterceptSuite) - our comprehensive MITM proxy for analyzing TLS, TCP, UDP, DTLS traffic. Perfect for security testing, network debugging, and system administration!
+> [!TIP]
+> **Need advanced traffic analysis?** Check out [**InterceptSuite**](https://github.com/InterceptSuite/InterceptSuite) - our comprehensive MITM proxy for analyzing TLS, TCP, UDP, DTLS traffic. Perfect for security testing, network debugging, and system administration!
## Table of Contents
@@ -28,7 +29,7 @@ ProxyBridge is a lightweight, open-source universal proxy client (Proxifier alte
## Features
-- **Cross-platform** - Available for Windows and macOS
+- **Cross-platform** - Available for Windows, macOS and Linux
- **Dual interface** - Feature-rich GUI and powerful CLI for all use cases
- **Process-based traffic control** - Route, block, or allow traffic for specific applications
- **Universal compatibility** - Works with proxy-unaware applications
@@ -54,14 +55,14 @@ ProxyBridge is a lightweight, open-source universal proxy client (Proxifier alte
> - Official Website: [https://interceptsuite.com/download/proxybridge](https://interceptsuite.com/download/proxybridge)
>
> If you prefer not to use prebuilt binaries, you may safely build ProxyBridge yourself by following the **Contribution Guide** and compiling directly from the **official source code**.
->
+>
> ProxyBridge does not communicate with any external servers except the GitHub API for update checks (triggered only on app launch or manual update checks);
## Platform Documentation
-ProxyBridge is available for both Windows and macOS, with platform-specific implementations:
+ProxyBridge is available for Windows, macOS, and Linux, with platform-specific implementations:
### 📘 Windows
- **[View Full Windows Documentation](Windows/README.md)**
@@ -78,6 +79,15 @@ ProxyBridge is available for both Windows and macOS, with platform-specific impl
- **Requirements**: macOS 13.0 (Ventura) or later, Apple Silicon (ARM) or Intel
- **GUI**: Native SwiftUI interface
+### 📙 Linux
+- **[View Full Linux Documentation](Linux/README.md)**
+- **Technology**: Netfilter NFQUEUE for kernel-level packet interception
+- **Distribution**: TAR.GZ archive or one-command install from [Releases](https://github.com/InterceptSuite/ProxyBridge/releases)
+- **Requirements**: Linux kernel with NFQUEUE support, root privileges (not compatible with WSL1/WSL2)
+- **GUI**: GTK3-based interface (optional)
+- **CLI**: Full-featured command-line tool with rule support
+- **Quick Install**: `curl -Lo deploy.sh https://raw.githubusercontent.com/InterceptSuite/ProxyBridge/refs/heads/master/Linux/deploy.sh && sudo bash deploy.sh`
+
## Screenshots
### macOS
@@ -142,6 +152,42 @@ ProxyBridge is available for both Windows and macOS, with platform-specific impl
ProxyBridge CLI Interface
+### Linux
+
+#### GUI
+
+
+
+
+ ProxyBridge GUI - Main Interface
+
+
+
+
+
+ Proxy Settings Configuration
+
+
+
+
+
+ Proxy Rules Management
+
+
+
+
+
+ Add/Edit Proxy Rule
+
+
+#### CLI
+
+
+
+
+ ProxyBridge CLI Interface
+
+
## Use Cases
- Redirect proxy-unaware applications (games, desktop apps) through InterceptSuite/Burp Suite for security testing
@@ -170,3 +216,6 @@ The Windows GUI is built using [Avalonia UI](https://avaloniaui.net/) - a cross-
**macOS Implementation:**
Built using Apple's Network Extension framework for transparent proxy capabilities on macOS.
+
+**Linux Implementation:**
+Built using Linux Netfilter NFQUEUE for kernel-level packet interception and iptables for traffic redirection. The GUI uses GTK3 for native Linux desktop integration.
diff --git a/Windows/README.md b/Windows/README.md
index 6ae4ed4..60a2ebe 100644
--- a/Windows/README.md
+++ b/Windows/README.md
@@ -209,7 +209,7 @@ ProxyBridge_CLI.exe -h
| |_) | '__/ _ \ \/ / | | | | _ \| '__| |/ _` |/ _` |/ _ \
| __/| | | (_) > <| |_| | | |_) | | | | (_| | (_| | __/
|_| |_| \___/_/\_\\__, | |____/|_| |_|\__,_|\__, |\___|
- |___/ |___/ V3.1.0
+ |___/ |___/ V3.2.0
Universal proxy client for Windows applications
@@ -223,21 +223,22 @@ Usage:
ProxyBridge_CLI [command] [options]
Options:
- --proxy Proxy server URL with optional authentication
- Format: type://ip:port or type://ip:port:username:password
- Examples: socks5://127.0.0.1:1080
- http://proxy.com:8080:myuser:mypass [default: socks5://127.0.0.1:4444]
- --rule Traffic routing rule (multiple values supported, can repeat)
- Format: process:hosts:ports:protocol:action
- process - Process name(s): chrome.exe, chr*.exe, *.exe, or *
- hosts - IP/host(s): *, google.com, 192.168.*.*, or multiple comma-separated
- ports - Port(s): *, 443, 80,443, 80-100, or multiple comma-separated
- protocol - TCP, UDP, or BOTH
- action - PROXY, DIRECT, or BLOCK
- Examples:
- chrome.exe:*:*:TCP:PROXY
- *:*:53:UDP:PROXY
- firefox.exe:*:80,443:TCP:DIRECT
+ --proxy Proxy server URL with optional authentication
+ Format: type://ip:port or type://ip:port:username:password
+ Examples: socks5://127.0.0.1:1080
+ http://proxy.com:8080:myuser:mypass [default: socks5://127.0.0.1:4444]
+ --rule Traffic routing rule (multiple values supported, can repeat)
+ Format: process:hosts:ports:protocol:action
+ process - Process name(s): chrome.exe, chr*.exe, *.exe, or * (use ; for multiple: chrome.exe;firefox.exe)
+ hosts - IP/host(s): *, google.com, 192.168.*.*, or multiple separated by ; or ,
+ ports - Port(s): *, 443, 80;8080, 80-100, or multiple separated by ; or ,
+ protocol - TCP, UDP, or BOTH
+ action - PROXY, DIRECT, or BLOCK
+ Examples:
+ chrome.exe:*:*:TCP:PROXY
+ chrome.exe;firefox.exe:*:*:TCP:PROXY
+ *:*:53:UDP:PROXY
+ firefox.exe:*:80;443:TCP:DIRECT
--rule-file Path to JSON file containing proxy rules
JSON format (same as GUI export):
[{
@@ -249,14 +250,16 @@ Options:
"enabled": true
}]
Example: --rule-file C:\\rules.json []
- --dns-via-proxy Route DNS queries through proxy (default: true) [default: True]
- --verbose Logging verbosity level
- 0 - No logs (default)
- 1 - Show log messages only
- 2 - Show connection events only
- 3 - Show both logs and connections [default: 0]
- --version Show version information
- -?, -h, --help Show help and usage information
+ --dns-via-proxy Route DNS queries through proxy (default: true) [default: True]
+ --localhost-via-proxy Route localhost traffic through proxy (default: false, most proxies block localhost for SSRF prevention, local traffic to remote proxy will cause issues)
+ [default: False]
+ --verbose Logging verbosity level
+ 0 - No logs (default)
+ 1 - Show log messages only
+ 2 - Show connection events only
+ 3 - Show both logs and connections [default: 0]
+ --version Show version information
+ -?, -h, --help Show help and usage information
Commands:
--update Check for updates and download latest version from GitHub
@@ -314,14 +317,42 @@ Commands:
- **DNS Traffic Handling**: DNS traffic on TCP and UDP port 53 is handled separately from proxy rules. Even if you configure rules for port 53, they will be ignored. Instead, DNS routing is controlled by the **DNS via Proxy** option in the Proxy menu (enabled by default). When enabled, all DNS queries are routed through the proxy; when disabled, DNS queries use direct connection.
+- **Localhost Traffic Handling**: Localhost traffic (127.0.0.0/8) requires special handling and is controlled by the **Localhost via Proxy** option in the Proxy menu (disabled by default):
+
+ **Default Behavior (Localhost via Proxy = Disabled):**
+ - ALL localhost traffic automatically uses direct connection
+ - Proxy rules matching 127.x.x.x addresses are automatically overridden to DIRECT
+ - This is the recommended setting for most users
+
+ **Why localhost should stay local:**
+ - **Security**: Most proxy servers reject localhost traffic to prevent SSRF (Server-Side Request Forgery) attacks
+ - **Compatibility**: Many applications run local services that must stay on your machine:
+ - NVIDIA GeForce Experience (local API servers)
+ - Chrome/Edge DevTools (127.0.0.1:9222 debugging protocol)
+ - Development servers (localhost web/database servers)
+ - Inter-process communication (IPC) using TCP/UDP on 127.0.0.1
+ - **Routing Issues**: When localhost traffic goes to a remote proxy:
+ - The proxy server cannot reach services running on YOUR machine
+ - Applications expecting local responses will timeout or fail
+ - Example: `curl http://127.0.0.1:8080` via remote proxy asks the proxy's localhost, not yours
+
+ **When to Enable Localhost via Proxy:**
+ - ✅ Proxy server is running on the same machine (127.0.0.1:1080)
+ - ✅ Security testing: Intercepting localhost traffic in Burp Suite/InterceptSuite
+ - ✅ Your proxy is configured to handle localhost requests properly
+ - ❌ Do NOT enable if proxy is on a different machine/IP address
+
+ **CLI/GUI Options:**
+ - GUI: **Proxy** menu → **Localhost via Proxy** (checkbox)
+ - CLI: `--localhost-via-proxy` flag
+
- **Automatic Direct Routing**: Certain IP addresses and ports automatically use direct connection regardless of proxy rules, though you can still create rules with **DIRECT** (default) or **BLOCK** actions for them:
- - **Localhost addresses** (127.0.0.0/8) - Loopback traffic
- **Broadcast addresses** (255.255.255.255 and x.x.x.255) - Network broadcast
- **Multicast addresses** (224.0.0.0 - 239.255.255.255) - Group communication
- **APIPA addresses** (169.254.0.0/16) - Automatic Private IP Addressing (link-local)
- **DHCP ports** (UDP 67, 68) - Dynamic Host Configuration Protocol
- These addresses and ports are used by system components, network discovery, and essential Windows services. While proxy rules are automatically overridden to DIRECT for these targets, you can still define rules with DIRECT or BLOCK actions to explicitly control or block this traffic. Note that Windows loopback traffic (127.x.x.x) uses a method that bypasses the network interface card (NIC), which currently doesn't support proxy routing due to technical limitations with WinDivert at the network layer.
+ These addresses and ports are used by system components, network discovery, and essential Windows services. While proxy rules are automatically overridden to DIRECT for these targets, you can still define rules with DIRECT or BLOCK actions to explicitly control or block this traffic.
- **UDP Proxy Requirements**: UDP traffic only works when a SOCKS5 proxy is configured. If an HTTP proxy server is configured, ProxyBridge will ignore UDP proxy rules and route UDP traffic as direct connection instead. This limitation does not affect UDP rules with BLOCK or DIRECT actions.
diff --git a/Windows/cli/Program.cs b/Windows/cli/Program.cs
index 733a3d4..e9fba4d 100644
--- a/Windows/cli/Program.cs
+++ b/Windows/cli/Program.cs
@@ -108,6 +108,11 @@ static async Task Main(string[] args)
description: "Route DNS queries through proxy (default: true)",
getDefaultValue: () => true);
+ var localhostViaProxyOption = new Option(
+ name: "--localhost-via-proxy",
+ description: "Route localhost traffic through proxy (default: false, most proxies block localhost for SSRF prevention, local traffic to remote proxy will cause issues)",
+ getDefaultValue: () => false);
+
var verboseOption = new Option(
name: "--verbose",
description: "Logging verbosity level\n" +
@@ -125,6 +130,7 @@ static async Task Main(string[] args)
ruleOption,
ruleFileOption,
dnsViaProxyOption,
+ localhostViaProxyOption,
verboseOption
};
@@ -135,10 +141,10 @@ static async Task Main(string[] args)
await CheckAndUpdate();
});
- rootCommand.SetHandler(async (proxyUrl, rules, ruleFile, dnsViaProxy, verbose) =>
+ rootCommand.SetHandler(async (proxyUrl, rules, ruleFile, dnsViaProxy, localhostViaProxy, verbose) =>
{
- await RunProxyBridge(proxyUrl, rules, ruleFile, dnsViaProxy, verbose);
- }, proxyOption, ruleOption, ruleFileOption, dnsViaProxyOption, verboseOption);
+ await RunProxyBridge(proxyUrl, rules, ruleFile, dnsViaProxy, localhostViaProxy, verbose);
+ }, proxyOption, ruleOption, ruleFileOption, dnsViaProxyOption, localhostViaProxyOption, verboseOption);
if (args.Contains("--help") || args.Contains("-h") || args.Contains("-?"))
{
@@ -148,7 +154,7 @@ static async Task Main(string[] args)
return await rootCommand.InvokeAsync(args);
}
- private static async Task RunProxyBridge(string proxyUrl, string[] rules, string? ruleFile, bool dnsViaProxy, int verboseLevel)
+ private static async Task RunProxyBridge(string proxyUrl, string[] rules, string? ruleFile, bool dnsViaProxy, bool localhostViaProxy, int verboseLevel)
{
_verboseLevel = verboseLevel;
ShowBanner();
@@ -201,6 +207,7 @@ private static async Task RunProxyBridge(string proxyUrl, string[] rules, s
Console.WriteLine($"Proxy Auth: {proxyInfo.Username}:***");
}
Console.WriteLine($"DNS via Proxy: {(dnsViaProxy ? "Enabled" : "Disabled")}");
+ Console.WriteLine($"Localhost via Proxy: {(localhostViaProxy ? "Enabled" : "Disabled (Security: most proxies block localhost)")}");
if (!ProxyBridgeNative.ProxyBridge_SetProxyConfig(
proxyInfo.Type,
@@ -214,6 +221,7 @@ private static async Task RunProxyBridge(string proxyUrl, string[] rules, s
}
ProxyBridgeNative.ProxyBridge_SetDnsViaProxy(dnsViaProxy);
+ ProxyBridgeNative.ProxyBridge_SetLocalhostViaProxy(localhostViaProxy);
if (parsedRules.Count > 0)
{
@@ -421,7 +429,7 @@ private static void ShowBanner()
Console.WriteLine(" | |_) | '__/ _ \\ \\/ / | | | | _ \\| '__| |/ _` |/ _` |/ _ \\");
Console.WriteLine(" | __/| | | (_) > <| |_| | | |_) | | | | (_| | (_| | __/");
Console.WriteLine(" |_| |_| \\___/_/\\_\\\\__, | |____/|_| |_|\\__,_|\\__, |\\___|");
- var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "3.1.0";
+ var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "3.2.0";
Console.WriteLine($" |___/ |___/ V{version}");
Console.WriteLine();
Console.WriteLine(" Universal proxy client for Windows applications");
@@ -486,12 +494,8 @@ private static async Task CheckAndUpdate()
return;
}
- Console.ForegroundColor = ConsoleColor.Yellow;
- Console.WriteLine($"⚠ New version available: {releaseName}");
- Console.ResetColor();
- Console.WriteLine();
-
-
+ // Check if Windows installer exists before showing update available
+ // (handles cross-platform releases where v4.0 might be released for Linux only)
var assets = root.GetProperty("assets").EnumerateArray();
string? setupUrl = null;
string? setupName = null;
@@ -511,13 +515,21 @@ private static async Task CheckAndUpdate()
if (string.IsNullOrEmpty(setupUrl))
{
- Console.ForegroundColor = ConsoleColor.Red;
- Console.WriteLine("ERROR: Setup installer not found in latest release.");
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"ℹ Version {latestVersionStr} exists but Windows installer not yet available.");
+ Console.WriteLine($" (Release might be for other platforms only)");
Console.ResetColor();
+ Console.WriteLine();
+ Console.WriteLine("You are using the latest version available for Windows.");
Console.WriteLine($"Visit: https://github.com/{repoOwner}/{repoName}/releases/latest");
return;
}
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine($"⚠ New version available: {releaseName}");
+ Console.ResetColor();
+ Console.WriteLine();
+
Console.WriteLine($"Downloading: {setupName}");
Console.WriteLine($"From: {setupUrl}");
Console.WriteLine();
diff --git a/Windows/cli/ProxyBridge.CLI.csproj b/Windows/cli/ProxyBridge.CLI.csproj
index 4502e24..8b63b01 100644
--- a/Windows/cli/ProxyBridge.CLI.csproj
+++ b/Windows/cli/ProxyBridge.CLI.csproj
@@ -6,7 +6,7 @@
enable
enable
ProxyBridge_CLI
- 3.1.0
+ 3.2.0
true
false
true
diff --git a/Windows/cli/ProxyBridgeNative.cs b/Windows/cli/ProxyBridgeNative.cs
index fc29a02..071ffd1 100644
--- a/Windows/cli/ProxyBridgeNative.cs
+++ b/Windows/cli/ProxyBridgeNative.cs
@@ -87,6 +87,9 @@ public static extern bool ProxyBridge_SetProxyConfig(
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern void ProxyBridge_SetDnsViaProxy([MarshalAs(UnmanagedType.Bool)] bool enable);
+ [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void ProxyBridge_SetLocalhostViaProxy([MarshalAs(UnmanagedType.Bool)] bool enable);
+
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ProxyBridge_Start();
diff --git a/Windows/compile.ps1 b/Windows/compile.ps1
index ec3cfd4..6bab012 100644
--- a/Windows/compile.ps1
+++ b/Windows/compile.ps1
@@ -296,7 +296,7 @@ if ($success) {
Pop-Location
if ($LASTEXITCODE -eq 0) {
Write-Host " Installer created successfully" -ForegroundColor Green
- $installerName = "ProxyBridge-Setup-3.1.0.exe"
+ $installerName = "ProxyBridge-Setup-3.2.0.exe"
if (Test-Path "installer\$installerName") {
Move-Item "installer\$installerName" -Destination $OutputDir -Force
Write-Host " Moved: $installerName -> $OutputDir\" -ForegroundColor Gray
diff --git a/Windows/gui/Interop/ProxyBridgeNative.cs b/Windows/gui/Interop/ProxyBridgeNative.cs
index 4c1c514..b9ba05f 100644
--- a/Windows/gui/Interop/ProxyBridgeNative.cs
+++ b/Windows/gui/Interop/ProxyBridgeNative.cs
@@ -83,6 +83,13 @@ public static extern bool ProxyBridge_EditRule(
RuleProtocol protocol,
RuleAction action);
+ [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
+ public static extern uint ProxyBridge_GetRulePosition(uint ruleId);
+
+ [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool ProxyBridge_MoveRuleToPosition(uint ruleId, uint newPosition);
+
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ProxyBridge_SetProxyConfig(
@@ -104,6 +111,9 @@ public static extern bool ProxyBridge_SetProxyConfig(
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern void ProxyBridge_SetDnsViaProxy([MarshalAs(UnmanagedType.Bool)] bool enable);
+ [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
+ public static extern void ProxyBridge_SetLocalhostViaProxy([MarshalAs(UnmanagedType.Bool)] bool enable);
+
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ProxyBridge_Start();
diff --git a/Windows/gui/ProxyBridge.GUI.csproj b/Windows/gui/ProxyBridge.GUI.csproj
index ace47c5..177c950 100644
--- a/Windows/gui/ProxyBridge.GUI.csproj
+++ b/Windows/gui/ProxyBridge.GUI.csproj
@@ -7,9 +7,9 @@
app.manifest
ProxyBridge
ProxyBridge.GUI
- 3.1.0
- 3.1.0
- 3.1.0
+ 3.2.0
+ 3.2.0
+ 3.2.0
Assets\logo.ico
InterceptSuite
ProxyBridge
diff --git a/Windows/gui/Resources/Resources.Designer.cs b/Windows/gui/Resources/Resources.Designer.cs
index 95d49cb..f428fd7 100644
--- a/Windows/gui/Resources/Resources.Designer.cs
+++ b/Windows/gui/Resources/Resources.Designer.cs
@@ -92,6 +92,12 @@ internal static string MenuDnsViaProxy {
}
}
+ internal static string MenuLocalhostViaProxy {
+ get {
+ return ResourceManager.GetString("MenuLocalhostViaProxy", resourceCulture);
+ }
+ }
+
internal static string MenuEnableTrafficLogging {
get {
return ResourceManager.GetString("MenuEnableTrafficLogging", resourceCulture);
diff --git a/Windows/gui/Resources/Resources.resx b/Windows/gui/Resources/Resources.resx
index 5fa3e8e..3615a04 100644
--- a/Windows/gui/Resources/Resources.resx
+++ b/Windows/gui/Resources/Resources.resx
@@ -26,6 +26,9 @@
DNS via Proxy
+
+ Localhost via Proxy
+
Enable Traffic Logging
@@ -228,7 +231,7 @@
Example: iexplore.exe; "C:\some app.exe"; fire*.exe; *.bin
- Example: 127.0.0.1; *.example.com; 192.168.1.*; 10.1.0.0-10.5.255.255
+ Example: 127.0.0.1; 192.168.1.*; 10.1.0.0-10.5.255.255
Example: 80; 8000-9000; 3128
diff --git a/Windows/gui/Resources/Resources.zh.resx b/Windows/gui/Resources/Resources.zh.resx
index 173fc3c..8551641 100644
--- a/Windows/gui/Resources/Resources.zh.resx
+++ b/Windows/gui/Resources/Resources.zh.resx
@@ -26,6 +26,9 @@
DNS通过代理
+
+ 本地主机通过代理
+
启用流量日志
@@ -229,7 +232,7 @@
示例: iexplore.exe; "C:\某个应用.exe"; fire*.exe; *.bin
- 示例: 127.0.0.1; *.example.com; 192.168.1.*; 10.1.0.0-10.5.255.255
+ 示例: 127.0.0.1; 192.168.1.*; 10.1.0.0-10.5.255.255
示例: 80; 8000-9000; 3128
diff --git a/Windows/gui/Services/ConfigManager.cs b/Windows/gui/Services/ConfigManager.cs
index b1b5055..e1d5acc 100644
--- a/Windows/gui/Services/ConfigManager.cs
+++ b/Windows/gui/Services/ConfigManager.cs
@@ -15,6 +15,7 @@ public class AppConfig
public string ProxyUsername { get; set; } = "";
public string ProxyPassword { get; set; } = "";
public bool DnsViaProxy { get; set; } = true;
+ public bool LocalhostViaProxy { get; set; } = false; // Default: disabled
public bool IsTrafficLoggingEnabled { get; set; } = true;
public string Language { get; set; } = "en";
public bool CloseToTray { get; set; } = true;
diff --git a/Windows/gui/Services/Loc.cs b/Windows/gui/Services/Loc.cs
index 43854fc..e2508a1 100644
--- a/Windows/gui/Services/Loc.cs
+++ b/Windows/gui/Services/Loc.cs
@@ -41,6 +41,7 @@ private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
public string MenuProxySettings => Resources.Resources.MenuProxySettings;
public string MenuProxyRules => Resources.Resources.MenuProxyRules;
public string MenuDnsViaProxy => Resources.Resources.MenuDnsViaProxy;
+ public string MenuLocalhostViaProxy => Resources.Resources.MenuLocalhostViaProxy;
public string MenuEnableTrafficLogging => Resources.Resources.MenuEnableTrafficLogging;
public string MenuSettings => Resources.Resources.MenuSettings;
public string MenuCloseToTray => Resources.Resources.MenuCloseToTray;
diff --git a/Windows/gui/Services/ProxyBridgeService.cs b/Windows/gui/Services/ProxyBridgeService.cs
index 6ddd5cc..fa34ce5 100644
--- a/Windows/gui/Services/ProxyBridgeService.cs
+++ b/Windows/gui/Services/ProxyBridgeService.cs
@@ -113,11 +113,26 @@ public bool EditRule(uint ruleId, string processName, string targetHosts, string
return ProxyBridgeNative.ProxyBridge_EditRule(ruleId, processName, targetHosts, targetPorts, ruleProtocol, ruleAction);
}
+ public uint GetRulePosition(uint ruleId)
+ {
+ return ProxyBridgeNative.ProxyBridge_GetRulePosition(ruleId);
+ }
+
+ public bool MoveRuleToPosition(uint ruleId, uint newPosition)
+ {
+ return ProxyBridgeNative.ProxyBridge_MoveRuleToPosition(ruleId, newPosition);
+ }
+
public void SetDnsViaProxy(bool enable)
{
ProxyBridgeNative.ProxyBridge_SetDnsViaProxy(enable);
}
+ public void SetLocalhostViaProxy(bool enable)
+ {
+ ProxyBridgeNative.ProxyBridge_SetLocalhostViaProxy(enable);
+ }
+
public static void SetTrafficLoggingEnabled(bool enable)
{
ProxyBridgeNative.ProxyBridge_SetTrafficLoggingEnabled(enable);
diff --git a/Windows/gui/Services/UpdateService.cs b/Windows/gui/Services/UpdateService.cs
index 19b7091..3699ae3 100644
--- a/Windows/gui/Services/UpdateService.cs
+++ b/Windows/gui/Services/UpdateService.cs
@@ -45,11 +45,17 @@ public async Task CheckForUpdatesAsync()
a.Name.Contains("installer", StringComparison.OrdinalIgnoreCase) ||
a.Name.Contains("ProxyBridge", StringComparison.OrdinalIgnoreCase)));
+ // Only mark update as available if:
+ // 1. Version is newer AND
+ // 2. Windows installer (.exe) exists in release (platform-specific check)
+ var hasWindowsInstaller = setupAsset != null && !string.IsNullOrEmpty(setupAsset.BrowserDownloadUrl);
+ var isNewerVersion = IsNewerVersion(latestVersion, currentVersion);
+
return new VersionInfo
{
CurrentVersion = currentVersion,
LatestVersion = latestVersion,
- IsUpdateAvailable = IsNewerVersion(latestVersion, currentVersion),
+ IsUpdateAvailable = isNewerVersion && hasWindowsInstaller,
LatestVersionString = release?.TagName ?? "Unknown",
CurrentVersionString = FormatVersion(currentVersion),
DownloadUrl = setupAsset?.BrowserDownloadUrl,
diff --git a/Windows/gui/ViewModels/MainWindowViewModel.cs b/Windows/gui/ViewModels/MainWindowViewModel.cs
index 84688d6..58393f0 100644
--- a/Windows/gui/ViewModels/MainWindowViewModel.cs
+++ b/Windows/gui/ViewModels/MainWindowViewModel.cs
@@ -122,6 +122,7 @@ public void SetMainWindow(Window window)
_activityLogTimer.Start();
_proxyService.SetDnsViaProxy(_dnsViaProxy);
+ _proxyService.SetLocalhostViaProxy(_localhostViaProxy);
if (!string.IsNullOrEmpty(_currentProxyIp) &&
!string.IsNullOrEmpty(_currentProxyPort) &&
ushort.TryParse(_currentProxyPort, out ushort portNum))
@@ -288,6 +289,20 @@ public bool DnsViaProxy
}
}
+ private bool _localhostViaProxy = false; // Default: disabled for security
+ public bool LocalhostViaProxy
+ {
+ get => _localhostViaProxy;
+ set
+ {
+ if (SetProperty(ref _localhostViaProxy, value))
+ {
+ _proxyService?.SetLocalhostViaProxy(value);
+ SaveConfigurationInternal();
+ }
+ }
+ }
+
private bool _isTrafficLoggingEnabled = true;
public bool IsTrafficLoggingEnabled
{
@@ -362,6 +377,7 @@ public bool StartWithWindows
public ICommand ShowAboutCommand { get; }
public ICommand CheckForUpdatesCommand { get; }
public ICommand ToggleDnsViaProxyCommand { get; }
+ public ICommand ToggleLocalhostViaProxyCommand { get; }
public ICommand ToggleTrafficLoggingCommand { get; }
public ICommand ToggleCloseToTrayCommand { get; }
public ICommand ToggleStartWithWindowsCommand { get; }
@@ -501,6 +517,11 @@ public MainWindowViewModel()
DnsViaProxy = !DnsViaProxy;
});
+ ToggleLocalhostViaProxyCommand = new RelayCommand(() =>
+ {
+ LocalhostViaProxy = !LocalhostViaProxy;
+ });
+
ToggleTrafficLoggingCommand = new RelayCommand(() =>
{
IsTrafficLoggingEnabled = !IsTrafficLoggingEnabled;
@@ -707,6 +728,7 @@ private void LoadConfiguration()
_currentProxyPassword = config.ProxyPassword ?? "";
DnsViaProxy = config.DnsViaProxy;
+ LocalhostViaProxy = config.LocalhostViaProxy;
CloseToTray = config.CloseToTray;
IsTrafficLoggingEnabled = config.IsTrafficLoggingEnabled;
@@ -763,6 +785,7 @@ private void SaveConfigurationInternalAsync()
ProxyUsername = _currentProxyUsername,
ProxyPassword = _currentProxyPassword,
DnsViaProxy = _dnsViaProxy,
+ LocalhostViaProxy = _localhostViaProxy,
IsTrafficLoggingEnabled = _isTrafficLoggingEnabled,
Language = _currentLanguage,
CloseToTray = _closeToTray,
diff --git a/Windows/gui/ViewModels/ProxyRulesViewModel.cs b/Windows/gui/ViewModels/ProxyRulesViewModel.cs
index 61ebb77..7556f14 100644
--- a/Windows/gui/ViewModels/ProxyRulesViewModel.cs
+++ b/Windows/gui/ViewModels/ProxyRulesViewModel.cs
@@ -87,6 +87,7 @@ public string ProcessNameError
public ICommand ToggleSelectAllCommand { get; }
public ICommand ExportRulesCommand { get; }
public ICommand ImportRulesCommand { get; }
+ public ICommand DeleteSelectedRulesCommand { get; }
public bool HasSelectedRules => ProxyRules.Any(r => r.IsSelected);
public bool AllRulesSelected => ProxyRules.Any() && ProxyRules.All(r => r.IsSelected);
@@ -96,6 +97,14 @@ public void SetWindow(Window window)
_window = window;
}
+ public bool MoveRuleToPosition(uint ruleId, uint newPosition)
+ {
+ if (_proxyService == null)
+ return false;
+
+ return _proxyService.MoveRuleToPosition(ruleId, newPosition);
+ }
+
private void ResetRuleForm()
{
NewProcessName = "*";
@@ -306,6 +315,33 @@ public ProxyRulesViewModel(ObservableCollection proxyRules, Action
+ {
+ var selectedRules = ProxyRules.Where(r => r.IsSelected).ToList();
+ if (selectedRules.Count == 0)
+ return;
+
+ var confirmMsg = selectedRules.Count == 1
+ ? $"Delete 1 selected rule?"
+ : $"Delete {selectedRules.Count} selected rules?";
+
+ var confirmed = await ShowConfirmDialogAsync("Delete Selected Rules", confirmMsg);
+ if (!confirmed)
+ return;
+
+ foreach (var rule in selectedRules)
+ {
+ if (_proxyService != null && _proxyService.DeleteRule(rule.RuleId))
+ {
+ ProxyRules.Remove(rule);
+ }
+ }
+
+ _onConfigChanged?.Invoke();
+ OnPropertyChanged(nameof(HasSelectedRules));
+ OnPropertyChanged(nameof(AllRulesSelected));
+ });
}
private async System.Threading.Tasks.Task ShowConfirmDialogAsync(string title, string message)
diff --git a/Windows/gui/Views/MainWindow.axaml b/Windows/gui/Views/MainWindow.axaml
index 76094b8..48b1694 100644
--- a/Windows/gui/Views/MainWindow.axaml
+++ b/Windows/gui/Views/MainWindow.axaml
@@ -221,6 +221,14 @@
Margin="0"/>
+
+
+
+
+
+
+
+
+
+
+
+ Padding="20,16"
+ PointerPressed="Rule_PointerPressed"
+ DragDrop.AllowDrop="True">
("RulesItemsControl") is ItemsControl itemsControl)
+ {
+ itemsControl.AddHandler(DragDrop.DropEvent, Rules_Drop);
+ itemsControl.AddHandler(DragDrop.DragOverEvent, Rules_DragOver);
+ }
}
private void ProxyRulesWindow_DataContextChanged(object? sender, EventArgs e)
@@ -151,4 +158,70 @@ item.Tag is string tag &&
vm.NewProtocol = tag;
}
}
+
+ private async void Rule_PointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ if (sender is not Border border || border.DataContext is not ProxyRule rule)
+ return;
+
+ var dragData = new DataObject();
+ dragData.Set("DraggedRule", rule);
+
+ var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Move);
+
+ if (result == DragDropEffects.Move && DataContext is ProxyRulesViewModel vm)
+ {
+ // refsh indices after drag completes
+ for (int i = 0; i < vm.ProxyRules.Count; i++)
+ {
+ vm.ProxyRules[i].Index = i + 1;
+ }
+ }
+ }
+
+ private void Rules_DragOver(object? sender, DragEventArgs e)
+ {
+ e.DragEffects = DragDropEffects.Move;
+ }
+
+ private void Rules_Drop(object? sender, DragEventArgs e)
+ {
+ if (DataContext is not ProxyRulesViewModel vm)
+ return;
+
+ if (e.Data.Get("DraggedRule") is not ProxyRule draggedRule)
+ return;
+
+ if (e.Source is Control control)
+ {
+ var current = control;
+ while (current != null && current is not Border)
+ {
+ current = current.Parent as Control;
+ }
+
+ if (current is Border border && border.DataContext is ProxyRule targetRule)
+ {
+ if (draggedRule.RuleId == targetRule.RuleId)
+ return;
+
+ int draggedIndex = vm.ProxyRules.IndexOf(draggedRule);
+ int targetIndex = vm.ProxyRules.IndexOf(targetRule);
+
+ if (draggedIndex == -1 || targetIndex == -1 || draggedIndex == targetIndex)
+ return;
+
+ uint newPosition = (uint)(targetIndex + 1);
+ if (vm.MoveRuleToPosition(draggedRule.RuleId, newPosition))
+ {
+ vm.ProxyRules.Move(draggedIndex, targetIndex);
+
+ for (int i = 0; i < vm.ProxyRules.Count; i++)
+ {
+ vm.ProxyRules[i].Index = i + 1;
+ }
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/Windows/gui/app.manifest b/Windows/gui/app.manifest
index 3b02a12..0011b3c 100644
--- a/Windows/gui/app.manifest
+++ b/Windows/gui/app.manifest
@@ -1,7 +1,7 @@
> 0) & 0xFF;
+ if (action == RULE_ACTION_PROXY && !g_localhost_via_proxy && dest_first_octet == 127)
+ action = RULE_ACTION_DIRECT;
+
// Override PROXY to DIRECT for critical IPs and ports
if (action == RULE_ACTION_PROXY && is_broadcast_or_multicast(dest_ip))
action = RULE_ACTION_DIRECT;
@@ -377,15 +391,21 @@ static DWORD WINAPI packet_processor(LPVOID arg)
{
add_connection(src_port, src_ip, dest_ip, dest_port);
- // dest to rely udp
+ // redirect to UDP relay server at 127.0.0.1:34011
udp_header->DstPort = htons(LOCAL_UDP_RELAY_PORT);
-
- // Set destination to localhost (127.0.0.1)
ip_header->DstAddr = htonl(INADDR_LOOPBACK);
- // Keep source IP unchanged
- addr.Outbound = FALSE;
+ // check if source is localhos
+ BYTE src_first_octet = (ntohl(ip_header->SrcAddr) >> 24) & 0xFF;
+ BOOL src_is_loopback = (src_first_octet == 127);
+ if (!src_is_loopback)
+ {
+ // for non loopback source: mark as inbound
+ addr.Outbound = FALSE;
+ }
+ // for loopback we need keep as outbound (127.x.x.x -> 127.0.0.1)
+ // for a fucking stupid reason i missed this part for 6 months
}
}
}
@@ -419,10 +439,18 @@ static DWORD WINAPI packet_processor(LPVOID arg)
if (get_connection(dst_port, &orig_dest_ip, &orig_dest_port))
tcp_header->SrcPort = htons(orig_dest_port);
- UINT32 temp_addr = ip_header->DstAddr;
- ip_header->DstAddr = ip_header->SrcAddr;
- ip_header->SrcAddr = temp_addr;
- addr.Outbound = FALSE;
+ BYTE src_first = (ntohl(ip_header->SrcAddr) >> 24) & 0xFF;
+ BYTE dst_first = (ntohl(ip_header->DstAddr) >> 24) & 0xFF;
+ BOOL is_loopback = (src_first == 127 && dst_first == 127);
+
+ if (!is_loopback)
+ {
+ UINT32 temp_addr = ip_header->DstAddr;
+ ip_header->DstAddr = ip_header->SrcAddr;
+ ip_header->SrcAddr = temp_addr;
+ addr.Outbound = FALSE;
+ }
+
if (tcp_header->Fin || tcp_header->Rst)
remove_connection(dst_port);
@@ -434,11 +462,20 @@ static DWORD WINAPI packet_processor(LPVOID arg)
if (tcp_header->Fin || tcp_header->Rst)
remove_connection(src_port);
- UINT32 temp_addr = ip_header->DstAddr;
tcp_header->DstPort = htons(g_local_relay_port);
- ip_header->DstAddr = ip_header->SrcAddr;
- ip_header->SrcAddr = temp_addr;
- addr.Outbound = FALSE;
+
+ BYTE src_first = (ntohl(ip_header->SrcAddr) >> 24) & 0xFF;
+ BYTE dst_first = (ntohl(ip_header->DstAddr) >> 24) & 0xFF;
+ BOOL is_loopback = (src_first == 127 && dst_first == 127);
+
+ if (!is_loopback)
+ {
+ UINT32 temp_addr = ip_header->DstAddr;
+ ip_header->DstAddr = ip_header->SrcAddr;
+ ip_header->SrcAddr = temp_addr;
+ addr.Outbound = FALSE;
+ }
+
}
else
{
@@ -462,6 +499,10 @@ static DWORD WINAPI packet_processor(LPVOID arg)
else
action = check_process_rule(src_ip, src_port, orig_dest_ip, orig_dest_port, FALSE, &pid);
+ BYTE orig_dest_first_octet = (orig_dest_ip >> 0) & 0xFF;
+ if (action == RULE_ACTION_PROXY && !g_localhost_via_proxy && orig_dest_first_octet == 127)
+ action = RULE_ACTION_DIRECT;
+
// Override PROXY to DIRECT for criticl ips
if (action == RULE_ACTION_PROXY && is_broadcast_or_multicast(orig_dest_ip))
action = RULE_ACTION_DIRECT;
@@ -521,11 +562,29 @@ static DWORD WINAPI packet_processor(LPVOID arg)
{
add_connection(src_port, src_ip, orig_dest_ip, orig_dest_port);
- UINT32 temp_addr = ip_header->DstAddr;
tcp_header->DstPort = htons(g_local_relay_port);
- ip_header->DstAddr = ip_header->SrcAddr;
- ip_header->SrcAddr = temp_addr;
- addr.Outbound = FALSE;
+
+ // check if this is localhost -> localhost traffic
+ BYTE src_first_octet = (ntohl(ip_header->SrcAddr) >> 24) & 0xFF;
+ BYTE dst_first_octet = (ntohl(ip_header->DstAddr) >> 24) & 0xFF;
+ BOOL is_loopback_to_loopback = (src_first_octet == 127 && dst_first_octet == 127);
+
+ if (is_loopback_to_loopback)
+ {
+ // for localhost -> localhost just change port, keep as outbound
+ // dont swap IPs Windows loopback routing needs both to stay 127.x.x.x
+ log_message("[PACKET] Loopback redirect: 127.x.x.x:%d -> 127.x.x.x:%d (relay port %d)",
+ ntohs(tcp_header->SrcPort), orig_dest_port, g_local_relay_port);
+ // addr.Outbound stays TRUE
+ }
+ else
+ {
+ // for normal traffic: swap IPs and mark as inbound (standard relay behavior)
+ UINT32 temp_addr = ip_header->DstAddr;
+ ip_header->DstAddr = ip_header->SrcAddr;
+ ip_header->SrcAddr = temp_addr;
+ addr.Outbound = FALSE;
+ }
}
}
}
@@ -767,6 +826,40 @@ static BOOL match_ip_pattern(const char *pattern, UINT32 ip)
if (pattern == NULL || strcmp(pattern, "*") == 0)
return TRUE;
+ // check for IP range
+ char *dash = strchr(pattern, '-');
+ if (dash != NULL)
+ {
+ char start_ip_str[64], end_ip_str[64];
+ size_t start_len = dash - pattern;
+ if (start_len >= sizeof(start_ip_str))
+ return FALSE;
+
+ strncpy_s(start_ip_str, sizeof(start_ip_str), pattern, start_len);
+ start_ip_str[start_len] = '\0';
+ strncpy_s(end_ip_str, sizeof(end_ip_str), dash + 1, _TRUNCATE);
+
+ // parse start and end IPs
+ UINT32 start_ip = 0, end_ip = 0;
+ int s1, s2, s3, s4, e1, e2, e3, e4;
+
+ if (sscanf_s(start_ip_str, "%d.%d.%d.%d", &s1, &s2, &s3, &s4) == 4 &&
+ sscanf_s(end_ip_str, "%d.%d.%d.%d", &e1, &e2, &e3, &e4) == 4)
+ {
+ start_ip = (s1 << 0) | (s2 << 8) | (s3 << 16) | (s4 << 24);
+ end_ip = (e1 << 0) | (e2 << 8) | (e3 << 16) | (e4 << 24);
+
+ // checking as network byte order would be wrong, compare as little-endian UINT32
+ // change to big-endian for proper comparison
+ UINT32 ip_be = ((ip & 0xFF) << 24) | ((ip & 0xFF00) << 8) | ((ip & 0xFF0000) >> 8) | ((ip & 0xFF000000) >> 24);
+ UINT32 start_be = ((start_ip & 0xFF) << 24) | ((start_ip & 0xFF00) << 8) | ((start_ip & 0xFF0000) >> 8) | ((start_ip & 0xFF000000) >> 24);
+ UINT32 end_be = ((end_ip & 0xFF) << 24) | ((end_ip & 0xFF00) << 8) | ((end_ip & 0xFF0000) >> 8) | ((end_ip & 0xFF000000) >> 24);
+
+ return (ip_be >= start_be && ip_be <= end_be);
+ }
+ return FALSE;
+ }
+
// Extract 4 octets from IP (little-endian)
unsigned char ip_octets[4];
ip_octets[0] = (ip >> 0) & 0xFF;
@@ -977,13 +1070,13 @@ static BOOL match_process_list(const char *process_list, const char *process_nam
static BOOL is_broadcast_or_multicast(UINT32 ip)
{
- // Localhost: 127.0.0.0/8 (127.x.x.x)
+ // note: Localhost (127.x.x.x) is now supported for proxying
+ // This allows intercepting localhost connections for MITM scenarios
+
BYTE first_octet = (ip >> 0) & 0xFF;
- if (first_octet == 127)
- return TRUE;
+ BYTE second_octet = (ip >> 8) & 0xFF;
// APIPA (Link-Local): 169.254.0.0/16 (169.254.x.x)
- BYTE second_octet = (ip >> 8) & 0xFF;
if (first_octet == 169 && second_octet == 254)
return TRUE;
@@ -1880,7 +1973,7 @@ static DWORD WINAPI connection_handler(LPVOID arg)
if (connect(socks_sock, (struct sockaddr *)&socks_addr, sizeof(socks_addr)) == SOCKET_ERROR)
{
- log_message("Failed to connect to proxy (%d)", WSAGetLastError());
+ log_message("[RELAY] Failed to connect to proxy (%d)", WSAGetLastError());
closesocket(client_sock);
closesocket(socks_sock);
return 0;
@@ -2385,6 +2478,92 @@ PROXYBRIDGE_API BOOL ProxyBridge_EditRule(UINT32 rule_id, const char* process_na
return FALSE;
}
+PROXYBRIDGE_API UINT32 ProxyBridge_GetRulePosition(UINT32 rule_id)
+{
+ if (rule_id == 0)
+ return 0;
+
+ UINT32 position = 1;
+ PROCESS_RULE *rule = rules_list;
+ while (rule != NULL)
+ {
+ if (rule->rule_id == rule_id)
+ return position;
+ position++;
+ rule = rule->next;
+ }
+ return 0;
+}
+
+PROXYBRIDGE_API BOOL ProxyBridge_MoveRuleToPosition(UINT32 rule_id, UINT32 new_position)
+{
+ if (rule_id == 0 || new_position == 0)
+ return FALSE;
+
+ // first rule and remove it from current position
+ PROCESS_RULE *rule = rules_list;
+ PROCESS_RULE *prev = NULL;
+
+ while (rule != NULL)
+ {
+ if (rule->rule_id == rule_id)
+ break;
+ prev = rule;
+ rule = rule->next;
+ }
+
+ if (rule == NULL)
+ return FALSE;
+
+ // Remove from current position
+ if (prev == NULL)
+ {
+ rules_list = rule->next;
+ }
+ else
+ {
+ prev->next = rule->next;
+ }
+
+ // Insert at new position
+ if (new_position == 1)
+ {
+ // Insert at head
+ rule->next = rules_list;
+ rules_list = rule;
+ }
+ else
+ {
+ // taken from stackflow
+ PROCESS_RULE *current = rules_list;
+ UINT32 pos = 1;
+
+ while (current != NULL && pos < new_position - 1)
+ {
+ current = current->next;
+ pos++;
+ }
+
+ if (current == NULL)
+ {
+ // position is beyond list end we can append to tail
+ current = rules_list;
+ while (current->next != NULL)
+ current = current->next;
+ current->next = rule;
+ rule->next = NULL;
+ }
+ else
+ {
+ rule->next = current->next;
+ current->next = rule;
+ }
+ }
+
+ log_message("Moved rule ID %u to position %u", rule_id, new_position);
+ return TRUE;
+}
+
PROXYBRIDGE_API BOOL ProxyBridge_SetProxyConfig(ProxyType type, const char* proxy_ip, UINT16 proxy_port, const char* username, const char* password)
{
if (proxy_ip == NULL || proxy_ip[0] == '\0' || proxy_port == 0)
@@ -2426,6 +2605,12 @@ PROXYBRIDGE_API void ProxyBridge_SetDnsViaProxy(BOOL enable)
log_message("DNS routing: %s", enable ? "via proxy" : "direct");
}
+PROXYBRIDGE_API void ProxyBridge_SetLocalhostViaProxy(BOOL enable)
+{
+ g_localhost_via_proxy = enable;
+ log_message("Localhost routing: %s (most proxies block localhost for SSRF prevention)", enable ? "via proxy" : "direct");
+}
+
PROXYBRIDGE_API void ProxyBridge_SetLogCallback(LogCallback callback)
{
g_log_callback = callback;
@@ -2724,9 +2909,11 @@ PROXYBRIDGE_API BOOL ProxyBridge_Start(void)
Sleep(500);
snprintf(filter, sizeof(filter),
- "(tcp and (outbound or (tcp.DstPort == %d or tcp.SrcPort == %d))) or (udp and (outbound or (udp.DstPort == %d or udp.SrcPort == %d)))",
+ "(tcp and (outbound or loopback or (tcp.DstPort == %d or tcp.SrcPort == %d))) or (udp and (outbound or loopback or (udp.DstPort == %d or udp.SrcPort == %d)))",
g_local_relay_port, g_local_relay_port, LOCAL_UDP_RELAY_PORT, LOCAL_UDP_RELAY_PORT);
+ // Note: Added 'loopback' to filter to capture localhost (127.x.x.x) traffic
+ // This enables proxying local connections for MITM scenarios
windivert_handle = WinDivertOpen(filter, WINDIVERT_LAYER_NETWORK, priority, 0);
if (windivert_handle == INVALID_HANDLE_VALUE)
{
diff --git a/Windows/src/ProxyBridge.h b/Windows/src/ProxyBridge.h
index 6f7f1c1..f728b76 100644
--- a/Windows/src/ProxyBridge.h
+++ b/Windows/src/ProxyBridge.h
@@ -38,8 +38,11 @@ PROXYBRIDGE_API BOOL ProxyBridge_EnableRule(UINT32 rule_id);
PROXYBRIDGE_API BOOL ProxyBridge_DisableRule(UINT32 rule_id);
PROXYBRIDGE_API BOOL ProxyBridge_DeleteRule(UINT32 rule_id);
PROXYBRIDGE_API BOOL ProxyBridge_EditRule(UINT32 rule_id, const char* process_name, const char* target_hosts, const char* target_ports, RuleProtocol protocol, RuleAction action);
+PROXYBRIDGE_API BOOL ProxyBridge_MoveRuleToPosition(UINT32 rule_id, UINT32 new_position); // Move rule to specific position (1=first, 2=second, etc)
+PROXYBRIDGE_API UINT32 ProxyBridge_GetRulePosition(UINT32 rule_id); // Get current position of rule in list (1-based)
PROXYBRIDGE_API BOOL ProxyBridge_SetProxyConfig(ProxyType type, const char* proxy_ip, UINT16 proxy_port, const char* username, const char* password); // proxy_ip can be IP address or hostname
PROXYBRIDGE_API void ProxyBridge_SetDnsViaProxy(BOOL enable);
+PROXYBRIDGE_API void ProxyBridge_SetLocalhostViaProxy(BOOL enable);
PROXYBRIDGE_API void ProxyBridge_SetLogCallback(LogCallback callback);
PROXYBRIDGE_API void ProxyBridge_SetConnectionCallback(ConnectionCallback callback);
PROXYBRIDGE_API void ProxyBridge_SetTrafficLoggingEnabled(BOOL enable);
diff --git a/img/ProxyBridge-linux.png b/img/ProxyBridge-linux.png
new file mode 100644
index 0000000..7171e4b
Binary files /dev/null and b/img/ProxyBridge-linux.png differ
diff --git a/img/ProxyBridge_CLI-linux.png b/img/ProxyBridge_CLI-linux.png
new file mode 100644
index 0000000..4c065f1
Binary files /dev/null and b/img/ProxyBridge_CLI-linux.png differ
diff --git a/img/proxy-rule-linux.png b/img/proxy-rule-linux.png
new file mode 100644
index 0000000..d6d3ad7
Binary files /dev/null and b/img/proxy-rule-linux.png differ
diff --git a/img/proxy-rule2-linux.png b/img/proxy-rule2-linux.png
new file mode 100644
index 0000000..69a6edf
Binary files /dev/null and b/img/proxy-rule2-linux.png differ
diff --git a/img/proxy-setting-linux.png b/img/proxy-setting-linux.png
new file mode 100644
index 0000000..3907d5e
Binary files /dev/null and b/img/proxy-setting-linux.png differ