├── README.md └── RPS.py /README.md: -------------------------------------------------------------------------------- 1 | # RPS.py 2 | Rotating Proxy Server 3 | 4 | # What it is 5 | RPS is a TCP proxy gateway that routes traffic through a randomly selected endpoint proxy from a pre-defined list. 6 | 7 | RPS is protocol-transparent. Which means, when endpoint proxies are all SOCKS5 protocol, gateway protocol should be set to SOCKS5 too. Similarly to HTTP/HTTPS.\ 8 | You should not mix different proxies together in a same list. 9 | 10 | This tool can be used to bypass firewalls. However, it is intended for educational purposes only. Illegal or immoral behavior is not encouraged. 11 | 12 | 13 | # Usage 14 | 15 | ```sh 16 | usage: RPS.py [-h] [-l ADDRESS] [-p PORT] [--log LOG_PATH] [--bufsize BUF_SIZE] [--backlog BACKLOG] [-v] PROXY_LIST 17 | 18 | Rotating Proxy Server 19 | 20 | positional arguments: 21 | PROXY_LIST Proxy list file, lines of "IP:PORT" 22 | 23 | options: 24 | -h, --help show this help message and exit 25 | -l ADDRESS, --listen ADDRESS 26 | IP address to listen on (default: 127.0.0.1) 27 | -p PORT, --port PORT Port to listen on (default: 1080) 28 | --log LOG_PATH Log Path (default: RPS.log) 29 | --bufsize BUF_SIZE Buffer size of each connection (default: 4096) 30 | --backlog BACKLOG Socket backlog (default: 4096) 31 | -v, --verbose Verbose mode (default: False) 32 | ``` 33 | 34 | ### DKing@ZeroSec 35 | 36 | 37 | -------------------------------------------------------------------------------- /RPS.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | #-*- coding: utf-8 -*- 3 | 4 | import argparse 5 | import signal 6 | import socket 7 | from concurrent.futures import ThreadPoolExecutor 8 | import re 9 | import logging 10 | import random 11 | 12 | def parse_args(): 13 | parser = argparse.ArgumentParser(description='Rotating Proxy Server', formatter_class=argparse.ArgumentDefaultsHelpFormatter) 14 | parser.add_argument('-l', '--listen', metavar='ADDRESS', default='127.0.0.1', help='IP address to listen on') 15 | parser.add_argument('-p', '--port', metavar='PORT', type=int, default=1080, help='Port to listen on') 16 | parser.add_argument('--log', metavar='LOG_PATH', default='RPS.log', help='Log Path') 17 | parser.add_argument('--bufsize', metavar='BUF_SIZE', type=int, default=4096, help='Buffer size of each connection') 18 | parser.add_argument('--backlog', metavar='BACKLOG', type=int, default=4096, help='Socket backlog') 19 | parser.add_argument('-v', '--verbose', action='store_true', help='Verbose mode') 20 | # positional arguments 21 | parser.add_argument('proxylist', metavar='PROXY_LIST', help='Proxy list file, lines of "IP:PORT"') 22 | return parser.parse_args() 23 | 24 | class ProxyPicker: 25 | def __init__(self, filename): 26 | self.proxies = [] 27 | with open(filename, encoding='utf-8') as f: 28 | for line in f: 29 | result = self._parse_line(line) 30 | if result == False: 31 | logging.error(f'Unknown proxy format: {line}') 32 | continue 33 | self.proxies.append(result) 34 | logging.info(f'Loaded {len(self.proxies)} proxies from {filename}') 35 | 36 | def _parse_line(self, line:str): 37 | proxy = line.strip() 38 | if not proxy: 39 | return False 40 | if proxy.startswith('#'): 41 | return False 42 | if ':' not in proxy: 43 | return False 44 | if proxy.count(':') != 1: 45 | return False 46 | addr, port = proxy.split(':') 47 | if not addr or not port: 48 | return False 49 | if port.isdigit() == False or int(port) < 0 or int(port) > 65535: 50 | return False 51 | if not (re.match('^\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b$', addr) or \ 52 | re.match('^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', addr)): 53 | return False 54 | return (addr, int(port)) 55 | 56 | def get_random_proxy(self): 57 | if len(self.proxies) == 0: 58 | return None 59 | return random.choice(self.proxies) 60 | 61 | class ProxyServer: 62 | def __init__(self): 63 | self.args = parse_args() 64 | # server 65 | self.listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 66 | self.listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 67 | self.listener.bind((self.args.listen, self.args.port)) 68 | self.listener.listen(self.args.backlog) 69 | if self.args.verbose: 70 | loglevel = logging.DEBUG 71 | else: 72 | loglevel = logging.INFO 73 | # log 74 | logging.basicConfig(level=loglevel, 75 | format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', 76 | datefmt='%m-%d %H:%M', 77 | filename=self.args.log, 78 | filemode='w' 79 | ) 80 | formatter = logging.Formatter('%(levelname)-5s %(message)s') 81 | console = logging.StreamHandler() 82 | console.setFormatter(formatter) 83 | logging.getLogger().addHandler(console) 84 | logging.info(f'Listening on {self.args.listen}:{self.args.port}, backlog={self.args.backlog}') 85 | ### after logging is configured 86 | 87 | self.proxypicker = ProxyPicker(self.args.proxylist) 88 | # threadpool 89 | self.threadpool = ThreadPoolExecutor(max_workers=self.args.backlog, thread_name_prefix='RPS') 90 | # loop 91 | self.running = False 92 | # ctrl-c handling 93 | def signal_handler(signal,frame): 94 | self.stop() 95 | signal.signal(signal.SIGINT, signal_handler) 96 | 97 | def stop(self): 98 | self.running = False 99 | self.listener.close() 100 | self.threadpool.shutdown(wait=True, cancel_futures=True) 101 | logging.info('ProxyServer is closed') 102 | logging.info('All threads are closed') 103 | 104 | 105 | def start(self): 106 | self.running = True 107 | while self.running: 108 | # Accept an incoming connection 109 | try: 110 | client_sock, client_addr = self.listener.accept() 111 | except Exception as e: 112 | if isinstance(e, OSError) and e.errno != 9: 113 | logging.error(f'Error: Failed to accept connection: {e}') 114 | break 115 | logging.debug(f'Accepted connection from {client_addr[0]}:{client_addr[1]}') 116 | 117 | # Get a random proxy 118 | target = self.proxypicker.get_random_proxy() 119 | if target == None: 120 | logging.error(f'Error: No proxy available') 121 | client_sock.close() 122 | break 123 | target_host, target_port = target 124 | 125 | # Create a TCP socket to connect to the target proxy 126 | target_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 127 | try: 128 | target_sock.connect((target_host, target_port)) 129 | except Exception as e: 130 | logging.error(f'Error: Failed to connect to proxy at {target_host}:{target_port}: {e}') 131 | logging.error(f'Error: Removing {target} from proxy pool') 132 | self.proxypicker.proxies.remove(target) 133 | client_sock.close() 134 | continue 135 | 136 | # Forward data between the two sockets, non-deadlockingly 137 | logging.debug(f'Connected established {client_addr[0]}:{client_addr[1]} <-> {target_host}:{target_port}') 138 | self.threadpool.submit(self.handle_client, client_sock, target_sock) 139 | self.threadpool.submit(self.handle_client, target_sock, client_sock) 140 | logging.info('Stopped accepting new connections') 141 | 142 | 143 | def handle_client(self, client_sock: socket.socket, target_sock: socket.socket): 144 | # Handle incoming data from client_sock and forward it to target_sock 145 | while True: 146 | try: 147 | data = client_sock.recv(self.args.bufsize) 148 | except ConnectionError as e: 149 | break 150 | except Exception as e: 151 | logging.error(f'Connection Error: {client_sock}->{target_sock}: {e}') 152 | break 153 | if not data: 154 | break 155 | target_sock.sendall(data) 156 | 157 | # Close both sockets when done 158 | client_sock.close() 159 | target_sock.close() 160 | 161 | if __name__ == '__main__': 162 | gateway = ProxyServer() 163 | gateway.start() 164 | 165 | --------------------------------------------------------------------------------