mirror of
https://github.com/tcsenpai/oproxy.git
synced 2025-06-02 17:30:04 +00:00
first commit
This commit is contained in:
commit
65e6c7e0f8
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
*.log
|
||||
*.log.*
|
||||
__pycache__
|
||||
.env
|
102
README.md
Normal file
102
README.md
Normal file
@ -0,0 +1,102 @@
|
||||
# OProxy
|
||||
|
||||
*High-performance, transparent proxy that supports both TCP and UDP protocols.*
|
||||
|
||||
A high-performance, transparent proxy that supports both TCP and UDP protocols.
|
||||
|
||||
**NOTE:** This proxy has been designed with local API proxy in mind. Specifically, I used it to forward Ollama API requests to the remote Ollama server for applications that try to connect to the local Ollama server on localhost.
|
||||
|
||||
## Features
|
||||
|
||||
- Transparent TCP proxying
|
||||
- HTTP/HTTPS proxying without decrypting the traffic
|
||||
- Optional UDP support
|
||||
- Detailed logging capabilities
|
||||
- Configurable through environment variables
|
||||
- Support for both file and stdout logging
|
||||
- Data content logging (optional)
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.7+
|
||||
- python-dotenv
|
||||
- socket
|
||||
- threading
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/tcsenpai/oproxy.git
|
||||
|
||||
cd oproxy
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
|
||||
3. Copy the example environment file:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
|
||||
4. Edit the .env file with your configuration:
|
||||
|
||||
```bash
|
||||
PROXY_PORT=11434
|
||||
TARGET_HOST=127.0.0.1
|
||||
TARGET_PORT=80
|
||||
```
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
Basic TCP proxy:
|
||||
|
||||
```bash
|
||||
python src/main.py
|
||||
```
|
||||
|
||||
Enable logging to file:
|
||||
|
||||
```bash
|
||||
python src/main.py --log-file=proxy.log
|
||||
```
|
||||
|
||||
Enable data logging with debug level:
|
||||
|
||||
```bash
|
||||
python src/main.py --log-file proxy.log --log-data --log-level DEBUG
|
||||
```
|
||||
|
||||
Enable UDP support:
|
||||
|
||||
```bash
|
||||
python src/main.py --enable-udp
|
||||
```
|
||||
|
||||
|
||||
## Command Line Arguments
|
||||
|
||||
- `--log-file`: Path to the log file
|
||||
- `--log-data`: Enable logging of data content
|
||||
- `--log-level`: Set logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
- `--enable-udp`: Enable UDP proxy alongside TCP
|
||||
|
||||
## Notes
|
||||
|
||||
- TCP proxy runs on the port specified in .env
|
||||
- UDP proxy (if enabled) runs on PROXY_PORT + 1
|
||||
- Data logging should be used carefully as it may contain sensitive information
|
||||
- UDP support is experimental and runs as a daemon thread
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
3
env.example
Normal file
3
env.example
Normal file
@ -0,0 +1,3 @@
|
||||
PROXY_PORT=11434
|
||||
TARGET_HOST=localhost
|
||||
TARGET_PORT=11434
|
174
main.py
Normal file
174
main.py
Normal file
@ -0,0 +1,174 @@
|
||||
import socket
|
||||
import threading
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
import logging
|
||||
from datetime import datetime
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Configuration
|
||||
PROXY_HOST = '0.0.0.0' # Listen on all interfaces
|
||||
PROXY_PORT = int(os.getenv('PROXY_PORT', 8080))
|
||||
TARGET_HOST = os.getenv('TARGET_HOST', 'localhost')
|
||||
TARGET_PORT = int(os.getenv('TARGET_PORT', 80))
|
||||
|
||||
def setup_logging(log_file=None, log_level=logging.INFO):
|
||||
# Configure logging format
|
||||
log_format = '%(asctime)s - %(levelname)s - %(message)s'
|
||||
|
||||
# Setup basic configuration
|
||||
if log_file:
|
||||
logging.basicConfig(
|
||||
level=log_level,
|
||||
format=log_format,
|
||||
handlers=[
|
||||
logging.FileHandler(log_file),
|
||||
logging.StreamHandler() # This will also print to stdout
|
||||
]
|
||||
)
|
||||
else:
|
||||
logging.basicConfig(
|
||||
level=log_level,
|
||||
format=log_format
|
||||
)
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description='Transparent TCP/UDP Proxy with logging capabilities')
|
||||
parser.add_argument('--log-file', type=str, help='Path to the log file')
|
||||
parser.add_argument('--log-data', action='store_true', help='Enable logging of data content')
|
||||
parser.add_argument('--log-level',
|
||||
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
||||
default='INFO',
|
||||
help='Set the logging level')
|
||||
parser.add_argument('--enable-udp', action='store_true', help='Enable UDP proxy alongside TCP')
|
||||
return parser.parse_args()
|
||||
|
||||
def handle_tcp_client(client_socket, log_data=False):
|
||||
client_address = client_socket.getpeername()
|
||||
logging.info(f"New connection from {client_address[0]}:{client_address[1]}")
|
||||
|
||||
# Connect to target server
|
||||
target_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
target_socket.connect((TARGET_HOST, TARGET_PORT))
|
||||
logging.info(f"Connected to target {TARGET_HOST}:{TARGET_PORT}")
|
||||
|
||||
def forward(source, destination, direction):
|
||||
try:
|
||||
while True:
|
||||
data = source.recv(4096)
|
||||
if not data:
|
||||
break
|
||||
if log_data:
|
||||
print("[INFO] Logging data is enabled")
|
||||
src = source.getpeername()
|
||||
dst = destination.getpeername()
|
||||
timestamp = datetime.now().isoformat()
|
||||
logging.debug(f"[{direction}] {src[0]}:{src[1]} -> {dst[0]}:{dst[1]}")
|
||||
logging.debug(f"Data: {data[:1024]!r}...") # Log first 1KB of data
|
||||
destination.send(data)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in {direction}: {str(e)}")
|
||||
finally:
|
||||
source.close()
|
||||
destination.close()
|
||||
logging.info(f"Connection closed ({direction})")
|
||||
|
||||
# Create two threads for bidirectional communication
|
||||
client_to_target = threading.Thread(
|
||||
target=forward,
|
||||
args=(client_socket, target_socket, "CLIENT->TARGET")
|
||||
)
|
||||
target_to_client = threading.Thread(
|
||||
target=forward,
|
||||
args=(target_socket, client_socket, "TARGET->CLIENT")
|
||||
)
|
||||
|
||||
client_to_target.start()
|
||||
target_to_client.start()
|
||||
|
||||
def handle_udp_proxy(log_data=False):
|
||||
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
udp_socket.bind((PROXY_HOST, PROXY_PORT + 1)) # Use next port for UDP
|
||||
|
||||
clients = defaultdict(dict)
|
||||
|
||||
logging.info(f"UDP proxy listening on {PROXY_HOST}:{PROXY_PORT + 1}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
data, client_addr = udp_socket.recvfrom(4096)
|
||||
if log_data:
|
||||
logging.debug(f"UDP: {client_addr} -> {TARGET_HOST}:{TARGET_PORT}")
|
||||
logging.debug(f"Data: {data[:1024]!r}...")
|
||||
|
||||
# Forward to target
|
||||
target_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
target_socket.sendto(data, (TARGET_HOST, TARGET_PORT))
|
||||
|
||||
# Store socket for this client
|
||||
clients[client_addr]['socket'] = target_socket
|
||||
clients[client_addr]['target'] = (TARGET_HOST, TARGET_PORT)
|
||||
|
||||
# Handle response in a separate thread to not block
|
||||
def handle_response(client_addr, target_socket):
|
||||
try:
|
||||
response, _ = target_socket.recvfrom(4096)
|
||||
udp_socket.sendto(response, client_addr)
|
||||
if log_data:
|
||||
logging.debug(f"UDP Response: {TARGET_HOST}:{TARGET_PORT} -> {client_addr}")
|
||||
except Exception as e:
|
||||
logging.error(f"UDP Response Error: {str(e)}")
|
||||
finally:
|
||||
target_socket.close()
|
||||
|
||||
threading.Thread(target=handle_response,
|
||||
args=(client_addr, target_socket)).start()
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"UDP Error: {str(e)}")
|
||||
|
||||
def main():
|
||||
# Parse command line arguments
|
||||
args = parse_args()
|
||||
|
||||
# Setup logging
|
||||
log_level = getattr(logging, args.log_level)
|
||||
setup_logging(args.log_file, log_level)
|
||||
|
||||
# Start TCP proxy (main functionality)
|
||||
tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
tcp_server.bind((PROXY_HOST, PROXY_PORT))
|
||||
tcp_server.listen(100)
|
||||
|
||||
logging.info(f"TCP proxy listening on {PROXY_HOST}:{PROXY_PORT}")
|
||||
logging.info(f"Forwarding to {TARGET_HOST}:{TARGET_PORT}")
|
||||
logging.info(f"Logging level: {args.log_level}")
|
||||
if args.log_file:
|
||||
logging.info(f"Logging to file: {args.log_file}")
|
||||
if args.log_data:
|
||||
logging.info("Data logging is enabled")
|
||||
|
||||
# Start UDP proxy if enabled
|
||||
if args.enable_udp:
|
||||
udp_thread = threading.Thread(target=handle_udp_proxy,
|
||||
args=(args.log_data,),
|
||||
daemon=True)
|
||||
udp_thread.start()
|
||||
logging.info("UDP proxy enabled")
|
||||
|
||||
# Main TCP loop
|
||||
while True:
|
||||
client_socket, addr = tcp_server.accept()
|
||||
proxy_thread = threading.Thread(
|
||||
target=handle_tcp_client,
|
||||
args=(client_socket, args.log_data)
|
||||
)
|
||||
proxy_thread.start()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
python-dotenv
|
69
src/main.py
Normal file
69
src/main.py
Normal file
@ -0,0 +1,69 @@
|
||||
import os
|
||||
import argparse
|
||||
from dotenv import load_dotenv
|
||||
import socket
|
||||
import threading
|
||||
import logging
|
||||
|
||||
from proxy.logger import setup_logging
|
||||
from proxy.tcp_handler import TCPHandler
|
||||
from proxy.udp_handler import UDPHandler
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description='Transparent TCP/UDP Proxy with logging capabilities')
|
||||
parser.add_argument('--log-file', type=str, help='Path to the log file')
|
||||
parser.add_argument('--log-data', action='store_true', help='Enable logging of data content')
|
||||
parser.add_argument('--full-debug', action='store_true', help='Enable full data logging (entire payload)')
|
||||
parser.add_argument('--log-level',
|
||||
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
||||
default='INFO',
|
||||
help='Set the logging level')
|
||||
parser.add_argument('--enable-udp', action='store_true', help='Enable UDP proxy alongside TCP')
|
||||
return parser.parse_args()
|
||||
|
||||
def main():
|
||||
# Load configuration
|
||||
load_dotenv()
|
||||
PROXY_HOST = '0.0.0.0'
|
||||
PROXY_PORT = int(os.getenv('PROXY_PORT', 8080))
|
||||
TARGET_HOST = os.getenv('TARGET_HOST', 'localhost')
|
||||
TARGET_PORT = int(os.getenv('TARGET_PORT', 80))
|
||||
|
||||
# Parse arguments and setup logging
|
||||
args = parse_args()
|
||||
setup_logging(args.log_file, getattr(logging, args.log_level))
|
||||
|
||||
# Initialize TCP handler
|
||||
tcp_handler = TCPHandler(TARGET_HOST, TARGET_PORT)
|
||||
|
||||
# Setup TCP server
|
||||
tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
tcp_server.bind((PROXY_HOST, PROXY_PORT))
|
||||
tcp_server.listen(100)
|
||||
|
||||
logging.info(f"TCP proxy listening on {PROXY_HOST}:{PROXY_PORT}")
|
||||
logging.info(f"Forwarding to {TARGET_HOST}:{TARGET_PORT}")
|
||||
|
||||
# Start UDP handler if enabled
|
||||
if args.enable_udp:
|
||||
udp_handler = UDPHandler(PROXY_HOST, PROXY_PORT, TARGET_HOST, TARGET_PORT)
|
||||
udp_thread = threading.Thread(
|
||||
target=udp_handler.start,
|
||||
args=(args.log_data,),
|
||||
daemon=True
|
||||
)
|
||||
udp_thread.start()
|
||||
logging.info("UDP proxy enabled")
|
||||
|
||||
# Main TCP loop
|
||||
while True:
|
||||
client_socket, addr = tcp_server.accept()
|
||||
proxy_thread = threading.Thread(
|
||||
target=tcp_handler.handle_client,
|
||||
args=(client_socket, args.log_data, args.full_debug)
|
||||
)
|
||||
proxy_thread.start()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
20
src/proxy/logger.py
Normal file
20
src/proxy/logger.py
Normal file
@ -0,0 +1,20 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
def setup_logging(log_file: Optional[str] = None, log_level: int = logging.INFO) -> None:
|
||||
log_format = '%(asctime)s - %(levelname)s - %(message)s'
|
||||
|
||||
if log_file:
|
||||
logging.basicConfig(
|
||||
level=log_level,
|
||||
format=log_format,
|
||||
handlers=[
|
||||
logging.FileHandler(log_file),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
else:
|
||||
logging.basicConfig(
|
||||
level=log_level,
|
||||
format=log_format
|
||||
)
|
104
src/proxy/tcp_handler.py
Normal file
104
src/proxy/tcp_handler.py
Normal file
@ -0,0 +1,104 @@
|
||||
import socket
|
||||
import threading
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Tuple, Optional
|
||||
|
||||
class TCPHandler:
|
||||
def __init__(self, target_host: str, target_port: int):
|
||||
self.target_host = target_host
|
||||
self.target_port = target_port
|
||||
|
||||
def log_data_content(self, data: bytes, src: tuple, dst: tuple, direction: str, full_debug: bool = False) -> None:
|
||||
try:
|
||||
# Always log basic info
|
||||
logging.info(f"[{direction}] {src[0]}:{src[1]} -> {dst[0]}:{dst[1]} ({len(data)} bytes)")
|
||||
|
||||
# Attempt to decode the data
|
||||
if all(32 <= byte <= 126 or byte in (9, 10, 13) for byte in data[:100]):
|
||||
decoded = data.decode('utf-8', errors='replace')
|
||||
if full_debug:
|
||||
# Log the entire payload with clear separators
|
||||
logging.debug("="*50)
|
||||
logging.debug(f"FULL DATA [{direction}] START")
|
||||
logging.debug("="*50)
|
||||
logging.debug(decoded)
|
||||
logging.debug("="*50)
|
||||
logging.debug(f"FULL DATA [{direction}] END")
|
||||
logging.debug("="*50)
|
||||
else:
|
||||
# Log just a preview
|
||||
logging.debug(f"Data preview: {decoded[:200]}...")
|
||||
else:
|
||||
if full_debug:
|
||||
# For binary data, log the full hex dump
|
||||
logging.debug("="*50)
|
||||
logging.debug(f"FULL BINARY DATA [{direction}] START")
|
||||
logging.debug("="*50)
|
||||
logging.debug(' '.join(f'{byte:02x}' for byte in data))
|
||||
logging.debug("="*50)
|
||||
logging.debug(f"FULL BINARY DATA [{direction}] END")
|
||||
logging.debug("="*50)
|
||||
else:
|
||||
logging.debug(f"Binary data: {len(data)} bytes transferred")
|
||||
except Exception as decode_err:
|
||||
logging.debug(f"Could not decode data: {decode_err}")
|
||||
|
||||
def forward(self, source: socket.socket, destination: socket.socket,
|
||||
direction: str, log_data: bool, full_debug: bool = False) -> None:
|
||||
total_bytes = 0
|
||||
try:
|
||||
while True:
|
||||
data = source.recv(4096)
|
||||
if not data:
|
||||
break
|
||||
total_bytes += len(data)
|
||||
destination.send(data)
|
||||
|
||||
if log_data:
|
||||
src = source.getpeername()
|
||||
dst = destination.getpeername()
|
||||
self.log_data_content(data, src, dst, direction, full_debug)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in {direction}: {str(e)}")
|
||||
finally:
|
||||
logging.info(f"Connection closed ({direction}). Total bytes transferred: {total_bytes}")
|
||||
try:
|
||||
source.close()
|
||||
destination.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
def handle_client(self, client_socket: socket.socket, log_data: bool, full_debug: bool = False) -> None:
|
||||
try:
|
||||
client_address = client_socket.getpeername()
|
||||
logging.info(f"New connection from {client_address[0]}:{client_address[1]}")
|
||||
|
||||
target_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
target_socket.connect((self.target_host, self.target_port))
|
||||
logging.info(f"Connected to target {self.target_host}:{self.target_port}")
|
||||
|
||||
client_to_target = threading.Thread(
|
||||
target=self.forward,
|
||||
args=(client_socket, target_socket, "CLIENT->TARGET", log_data, full_debug),
|
||||
name="ClientToTarget"
|
||||
)
|
||||
target_to_client = threading.Thread(
|
||||
target=self.forward,
|
||||
args=(target_socket, client_socket, "TARGET->CLIENT", log_data, full_debug),
|
||||
name="TargetToClient"
|
||||
)
|
||||
|
||||
client_to_target.start()
|
||||
target_to_client.start()
|
||||
|
||||
client_to_target.join()
|
||||
target_to_client.join()
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in handle_client: {str(e)}")
|
||||
try:
|
||||
client_socket.close()
|
||||
except:
|
||||
pass
|
50
src/proxy/udp_handler.py
Normal file
50
src/proxy/udp_handler.py
Normal file
@ -0,0 +1,50 @@
|
||||
import socket
|
||||
import threading
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from typing import Dict, Any
|
||||
|
||||
class UDPHandler:
|
||||
def __init__(self, proxy_host: str, proxy_port: int,
|
||||
target_host: str, target_port: int):
|
||||
self.proxy_host = proxy_host
|
||||
self.proxy_port = proxy_port + 1 # UDP uses next port
|
||||
self.target_host = target_host
|
||||
self.target_port = target_port
|
||||
self.clients: Dict[Any, dict] = defaultdict(dict)
|
||||
|
||||
def handle_response(self, client_addr: tuple, target_socket: socket.socket,
|
||||
udp_socket: socket.socket, log_data: bool) -> None:
|
||||
try:
|
||||
response, _ = target_socket.recvfrom(4096)
|
||||
udp_socket.sendto(response, client_addr)
|
||||
if log_data:
|
||||
logging.debug(f"UDP Response: {self.target_host}:{self.target_port} -> {client_addr}")
|
||||
except Exception as e:
|
||||
logging.error(f"UDP Response Error: {str(e)}")
|
||||
finally:
|
||||
target_socket.close()
|
||||
|
||||
def start(self, log_data: bool) -> None:
|
||||
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
udp_socket.bind((self.proxy_host, self.proxy_port))
|
||||
|
||||
logging.info(f"UDP proxy listening on {self.proxy_host}:{self.proxy_port}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
data, client_addr = udp_socket.recvfrom(4096)
|
||||
if log_data:
|
||||
logging.debug(f"UDP: {client_addr} -> {self.target_host}:{self.target_port}")
|
||||
logging.debug(f"Data: {data[:1024]!r}...")
|
||||
|
||||
target_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
target_socket.sendto(data, (self.target_host, self.target_port))
|
||||
|
||||
threading.Thread(
|
||||
target=self.handle_response,
|
||||
args=(client_addr, target_socket, udp_socket, log_data)
|
||||
).start()
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"UDP Error: {str(e)}")
|
Loading…
x
Reference in New Issue
Block a user