first commit

This commit is contained in:
tcsenpai 2024-11-09 13:27:10 +01:00
commit 65e6c7e0f8
9 changed files with 527 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.log
*.log.*
__pycache__
.env

102
README.md Normal file
View 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
View File

@ -0,0 +1,3 @@
PROXY_PORT=11434
TARGET_HOST=localhost
TARGET_PORT=11434

174
main.py Normal file
View 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
View File

@ -0,0 +1 @@
python-dotenv

69
src/main.py Normal file
View 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
View 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
View 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
View 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)}")