├── .gitignore ├── GeoLite2-Country.mmdb ├── README.md ├── genkey.sh ├── proxy ├── proxy.py ├── requirements.txt └── rules.conf.default /.gitignore: -------------------------------------------------------------------------------- 1 | env/ 2 | -------------------------------------------------------------------------------- /GeoLite2-Country.mmdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhicheng/proxy/0b51835682a9f5d2f78216f298b193e3fafeb1fb/GeoLite2-Country.mmdb -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How to use 2 | 3 | ## Download code 4 | 5 | $ git clone https://github.com/zhicheng/proxy.git 6 | $ cd proxy 7 | 8 | ## Setup virtualenv and active virtualenv 9 | 10 | $ virtualenv env 11 | $ source env/bin/activate 12 | 13 | ## Install 3rdparty package 14 | 15 | $ pip install -r requirements.txt 16 | 17 | ## Change configure file 18 | 19 | $ cp rules.conf.default rules.conf 20 | 21 | ## Run 22 | 23 | $ ./proxy 24 | 25 | # Configure 26 | 27 | 28 | how many process use. 29 | 30 | workers = 8 31 | 32 | local listen proxy port `socks5` for client use,`socks5s` may not work with your client but for remote upstream will work well. 33 | 34 | socks5_port = 8888 35 | socks5s_port = 8443 36 | 37 | `socks5s` ssl server key and crt,`socks5s` repr socks5 over ssl.`genkey.sh` can generate self-signed crt. 38 | 39 | socks5s_key = 'server.key' 40 | socks5s_crt = 'server.crt' 41 | 42 | default rule set `mode` value 43 | 44 | 1. `pass` passthrough mode 45 | 2. `socks5` use `socks5` proxy upstream 46 | 3. `socks5s` same as above but ssl 47 | 4. `reject` do not proxy anything 48 | 49 | 50 | ``` 51 | default = [{'mode': 'socks5s', 'host': '127.0.0.1', 'port': 7443}] 52 | pass = {'mode': 'pass'} 53 | ``` 54 | 55 | 56 | Authenticate for client,may not work with your client. 57 | 58 | auth = { 59 | 'username': 'password' 60 | } 61 | 62 | country rule set 63 | 64 | country_rules = { 65 | 'cn': pass, 66 | } 67 | 68 | hostname rule set high priority, support wildcard match like `*.example.com` 69 | 70 | hostname_rules = [ 71 | ('127.0.0.1', pass), 72 | ('localhost', pass), 73 | ('10.0.0.0/8', pass), 74 | ('172.16.0.0/12', pass), 75 | ('192.168.0.0/16', pass), 76 | ('*example.com', default), 77 | ] 78 | 79 | # Best Practice 80 | 81 | TODO 82 | 83 | # Bugs 84 | 85 | * DNS Query will block and can be poisoning,current use hostname_rules avoiding make local domain query. 86 | * Slow performance. 87 | 88 | -------------------------------------------------------------------------------- /genkey.sh: -------------------------------------------------------------------------------- 1 | openssl genrsa -des3 -out server.key 1024 2 | openssl req -new -key server.key -out server.csr 3 | cp server.key server.key.org 4 | openssl rsa -in server.key.org -out server.key 5 | openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt 6 | -------------------------------------------------------------------------------- /proxy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | . env/bin/activate 4 | 5 | python proxy.py 6 | -------------------------------------------------------------------------------- /proxy.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | import struct 3 | import socket 4 | import random 5 | import logging 6 | import fnmatch 7 | import ipaddress 8 | import functools 9 | import tornado.gen 10 | import tornado.ioloop 11 | import tornado.netutil 12 | import tornado.options 13 | import tornado.iostream 14 | import tornado.tcpclient 15 | import tornado.tcpserver 16 | import tornado.autoreload 17 | 18 | import geoip2.database 19 | geoip = geoip2.database.Reader('GeoLite2-Country.mmdb') 20 | 21 | logging.basicConfig(format="%(asctime)-15s %(message)s") 22 | 23 | class SOCKS5(object): 24 | VER = 5 25 | RSV = 0 26 | 27 | REP_SUCCESSED = 0 28 | REP_SERVERFAIL = 1 29 | REP_NOTALLOWED = 2 30 | REP_NETWORKFAIL = 3 31 | REP_HOSTFAIL = 4 32 | REP_CONNFAIL = 5 33 | REP_TTLEXPIRED = 6 34 | REP_NOTSUPPORTED = 7 35 | REP_ADDRESSERROR = 8 36 | 37 | METHOD_NOAUTH = 0 38 | METHOD_GSSAPI = 1 39 | METHOD_USERNAME = 2 40 | METHOD_NOACCEPT = 0xFF 41 | 42 | ATYP_IPV4 = 1 43 | ATYP_DOMAINNAME = 3 44 | ATYP_IPV6 = 4 45 | 46 | CMD_CONNECT = 1 47 | CMD_BIND = 2 48 | CMD_UDP = 3 49 | 50 | 51 | class ProxyConnection(object): 52 | 53 | def __init__(self, client, address, server): 54 | self.server = server 55 | self.address = address 56 | self.resolver = server.resolver 57 | 58 | self.handshake(client) 59 | 60 | def remote_close_callback(self): 61 | if not self.client.writing(): 62 | self.client.close() 63 | 64 | if self.client.closed(): 65 | logging.info("close %s:%d -> %s:%d by remote", 66 | self.address[0], self.address[1], self.remote_addr[0], self.remote_addr[1]) 67 | 68 | if self.address in self.server.connection: 69 | del self.server.connection[self.address] 70 | 71 | def client_close_callback(self): 72 | if not self.remote.writing(): 73 | self.remote.close() 74 | 75 | if self.remote.closed(): 76 | logging.info("close %s:%d -> %s:%d by client", 77 | self.address[0], self.address[1], self.remote_addr[0], self.remote_addr[1]) 78 | 79 | if self.address in self.server.connection: 80 | del self.server.connection[self.address] 81 | 82 | @tornado.gen.coroutine 83 | def client_recv(self, data): 84 | if not self.remote.closed(): 85 | try: 86 | yield self.remote.write(data) 87 | except Exception as e: 88 | logging.exception('remote_write') 89 | if self.client.closed(): 90 | self.remote.close() 91 | else: 92 | self.client.close() 93 | 94 | @tornado.gen.coroutine 95 | def remote_recv(self, data): 96 | if not self.client.closed(): 97 | try: 98 | yield self.client.write(data) 99 | except Exception as e: 100 | logging.exception('client_write') 101 | if self.remote.closed(): 102 | self.client.close() 103 | else: 104 | self.remote.close() 105 | 106 | @tornado.gen.coroutine 107 | def upstream(self, client, atyp, dstaddr, dstport): 108 | ip = '0.0.0.0' 109 | rule = {} 110 | matched = False 111 | for pat, val in tornado.options.options.hostname_rules: 112 | if fnmatch.fnmatch(dstaddr, pat): 113 | rule = val 114 | matched = True 115 | break 116 | elif atyp == SOCKS5.ATYP_IPV4 or atyp == SOCKS5.ATYP_IPV6: 117 | try: 118 | if ipaddress.ip_address(unicode(dstaddr)) in ipaddress.ip_network(unicode(pat)): 119 | rule = val 120 | matched = True 121 | break 122 | except Exception: 123 | pass 124 | 125 | if not matched: 126 | if atyp == SOCKS5.ATYP_IPV4: 127 | ip = dstaddr 128 | elif atyp == SOCKS5.ATYP_DOMAINNAME: 129 | try: 130 | addr = yield self.resolver.resolve(dstaddr, dstport) 131 | ip = addr[0][1][0] 132 | except Exception as e: 133 | ip = None 134 | logging.exception('resolve') 135 | rule = tornado.options.options.default 136 | if ip != '0.0.0.0' and ip != None: 137 | rule = tornado.options.options.default 138 | cc = geoip.country(ip).country.iso_code 139 | if cc: 140 | cc = cc.lower() 141 | rule = tornado.options.options.country_rules.get(cc, rule) 142 | if isinstance(rule, (list, tuple)) and rule: 143 | rule = rule[random.randint(0, len(rule))-1] 144 | 145 | mode = rule.get('mode', 'pass').lower() 146 | host = rule.get('host', None) 147 | port = rule.get('port', None) 148 | 149 | if mode == 'pass': 150 | logging.info('pass %s:%d -> %s:%d', 151 | self.address[0], self.address[1], dstaddr, dstport) 152 | 153 | stream = yield tornado.tcpclient.TCPClient().connect(dstaddr, dstport) 154 | data = struct.pack('!BBBB', 155 | SOCKS5.VER, SOCKS5.REP_SUCCESSED, SOCKS5.RSV, SOCKS5.ATYP_IPV4) 156 | data += socket.inet_aton(ip) + struct.pack('!H', dstport) 157 | client.write(data) 158 | elif mode == 'reject': 159 | logging.info('reject %s:%d -> %s:%d', 160 | self.address[0], self.address[1], dstaddr, dstport) 161 | client.write( 162 | '''HTTP/1.1 503 Service Unavailable\r\n''' 163 | '''Content-Length: 28\r\n''' 164 | '''Server: nginx/2.0\r\n''' 165 | '''Content-Type: text/plain\r\n\r\n''' 166 | '''Proxy Error (Reject by rule)''' 167 | ) 168 | client.close() 169 | elif mode == 'socks5' or mode == 'socks5s': 170 | logging.info('proxy %s:%d -> %s:%d via %s:%d', 171 | self.address[0], self.address[1], dstaddr, dstport, host, port) 172 | 173 | if mode == 'socks5s': 174 | stream = yield tornado.tcpclient.TCPClient().connect(host, port, 175 | ssl_options=dict(cert_reqs=ssl.CERT_NONE)) 176 | else: 177 | stream = yield tornado.tcpclient.TCPClient().connect(host, port) 178 | 179 | if rule.get('username', None): 180 | stream.write( 181 | struct.pack('BBBB', 182 | SOCKS5.VER, 183 | 2, 184 | SOCKS5.METHOD_NOAUTH, 185 | SOCKS5.METHOD_USERNAME 186 | )) 187 | else: 188 | stream.write( 189 | struct.pack('BBB', 190 | SOCKS5.VER, 191 | 1, 192 | SOCKS5.METHOD_NOAUTH, 193 | )) 194 | 195 | data = yield stream.read_bytes(2) 196 | ver, method = struct.unpack('BB', data) 197 | 198 | if method == SOCKS5.METHOD_USERNAME: 199 | data = struct.pack('BB', 1, len(rule.get('username', ''))) 200 | data += rule.get('username', '') 201 | data += struct.pack('B', len(rule.get('password', ''))) 202 | data += rule.get('password', '') 203 | stream.write(data) 204 | data = yield stream.read_bytes(2) 205 | ver, status = struct.unpack('BB', data) 206 | if status != 0: 207 | stream.close() 208 | raise tornado.gen.Return(None) 209 | elif method == SOCKS5.METHOD_NOAUTH: 210 | pass 211 | else: 212 | raise tornado.gen.Return(None) 213 | 214 | request = struct.pack('!BBBB', 215 | SOCKS5.VER, 216 | SOCKS5.CMD_CONNECT, 217 | SOCKS5.RSV, 218 | atyp, 219 | ) 220 | if atyp == SOCKS5.ATYP_IPV4: 221 | request += socket.inet_aton(dstaddr) + struct.pack('!H', dstport) 222 | elif atyp == SOCKS5.ATYP_DOMAINNAME: 223 | request += struct.pack('B', len(dstaddr)) + \ 224 | dstaddr + struct.pack('!H', dstport) 225 | stream.write(request) 226 | else: 227 | raise tornado.gen.Return(None) 228 | 229 | raise tornado.gen.Return(stream) 230 | 231 | @tornado.gen.coroutine 232 | def handshake(self, client): 233 | 234 | data = yield client.read_bytes(2) 235 | ver, nmethods = struct.unpack('BB', data) 236 | if ver != SOCKS5.VER: 237 | client.write( 238 | '''HTTP/1.1 200 OK\r\n''' 239 | '''Content-Length: 10\r\n''' 240 | '''Server: nginx/2.0\r\n''' 241 | '''Content-Type: text/plain\r\n\r\n''' 242 | '''HelloWorld''' 243 | ) 244 | client.close() 245 | return 246 | 247 | methods = yield client.read_bytes(nmethods) 248 | if tornado.options.options.auth: 249 | if struct.pack('B', SOCKS5.METHOD_USERNAME) not in methods: 250 | client.write(struct.pack('BB', SOCKS5.VER, SOCKS5.METHOD_NOACCEPT)) 251 | client.close() 252 | return 253 | 254 | client.write(struct.pack('BB', SOCKS5.VER, SOCKS5.METHOD_USERNAME)) 255 | ver = yield client.read_bytes(1) 256 | 257 | data = yield client.read_bytes(1) 258 | ulen = struct.unpack('B', data)[0] 259 | uname = yield client.read_bytes(ulen) 260 | 261 | data = yield client.read_bytes(1) 262 | plen = struct.unpack('B', data)[0] 263 | passwd = yield client.read_bytes(plen) 264 | 265 | if tornado.options.options.auth.get(uname, None) == passwd: 266 | client.write(struct.pack('BB', 1, 0)) 267 | else: 268 | client.write(struct.pack('BB', 1, 1)) 269 | client.close() 270 | return 271 | else: 272 | client.write(struct.pack('BB', SOCKS5.VER, SOCKS5.METHOD_NOAUTH)) 273 | 274 | data = yield client.read_bytes(4) 275 | 276 | ver, cmd, rsv, atyp = struct.unpack('BBBB', data) 277 | if atyp == SOCKS5.ATYP_IPV4: 278 | data = yield client.read_bytes(4) 279 | dstaddr = socket.inet_ntoa(data) 280 | 281 | data = yield client.read_bytes(2) 282 | dstport = struct.unpack('!H', data)[0] 283 | elif atyp == SOCKS5.ATYP_DOMAINNAME: 284 | data = yield client.read_bytes(1) 285 | dstaddr = yield client.read_bytes(struct.unpack('B', data)[0]) 286 | 287 | data = yield client.read_bytes(2) 288 | dstport = struct.unpack('!H', data)[0] 289 | elif atyp == SOCKS5.ATYP_IPV6: 290 | client.write( 291 | struct.pack('!BBBBIH', 292 | SOCKS5.VER, 293 | SOCKS5.REP_ADDRESSERROR, 294 | SOCKS5.RSV, 295 | SOCKS5.ATYP_IPV4, 0, 0 296 | )) 297 | client.close() 298 | return 299 | else: 300 | client.write( 301 | struct.pack('!BBBBIH', 302 | SOCKS5.VER, 303 | SOCKS5.REP_ADDRESSERROR, 304 | SOCKS5.RSV, 305 | SOCKS5.ATYP_IPV4, 0, 0 306 | )) 307 | client.close() 308 | return 309 | 310 | try: 311 | remote = yield self.upstream(client, atyp, dstaddr, dstport) 312 | except Exception as e: 313 | remote = None 314 | 315 | if remote is None: 316 | client.close() 317 | return 318 | 319 | self.client = client 320 | self.remote = remote 321 | 322 | client.set_nodelay(True) 323 | remote.set_nodelay(True) 324 | 325 | client.read_until_close(streaming_callback=self.client_recv) 326 | remote.read_until_close(streaming_callback=self.remote_recv) 327 | 328 | client.set_close_callback(self.client_close_callback) 329 | remote.set_close_callback(self.remote_close_callback) 330 | 331 | self.remote_addr = (dstaddr, dstport) 332 | 333 | class ProxyServer(tornado.tcpserver.TCPServer): 334 | 335 | def handle_stream(self, stream, address): 336 | self.connection[address] = ProxyConnection(stream, address, server=self) 337 | 338 | def main(): 339 | tornado.options.define("workers", default=1) 340 | tornado.options.define("auth", default={}) 341 | tornado.options.define("default", default=[{'mode': 'pass'}]) 342 | 343 | tornado.options.define("socks5_port", default=8888) 344 | tornado.options.define("socks5s_port", default=8443) 345 | tornado.options.define("socks5s_key", default="server.key") 346 | tornado.options.define("socks5s_crt", default="server.crt") 347 | 348 | tornado.options.define("country_rules", default={}) 349 | tornado.options.define("hostname_rules", default=[]) 350 | 351 | tornado.options.define("config", default='rules.conf') 352 | 353 | tornado.options.parse_command_line() 354 | tornado.options.parse_config_file(tornado.options.options.config) 355 | 356 | socks5_sockets = tornado.netutil.bind_sockets(tornado.options.options.socks5_port) 357 | for s in socks5_sockets: 358 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 359 | if tornado.options.options.socks5s_port: 360 | socks5s_sockets = tornado.netutil.bind_sockets(tornado.options.options.socks5s_port) 361 | for s in socks5s_sockets: 362 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 363 | tornado.process.fork_processes(tornado.options.options.workers) 364 | 365 | socks5_server = ProxyServer() 366 | socks5_server.connection = {} 367 | socks5_server.add_sockets(socks5_sockets) 368 | socks5_server.resolver = tornado.netutil.Resolver() 369 | 370 | if tornado.options.options.socks5s_port: 371 | ssl_options = { 372 | 'keyfile': tornado.options.options.socks5s_key, 373 | 'certfile': tornado.options.options.socks5s_crt, 374 | } 375 | socks5s_server = ProxyServer(ssl_options=ssl_options) 376 | socks5s_server.connection = {} 377 | socks5s_server.add_sockets(socks5s_sockets) 378 | socks5s_server.resolver = tornado.netutil.Resolver() 379 | 380 | tornado.netutil.Resolver.configure('tornado.netutil.ThreadedResolver', num_threads=10) 381 | 382 | tornado.autoreload.watch(tornado.options.options.config) 383 | tornado.autoreload.start() 384 | 385 | tornado.ioloop.IOLoop.current().start() 386 | 387 | if __name__ == '__main__': 388 | main() 389 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | backports.ssl-match-hostname==3.4.0.2 2 | certifi==2015.4.28 3 | futures==3.0.3 4 | geoip2==2.2.0 5 | ipaddr==2.1.11 6 | ipaddress==1.0.14 7 | maxminddb==1.2.0 8 | requests==2.7.0 9 | tornado==4.2.1 10 | virtualenv==13.1.2 11 | -------------------------------------------------------------------------------- /rules.conf.default: -------------------------------------------------------------------------------- 1 | workers = 8 2 | 3 | socks5_port = 8888 4 | socks5s_port = 8443 5 | socks5s_key = 'server.key' 6 | socks5s_crt = 'server.crt' 7 | 8 | default = [{'mode': 'socks5s', 'host': '127.0.0.1', 'port': 7443}] 9 | pass = {'mode': 'pass'} 10 | 11 | auth = { 12 | #'username': 'password' # should not add this for client use Chrome,Safari,Firefox all handle this wrong 13 | } 14 | 15 | country_rules = { 16 | 'cn': {'mode': 'pass'}, 17 | } 18 | 19 | hostname_rules = [ 20 | ('127.0.0.1', pass), 21 | ('localhost', pass), 22 | ('192.168.0.0/16', pass), 23 | ('10.0.0.0/8', pass), 24 | ('172.16.0.0/12', pass), 25 | ('*example.com', default), 26 | ] 27 | --------------------------------------------------------------------------------