├── .coveragerc ├── .github └── workflows │ └── pythonpackage.yml ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── iptables_redir.sh ├── iptables_tproxy.sh ├── legacy └── shadowproxy_v0_2_5.py ├── setup.cfg ├── setup.py ├── shadowproxy ├── __init__.py ├── __main__.py ├── ciphers.py ├── gvars.py ├── plugins │ ├── __init__.py │ ├── base.py │ ├── http_simple.py │ ├── tls1_2.py │ └── tls_parser.py ├── protocols │ ├── __init__.py │ ├── exceptions.py │ ├── http.py │ ├── socks4.py │ └── socks5.py ├── proxies │ ├── __init__.py │ ├── aead │ │ ├── __init__.py │ │ ├── client.py │ │ ├── parser.py │ │ └── server.py │ ├── base │ │ ├── __init__.py │ │ ├── client.py │ │ ├── server.py │ │ ├── udpclient.py │ │ └── udpserver.py │ ├── http │ │ ├── __init__.py │ │ ├── client.py │ │ └── server.py │ ├── shadowsocks │ │ ├── __init__.py │ │ ├── client.py │ │ ├── parser.py │ │ ├── server.py │ │ ├── udpclient.py │ │ └── udpserver.py │ ├── socks │ │ ├── __init__.py │ │ ├── client.py │ │ └── server.py │ ├── transparent │ │ ├── __init__.py │ │ ├── server.py │ │ └── udpserver.py │ └── tunnel │ │ ├── __init__.py │ │ └── udpserver.py └── utils.py └── tests ├── test_ciphers.py ├── test_cli.py ├── test_http_request.py ├── test_main.py ├── test_protocols.py └── test_udp.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = shadowproxy 3 | 4 | [report] 5 | include = 6 | shadowproxy/*.py 7 | tests/*.py 8 | omit = 9 | shadowproxy/proxies/transparent/server.py 10 | shadowproxy/proxies/transparent/udpserver.py 11 | -------------------------------------------------------------------------------- /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: [3.6, 3.7, 3.8] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v1 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install codecov 30 | - name: Lint with flake8 31 | run: | 32 | pip install flake8 33 | # stop the build if there are Python syntax errors or undefined names 34 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 35 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 36 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 37 | - name: Black Code Formatter 38 | uses: lgeiger/black-action@v1.0.1 39 | - name: Test with pytest 40 | run: | 41 | pip install pytest 42 | python setup.py test 43 | - name: Upload coverage to Codecov 44 | uses: codecov/codecov-action@v1 45 | with: 46 | file: ./coverage.xml 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | .pytest_cache/ 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | sudo: required 4 | python: 5 | - 3.6 6 | - 3.7 7 | before_install: 8 | - sudo sh iptables_redir.sh 9 | install: 10 | - python setup.py install 11 | - pip install codecov 12 | script: 13 | - python setup.py test 14 | # after_success: 15 | # - codecov 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | 3 | WORKDIR /app/ShadowProxy 4 | 5 | ADD . . 6 | 7 | RUN python setup.py install 8 | 9 | RUN rm -rf /app/ShadowProxy 10 | 11 | ENTRYPOINT ["/usr/local/bin/shadowproxy"] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 guyingbo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shadowproxy 2 | 3 | ![Python package](https://github.com/guyingbo/shadowproxy/workflows/Python%20package/badge.svg?branch=master) 4 | [![Build Status](https://travis-ci.org/guyingbo/shadowproxy.svg?branch=master)](https://travis-ci.org/guyingbo/shadowproxy) 5 | [![Python Version](https://img.shields.io/pypi/pyversions/shadowproxy.svg)](https://pypi.python.org/pypi/shadowproxy) 6 | [![Version](https://img.shields.io/pypi/v/shadowproxy.svg)](https://pypi.python.org/pypi/shadowproxy) 7 | [![Format](https://img.shields.io/pypi/format/shadowproxy.svg)](https://pypi.python.org/pypi/shadowproxy) 8 | [![License](https://img.shields.io/pypi/l/shadowproxy.svg)](https://pypi.python.org/pypi/shadowproxy) 9 | [![Code Coverage](https://codecov.io/gh/guyingbo/shadowproxy/branch/master/graph/badge.svg)](https://codecov.io/gh/guyingbo/shadowproxy) 10 | [![Lines Of Code](https://tokei.rs/b1/github/guyingbo/shadowproxy?category=code)](https://github.com/guyingbo/shadowproxy) 11 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 12 | 13 | 14 | ## Introduction 15 | 16 | A proxy server that implements Socks5/Shadowsocks/Redirect/HTTP (tcp) and Shadowsocks/TProxy/Tunnel (udp) protocols. 17 | 18 | Thanks to Dabeaz's awesome [curio](https://github.com/dabeaz/curio) project. 19 | 20 | This project is inspired by qwj's [python-proxy](https://github.com/qwj/python-proxy) project. 21 | 22 | It is a replacement of shadowsocks and shadowsocks-libev, you can replace ss-redir, ss-tunnel, ss-server, ss-local with just one shadowproxy command. 23 | 24 | ## Installation 25 | 26 | shadowproxy requires Python3.6+ 27 | 28 | install with pip 29 | 30 | ``` 31 | pip3 install shadowproxy 32 | ``` 33 | 34 | or run with docker, for example: 35 | 36 | ``` 37 | docker run -it --rm -p 8000:8527 tensiongyb/shadowproxy -vv socks://:8527 38 | ``` 39 | 40 | ## Features 41 | 42 | ### supported protocols 43 | 44 | protocol | server | client | scheme 45 | --- | --- | --- | --- 46 | socks5 | ✓ | ✓ | socks:// 47 | socks4 | ✓ | ✓ | socks4:// 48 | ss | ✓ | ✓ | ss:// 49 | ss aead | ✓ | ✓ | ss:// 50 | http connect | ✓ | ✓ | http:// 51 | http forward | | ✓ | forward:// 52 | transparent proxy | ✓ | | red:// 53 | tunnel(udp) | ✓ | | tunneludp:// 54 | ss(udp) | ✓ | ✓ | ssudp:// 55 | 56 | ### supported plugins 57 | 58 | plugin | server | client 59 | --- | --- | --- 60 | http_simple | ✓ | ✓ 61 | tls1.2_ticket_auth | ✓ | ✓ 62 | 63 | ### supported ciphers 64 | 65 | * aes-256-cfb 66 | * aes-128-cfb 67 | * aes-192-cfb 68 | * chacha20 69 | * salsa20 70 | * rc4 71 | * chacha20-ietf-poly1305 72 | * aes-256-gcm 73 | * aes-192-gcm 74 | * aes-128-gcm 75 | 76 | ### other features 77 | 78 | * support both IPv4 and IPv6 79 | 80 | Here are some ipv6 url examples: 81 | 82 | ``` 83 | http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html 84 | http://[1080:0:0:0:8:800:200C:417A]/index.html 85 | http://[3ffe:2a00:100:7031::1] 86 | http://[1080::8:800:200C:417A]/foo 87 | http://[::192.9.5.5]/ipng 88 | http://[::FFFF:129.144.52.38]:80/index.html 89 | http://[2010:836B:4179::836B:4179] 90 | ``` 91 | 92 | ## Usage 93 | 94 | ``` 95 | usage: shadowproxy [-h] [-v] [--version] server [server ...] 96 | 97 | uri syntax: 98 | 99 | {scheme}://[{userinfo}@][hostname]:{port}[/?[plugin={p;args}][via={uri}][target={t}][source_ip={ip}]][#{fragment}] 100 | 101 | userinfo = cipher:password or base64(cipher:password) when scheme is ss, ssudp 102 | userinfo = username:password or base64(username:password) when scheme is socks, http. 103 | 104 | ``` 105 | 106 | examples: 107 | 108 | ``` 109 | # simple shadowsocks server 110 | shadowproxy ss://chacha20:password@0.0.0.0:8888 111 | 112 | # ipv6 binding 113 | shadowproxy ss://chacha20:password@[::]:8888 114 | 115 | # socks5 --> shadowsocks 116 | shadowproxy -v socks://:8527/?via=ss://aes-256-cfb:password@127.0.0.1:8888 117 | 118 | # http --> shadowsocks 119 | shadowproxy -v http://:8527/?via=ss://aes-256-cfb:password@127.0.0.1:8888 120 | 121 | # redir --> shadowsocks 122 | shadowproxy -v red://:12345/?via=ss://aes-256-cfb:password@127.0.0.1:8888 123 | 124 | # shadowsocks server (udp) 125 | shadowproxy -v ssudp://aes-256-cfb:password@:8527 126 | 127 | # tunnel --> shadowsocks (udp) 128 | shadowproxy -v tunneludp://:8527/?target=8.8.8.8:53&via=ssudp://aes-256-cfb:password@127.0.0.1:8888 129 | 130 | # tproxy --> shadowsocks (udp) 131 | shadowproxy -v tproxyudp://:8527/?via=ssudp://aes-256-cfb:password@127.0.0.1:8888 132 | ``` 133 | -------------------------------------------------------------------------------- /iptables_redir.sh: -------------------------------------------------------------------------------- 1 | # 创建一个 nat 类型的新表,然后插入到prerouting链中。 2 | iptables -t nat -N shadowsocks 3 | # iptables -t nat -A shadowsocks -d {your_ip} -j RETURN 4 | 5 | iptables -t nat -A shadowsocks -d 0.0.0.0/8 -j RETURN 6 | iptables -t nat -A shadowsocks -d 10.0.0.0/8 -j RETURN 7 | iptables -t nat -A shadowsocks -d 127.0.0.0/8 -j RETURN 8 | iptables -t nat -A shadowsocks -d 169.254.0.0/16 -j RETURN 9 | iptables -t nat -A shadowsocks -d 172.16.0.0/12 -j RETURN 10 | iptables -t nat -A shadowsocks -d 192.168.0.0/16 -j RETURN 11 | iptables -t nat -A shadowsocks -d 224.0.0.0/4 -j RETURN 12 | iptables -t nat -A shadowsocks -d 240.0.0.0/4 -j RETURN 13 | iptables -t nat -A shadowsocks -p tcp -j REDIRECT --to-ports 12345 14 | # 12345 是 shadowsocks 的默认监听端口 15 | # 将目的地为1.1.1.1的tcp流量应用shadowsocks表的规则 16 | iptables -t nat -I PREROUTING -p tcp -d 1.1.1.1/32 -j shadowsocks 17 | # iptables -t nat -I PREROUTING -p tcp -j shadowsocks 18 | # 在 PREROUTING 链前部插入 shadowsocks 表,使其生效 19 | -------------------------------------------------------------------------------- /iptables_tproxy.sh: -------------------------------------------------------------------------------- 1 | ip rule add fwmark 0x01/0x01 lookup 100 2 | ip route add local 0.0.0.0/0 dev lo table 100 3 | iptables -t mangle -A PREROUTING -d {remote_ip} -p udp -j RETURN 4 | iptables -t mangle -A PREROUTING -d 0.0.0.0/8 -p udp -j RETURN 5 | iptables -t mangle -A PREROUTING -d 10.0.0.0/8 -p udp -j RETURN 6 | iptables -t mangle -A PREROUTING -d 127.0.0.0/8 -p udp -j RETURN 7 | iptables -t mangle -A PREROUTING -d 169.254.0.0/16 -p udp -j RETURN 8 | iptables -t mangle -A PREROUTING -d 172.16.0.0/12 -p udp -j RETURN 9 | iptables -t mangle -A PREROUTING -d 192.168.0.0/16 -p udp -j RETURN 10 | iptables -t mangle -A PREROUTING -d 224.0.0.0/4 -p udp -j RETURN 11 | iptables -t mangle -A PREROUTING -d 240.0.0.0/4 -p udp -j RETURN 12 | iptables -t mangle -A PREROUTING -p udp -j TPROXY --tproxy-mark 0x01/0x01 --on-port 12345 13 | #iptables -t mangle -A OUTPUT -p udp --dport 53 -j MARK --set-mark 1 14 | -------------------------------------------------------------------------------- /legacy/shadowproxy_v0_2_5.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.6 2 | """ 3 | An universal proxy server/client which support 4 | Socks5/HTTP/Shadowsocks/Redirect (tcp) and 5 | Shadowsocks/TProxy/Tunnel (udp) protocols. 6 | 7 | uri syntax: {local_scheme}://[cipher:password@]{netloc}[#fragment] 8 | [{=remote_scheme}://[cipher:password@]{netloc}] 9 | support tcp schemes: 10 | local_scheme: socks, ss, red, http, https 11 | remote_scheme: ss 12 | support udp schemes: 13 | local_scheme: ssudp, tproxyudp, tunneludp 14 | remote_scheme: ssudp 15 | 16 | examples: 17 | # http(s) proxy 18 | shadowproxy -v http://:8527 19 | 20 | # socks5 --> shadowsocks 21 | shadowproxy -v socks://:8527=ss://aes-256-cfb:password@127.0.0.1:8888 22 | 23 | # http --> shadowsocks 24 | shadowproxy -v http://:8527=ss://aes-256-cfb:password@127.0.0.1:8888 25 | 26 | # redir --> shadowsocks 27 | shadowproxy -v red://:12345=ss://aes-256-cfb:password@127.0.0.1:8888 28 | 29 | # shadowsocks server (tcp) 30 | shadowproxy -v ss://aes-256-cfb:password@:8888 31 | 32 | # shadowsocks server (udp) 33 | shadowproxy -v ssudp://aes-256-cfb:password@:8527 34 | 35 | # tunnel --> shadowsocks (udp) 36 | shadowproxy -v \ 37 | tunneludp://:8527#8.8.8.8:53=ssudp://aes-256-cfb:password@127.0.0.1:8888 38 | 39 | # tproxy --> shadowsocks (udp) 40 | sudo shadowproxy -v \ 41 | tproxyudp://:8527=ssudp://aes-256-cfb:password@127.0.0.1:8888 42 | """ 43 | import argparse 44 | import base64 45 | import ipaddress 46 | import os 47 | import re 48 | import resource 49 | import signal 50 | import struct 51 | import sys 52 | import traceback 53 | import types 54 | import urllib.parse 55 | import weakref 56 | from functools import partial 57 | from hashlib import md5 58 | 59 | import curio 60 | import httptools 61 | from Crypto import Random 62 | from Crypto.Cipher import AES, ARC4, ChaCha20, Salsa20 63 | from curio import CancelledError, TaskGroup, socket, spawn, ssl, tcp_server 64 | from curio.signal import SignalEvent 65 | from microstats import MicroStats 66 | 67 | __version__ = "0.2.5" 68 | SO_ORIGINAL_DST = 80 69 | IP_TRANSPARENT = 19 70 | IP_ORIGDSTADDR = 20 71 | IP_RECVORIGDSTADDR = IP_ORIGDSTADDR 72 | # SOL_IPV6 = 41 73 | # IPV6_ORIGDSTADDR = 74 74 | # IPV6_RECVORIGDSTADDR = IPV6_ORIGDSTADDR 75 | verbose = 0 76 | remote_num = 0 77 | print = partial(print, flush=True) 78 | local_networks = [ 79 | "0.0.0.0/8", 80 | "10.0.0.0/8", 81 | "127.0.0.0/8", 82 | "169.254.0.0/16", 83 | "172.16.0.0/12", 84 | "192.168.0.0/16", 85 | "224.0.0.0/4", 86 | "240.0.0.0/4", 87 | ] 88 | local_networks = [ipaddress.ip_network(s) for s in local_networks] 89 | # HTTP_HEADER = re.compile('([^ ]+) +(.+?) +(HTTP/[^ ]+)') 90 | HTTP_LINE = re.compile(b"([^ ]+) +(.+?) +(HTTP/[^ ]+)") 91 | stats = MicroStats() 92 | 93 | 94 | def is_local(host): 95 | try: 96 | address = ipaddress.ip_address(host) 97 | except ValueError: 98 | return False 99 | return any(address in nw for nw in local_networks) 100 | 101 | 102 | def pack_addr(addr): 103 | host, port = addr 104 | try: # IPV4 105 | packed = b"\x01" + socket.inet_aton(host) 106 | except OSError: 107 | try: # IPV6 108 | packed = b"\x04" + socket.inet_pton(socket.AF_INET6, host) 109 | except OSError: # hostname 110 | packed = host.encode("ascii") 111 | packed = b"\x03" + len(packed).to_bytes(1, "big") + packed 112 | return packed + port.to_bytes(2, "big") 113 | 114 | 115 | def unpack_addr(data, start=0): 116 | atyp = data[start] 117 | if atyp == 1: # IPV4 118 | end = start + 5 119 | ipv4 = data[start + 1 : end] 120 | host = socket.inet_ntoa(ipv4) 121 | elif atyp == 4: # IPV6 122 | end = start + 17 123 | ipv6 = data[start:end] 124 | host = socket.inet_ntop(socket.AF_INET6, ipv6) 125 | elif atyp == 3: # hostname 126 | length = data[start + 1] 127 | end = start + 2 + length 128 | host = data[start + 2 : end].decode("ascii") 129 | else: 130 | raise Exception(f"unknow atyp: {atyp}") 131 | port = int.from_bytes(data[end : end + 2], "big") 132 | return (host, port), data[end + 2 :] 133 | 134 | 135 | readfunc = Random.new().read 136 | 137 | 138 | class BaseCipher: 139 | def get_key(self, password: bytes, salt: bytes = b""): 140 | keybuf = [] 141 | while len(b"".join(keybuf)) < self.KEY_LENGTH: 142 | keybuf.append( 143 | md5((keybuf[-1] if keybuf else b"") + password + salt).digest() 144 | ) 145 | return b"".join(keybuf)[: self.KEY_LENGTH] 146 | 147 | def __init__(self, password, iv=None): 148 | self.key = self.get_key(password) 149 | self.iv = iv if iv is not None else readfunc(self.IV_LENGTH) 150 | self.setup() 151 | 152 | def decrypt(self, data): 153 | return self.cipher.decrypt(data) 154 | 155 | def encrypt(self, data): 156 | return self.cipher.encrypt(data) 157 | 158 | def setup(self): 159 | pass 160 | 161 | 162 | class AES256CFBCipher(BaseCipher): 163 | KEY_LENGTH = 32 164 | IV_LENGTH = 16 165 | 166 | def setup(self): 167 | self.cipher = AES.new(self.key, mode=AES.MODE_CFB, iv=self.iv, segment_size=128) 168 | 169 | 170 | class AES128CFBCipher(AES256CFBCipher): 171 | KEY_LENGTH = 16 172 | 173 | 174 | class AES192CFBCipher(AES256CFBCipher): 175 | KEY_LENGTH = 24 176 | 177 | 178 | class ChaCha20Cipher(BaseCipher): 179 | KEY_LENGTH = 32 180 | IV_LENGTH = 8 181 | 182 | def setup(self): 183 | self.cipher = ChaCha20.new(key=self.key, nonce=self.iv) 184 | 185 | 186 | class Salsa20Cipher(BaseCipher): 187 | KEY_LENGTH = 32 188 | IV_LENGTH = 8 189 | 190 | def setup(self): 191 | self.cipher = Salsa20.new(key=self.key, nonce=self.iv) 192 | 193 | 194 | class RC4Cipher(BaseCipher): 195 | KEY_LENGTH = 16 196 | IV_LENGTH = 0 197 | 198 | def setup(self): 199 | self.cipher = ARC4.new(self.key) 200 | 201 | 202 | class ServerBase: 203 | def __repr__(self): 204 | s = f"{self.laddr[0]}:{self.laddr[1]} --> {self.__proto__}" 205 | if getattr(self, "via_client", None): 206 | s += f" --> {self.via_client.raddr[0]}:{self.via_client.raddr[1]}" 207 | if hasattr(self, "taddr"): 208 | target_host, target_port = self.taddr 209 | else: 210 | target_host, target_port = "unknown", -1 211 | s += f" --> {target_host}:{target_port}" 212 | return s 213 | 214 | @property 215 | def __proto__(self): 216 | proto = self.__class__.__name__[:-10] 217 | if getattr(self, "command", None) == "associate": 218 | proto += "(UDP)" 219 | return proto 220 | 221 | def setup(self, stream, addr): 222 | self._stream = stream 223 | self.laddr = addr 224 | 225 | async def __call__(self, client, addr): 226 | try: 227 | async with client: 228 | self.setup(client.as_stream(), addr) 229 | async with self._stream: 230 | await self.interact() 231 | except Exception as e: 232 | if verbose > 0: 233 | print(f"{self} error: {e}") 234 | if verbose > 1: 235 | traceback.print_exc() 236 | 237 | async def interact(self): 238 | raise NotImplemented 239 | 240 | async def connect_remote(self): 241 | global remote_num 242 | remote_num += 1 243 | if getattr(self, "via", None): 244 | self.via_client = self.via() 245 | if verbose > 0: 246 | print(f"tcp: {self}") 247 | remote_conn = await self.via_client.connect() 248 | else: 249 | self.via_client = None 250 | if verbose > 0: 251 | print(f"tcp: {self}") 252 | remote_conn = await curio.open_connection(*self.taddr) 253 | return remote_conn 254 | 255 | def on_disconnect_remote(self): 256 | global remote_num 257 | remote_num -= 1 258 | return 259 | if getattr(self, "via", None): 260 | if verbose > 0: 261 | print(f"Disconnect {self}") 262 | else: 263 | if verbose > 0: 264 | print(f"Disconnect {self}") 265 | 266 | async def get_remote_stream(self, remote_conn): 267 | if self.via_client: 268 | remote_stream = self.via_client.as_stream(remote_conn) 269 | try: 270 | await self.via_client.init(self._stream, remote_stream, self.taddr) 271 | except Exception: 272 | await remote_stream.close() 273 | raise 274 | else: 275 | remote_stream = remote_conn.as_stream() 276 | return remote_stream 277 | 278 | async def relay(self, remote_stream): 279 | t1 = await spawn(self._relay(self._stream, remote_stream)) 280 | t2 = await spawn(self._relay2(remote_stream, self._stream)) 281 | try: 282 | async with TaskGroup([t1, t2]) as g: 283 | task = await g.next_done() 284 | await task.join() 285 | await g.cancel_remaining() 286 | except CancelledError: 287 | pass 288 | 289 | async def _relay(self, rstream, wstream): 290 | try: 291 | while True: 292 | data = await rstream.read() 293 | if not data: 294 | return 295 | await wstream.write(data) 296 | stats.incr("traffic", len(data)) 297 | except CancelledError: 298 | pass 299 | except Exception as e: 300 | if verbose > 0: 301 | print(f"{self} error: {e}") 302 | if verbose > 1: 303 | traceback.print_exc() 304 | 305 | _relay2 = _relay 306 | 307 | async def read_addr(self): 308 | atyp = await self._stream.read_exactly(1) 309 | if atyp == b"\x01": # IPV4 310 | data = await self._stream.read_exactly(4) 311 | host = socket.inet_ntoa(data) 312 | elif atyp == b"\x04": # IPV6 313 | data = await self._stream.read_exactly(16) 314 | host = socket.inet_ntop(socket.AF_INET6, data) 315 | elif atyp == b"\x03": # hostname 316 | data = await self._stream.read_exactly(1) 317 | data += await self._stream.read_exactly(data[0]) 318 | host = data[1:].decode("ascii") 319 | else: 320 | raise Exception(f"unknow atyp: {atyp}") 321 | data_port = await self._stream.read_exactly(2) 322 | port = int.from_bytes(data_port, "big") 323 | return (host, port), atyp + data + data_port 324 | 325 | 326 | # Transparent proxy 327 | class RedirectConnection(ServerBase): 328 | def __init__(self, via=None): 329 | self.via = via 330 | 331 | async def __call__(self, client, addr): 332 | try: 333 | buf = client._socket.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, 16) 334 | port, host = struct.unpack("!2xH4s8x", buf) 335 | self.taddr = (socket.inet_ntoa(host), port) 336 | except Exception as e: 337 | if verbose > 0: 338 | print(f"{self} error: {e}") 339 | print("--> It is not a redirect proxy connection") 340 | await client.close() 341 | return 342 | return await super().__call__(client, addr) 343 | 344 | async def interact(self): 345 | remote_conn = await self.connect_remote() 346 | async with remote_conn: 347 | remote_stream = await self.get_remote_stream(remote_conn) 348 | async with remote_stream: 349 | await self.relay(remote_stream) 350 | self.on_disconnect_remote() 351 | 352 | 353 | class SSStream: 354 | def __repr__(self): 355 | return "<{} {}>".format(self.__class__.__name__, self._stream) 356 | 357 | async def close(self): 358 | await self._stream.close() 359 | 360 | async def __aenter__(self): 361 | await self._stream.__aenter__() 362 | 363 | async def __aexit__(self, *args): 364 | await self._stream.__aexit__(*args) 365 | 366 | async def read_exactly(self, nbytes): 367 | # patch for official shadowsocks 368 | # because official shadowsocks send iv as late as possible 369 | if not hasattr(self, "decrypter"): 370 | iv = await self._stream.read_exactly(self.cipher_cls.IV_LENGTH) 371 | self.decrypter = self.cipher_cls(self.password, iv) 372 | return self.decrypter.decrypt(await self._stream.read_exactly(nbytes)) 373 | 374 | async def read(self, maxbytes=-1): 375 | # patch for official shadowsocks 376 | # because official shadowsocks send iv as late as possible 377 | if not hasattr(self, "decrypter"): 378 | iv = await self._stream.read_exactly(self.cipher_cls.IV_LENGTH) 379 | self.decrypter = self.cipher_cls(self.password, iv) 380 | return self.decrypter.decrypt(await self._stream.read(maxbytes)) 381 | 382 | async def read_until(self, bts): 383 | # side-effect: read more data than you want, 384 | # left those data in self.buffer, 385 | # callers should handle this buffer themselves. 386 | self.buffer = buf = bytearray() 387 | while True: 388 | bts_index = buf.find(bts) 389 | if bts_index >= 0: 390 | resp = bytes(buf[: bts_index + len(bts)]) 391 | del buf[: bts_index + len(bts)] 392 | return resp 393 | data = await self.read() 394 | if data == b"": 395 | raise EOFError("unexpect end of data") 396 | buf.extend(data) 397 | 398 | async def write(self, data): 399 | # implement the same as official shadowsocks 400 | # send iv as late as possible 401 | if not hasattr(self, "encrypter"): 402 | self.encrypter = self.cipher_cls(self.password) 403 | await self._stream.write(self.encrypter.iv) 404 | await self._stream.write(self.encrypter.encrypt(data)) 405 | await self._stream.flush() 406 | 407 | 408 | class SSConnection(ServerBase): 409 | def __init__(self, cipher_cls, password, via=None): 410 | self.cipher_cls = cipher_cls 411 | self.password = password 412 | self.via = via 413 | 414 | def setup(self, stream, addr): 415 | self._stream = SSStream() 416 | self._stream._stream = stream 417 | self._stream.cipher_cls = self.cipher_cls 418 | self._stream.password = self.password 419 | self.laddr = addr 420 | 421 | async def interact(self): 422 | # don't send iv from start 423 | self.taddr, _ = await self.read_addr() 424 | remote_conn = await self.connect_remote() 425 | async with remote_conn: 426 | remote_stream = await self.get_remote_stream(remote_conn) 427 | async with remote_stream: 428 | await self.relay(remote_stream) 429 | self.on_disconnect_remote() 430 | 431 | 432 | class SSClient: 433 | def __init__(self, cipher_cls, password, host, port): 434 | self.cipher_cls = cipher_cls 435 | self.password = password 436 | self.raddr = (host, port) 437 | 438 | async def connect(self): 439 | return await curio.open_connection(*self.raddr) 440 | 441 | def as_stream(self, conn): 442 | stream = SSStream() 443 | stream._stream = conn.as_stream() 444 | stream.encrypter = self.cipher_cls(self.password) 445 | stream.cipher_cls = self.cipher_cls 446 | stream.password = self.password 447 | return stream 448 | 449 | async def init(self, server_stream, remote_stream, taddr): 450 | await remote_stream._stream.write(remote_stream.encrypter.iv) 451 | await remote_stream.write(pack_addr(taddr)) 452 | 453 | 454 | class HTTPClient: 455 | def __init__(self, auth, host, port): 456 | self.auth = auth 457 | self.raddr = (host, port) 458 | 459 | def __repr__(self): 460 | return f"<{self.__class__.__name__}: {self.raddr[0]}:{self.raddr[1]}>" 461 | 462 | async def connect(self): 463 | return await curio.open_connection(*self.raddr) 464 | 465 | def as_stream(self, conn): 466 | return conn.as_stream() 467 | 468 | async def init(self, server_stream, remote_stream, taddr): 469 | if taddr[1] != 443: 470 | await self.init_http(server_stream, remote_stream, taddr) 471 | return 472 | headers_str = ( 473 | f"CONNECT {taddr[0]}:{taddr[1]} HTTP/1.1\r\n" 474 | f"Host: {taddr[0]}:{taddr[1]}\r\n" 475 | f"User-Agent: shadowproxy/{__version__}\r\n" 476 | "Proxy-Connection: Keep-Alive\r\n" 477 | ) 478 | if self.auth: 479 | headers_str += "Proxy-Authorization: Basic {}\r\n".format( 480 | base64.b64encode(self.auth[0] + b":" + self.auth[1]).decode() 481 | ) 482 | headers_str += "\r\n" 483 | await remote_stream.write(headers_str.encode()) 484 | data = await read_until(remote_stream, b"\r\n\r\n") 485 | if not data.startswith(b"HTTP/1.1 200 OK"): 486 | if verbose > 0: 487 | print(f"{self!r} {data}") 488 | if data.startswith(b"HTTP/1.1 407"): 489 | raise Exception(data) 490 | 491 | async def init_http(self, server_stream, remote_stream, taddr): 492 | data = await read_until(server_stream, b"\r\n\r\n") 493 | headers = data[:-4].split(b"\r\n") 494 | method, path, ver = HTTP_LINE.fullmatch(headers.pop(0)).groups() 495 | headers.append(b"Proxy-Connection: Keep-Alive") 496 | if self.auth: 497 | headers.append( 498 | b"Proxy-Authorization: Basic %s\r\n" 499 | % base64.b64encode(self.auth[0] + b":" + self.auth[1]) 500 | ) 501 | url = urllib.parse.urlparse(path) 502 | newpath = url._replace( 503 | scheme=b"http", netloc=("%s:%s" % taddr).encode() 504 | ).geturl() 505 | data = b"%b %b %b\r\n%b\r\n\r\n" % (method, newpath, ver, b"\r\n".join(headers)) 506 | await remote_stream.write(data) 507 | if hasattr(server_stream, "buffer") and server_stream.buffer: 508 | await remote_stream.write(server_stream.buffer) 509 | del server_stream.buffer[:] 510 | 511 | 512 | async def read_until(stream, bts): 513 | if hasattr(stream, "read_until"): 514 | return await stream.read_until(bts) 515 | buf = stream._buffer 516 | while True: 517 | bts_index = buf.find(bts) 518 | if bts_index >= 0: 519 | resp = bytes(buf[: bts_index + len(bts)]) 520 | del buf[: bts_index + len(bts)] 521 | return resp 522 | data = await stream._read(65536) 523 | if data == b"": 524 | raise EOFError("unexpect end of data") 525 | buf.extend(data) 526 | 527 | 528 | class SocksConnection(ServerBase): 529 | def __init__(self, auth=None, via=None): 530 | self.auth = auth 531 | self.via = via 532 | 533 | async def interact(self): 534 | ver, nmethods = struct.unpack("!BB", await self._stream.read_exactly(2)) 535 | assert ver == 5, f"unknown socks version: {ver}" 536 | assert nmethods != 0, f"nmethods can not be 0" 537 | methods = await self._stream.read_exactly(nmethods) 538 | if self.auth and b"\x02" not in methods: 539 | await self._stream.write(b"\x05\xff") 540 | raise Exception("server need auth") 541 | elif b"\x00" not in methods: 542 | await self._stream.write(b"\x05\xff") 543 | raise Exception("method not support") 544 | if self.auth: 545 | await self._stream.write(b"\x05\x02") 546 | auth_ver, username_length = struct.unpack( 547 | "!BB", await self._stream.read_exactly(2) 548 | ) 549 | assert auth_ver == 1 550 | username = await self._stream.read_exactly(username_length) 551 | password_length = (await self._stream.read_exactly(1))[0] 552 | password = await self._stream.read_exactly(password_length) 553 | if (username, password) != self.auth: 554 | await self._stream.write(b"\x01\x01") 555 | raise Exception("auth failed") 556 | else: 557 | await self._stream.write(b"\x01\x00") 558 | else: 559 | await self._stream.write(b"\x05\x00") 560 | ver, cmd, rsv = struct.unpack("!BBB", await self._stream.read_exactly(3)) 561 | try: 562 | self.command = {1: "connect", 2: "bind", 3: "associate"}[cmd] 563 | except KeyError: 564 | raise Exception(f"unknown cmd: {cmd}") 565 | self.taddr, data = await self.read_addr() 566 | if self.command == "associate": 567 | self.taddr = (self.laddr[0], self.taddr[1]) 568 | return await getattr(self, "cmd_" + self.command)() 569 | 570 | async def cmd_connect(self): 571 | remote_conn = await self.connect_remote() 572 | async with remote_conn: 573 | await self._stream.write(self._make_resp()) 574 | remote_stream = await self.get_remote_stream(remote_conn) 575 | async with remote_stream: 576 | await self.relay(remote_stream) 577 | self.on_disconnect_remote() 578 | 579 | async def cmd_associate(self): 580 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 581 | try: 582 | sock.bind(("", 0)) 583 | host, port = sock.getsockname() 584 | async with sock: 585 | await self._stream.write(self._make_resp(host=host, port=port)) 586 | task = await spawn(self.relay_udp(sock)) 587 | while True: 588 | data = await self._stream.read() 589 | if not data: 590 | await task.cancel() 591 | return 592 | if verbose > 0: 593 | print("receive unexpect data:", data) 594 | except Exception: 595 | sock._socket.close() 596 | 597 | async def relay_udp(self, sock): 598 | while True: 599 | try: 600 | data, addr = await sock.recvfrom(8192) 601 | print(data, addr, self.taddr) 602 | if addr == self.taddr: 603 | taddr, data = unpack_addr(data, 3) 604 | address = taddr 605 | else: 606 | address = self.taddr 607 | while data: 608 | nbytes = await sock.sendto(data, address) 609 | data = data[nbytes:] 610 | except CancelledError: 611 | return 612 | except Exception as e: 613 | if verbose > 0: 614 | print(f"{self} error: {e}") 615 | if verbose > 1: 616 | traceback.print_exc() 617 | 618 | def _make_resp(self, code=0, host="0.0.0.0", port=0): 619 | return b"\x05" + code.to_bytes(1, "big") + b"\x00" + pack_addr((host, port)) 620 | 621 | 622 | class OldHTTPConnection(ServerBase): 623 | def __init__(self, auth=None, via=None): 624 | self.auth = auth 625 | self.via = via 626 | 627 | async def read_until(self, bts): 628 | buf = self._stream._buffer 629 | while True: 630 | bts_index = buf.find(bts) 631 | if bts_index >= 0: 632 | resp = bytes(buf[: bts_index + len(bts)]) 633 | del buf[: bts_index + len(bts)] 634 | return resp 635 | data = await self._stream._read(65536) 636 | if data == b"": 637 | raise EOFError("unexpect end of data") 638 | buf.extend(data) 639 | 640 | async def interact(self): 641 | header_lines = await self.read_until(b"\r\n\r\n") 642 | headers = header_lines[:-4].split(b"\r\n") 643 | method, path, ver = HTTP_LINE.fullmatch(headers.pop(0)).groups() 644 | lines = b"\r\n".join(line for line in headers if not line.startswith(b"Proxy-")) 645 | headers = dict(line.split(b": ", 1) for line in headers) 646 | if self.auth: 647 | pauth = headers.get(b"Proxy-Authorization", None) 648 | httpauth = b"Basic " + base64.b64encode(b":".join(self.auth)) 649 | if httpauth != pauth: 650 | await self._stream.write( 651 | ver + b" 407 Proxy Authentication Required\r\n" 652 | b"Connection: close\r\n" 653 | b'Proxy-Authenticate: Basic realm="simple"\r\n\r\n' 654 | ) 655 | raise Exception("Unauthorized HTTP Request") 656 | if method == b"CONNECT": 657 | host, _, port = path.partition(b":") 658 | self.taddr = (host.decode(), int(port)) 659 | else: 660 | url = urllib.parse.urlparse(path) 661 | if not url.hostname: 662 | await self._stream.write( 663 | b"HTTP/1.1 200 OK\r\n" 664 | b"Connection: close\r\n" 665 | b"Content-Type: text/plain\r\n" 666 | b"Content-Length: 2\r\n\r\n" 667 | b"ok" 668 | ) 669 | return 670 | self.taddr = (url.hostname.decode(), url.port or 80) 671 | newpath = url._replace(netloc=b"", scheme=b"").geturl() 672 | remote_conn = await self.connect_remote() 673 | async with remote_conn: 674 | if method == b"CONNECT": 675 | await self._stream.write( 676 | b"HTTP/1.1 200 Connection: Established\r\n\r\n" 677 | ) 678 | remote_req_headers = None 679 | else: 680 | remote_req_headers = b"%s %s %s\r\n%s\r\n\r\n" % ( 681 | method, 682 | newpath, 683 | ver, 684 | lines, 685 | ) 686 | remote_stream = await self.get_remote_stream(remote_conn) 687 | async with remote_stream: 688 | if remote_req_headers: 689 | await remote_stream.write(remote_req_headers) 690 | await self.relay(remote_stream) 691 | self.on_disconnect_remote() 692 | 693 | 694 | class CIDict(dict): 695 | """Case Insensitive dict where all keys are converted to lowercase 696 | This does not maintain the inputted case when calling items() or keys() 697 | in favor of speed, since headers are case insensitive 698 | """ 699 | 700 | def get(self, key, default=None): 701 | return super().get(key.lower(), default) 702 | 703 | def __getitem__(self, key): 704 | return super().__getitem__(key.lower()) 705 | 706 | def __setitem__(self, key, value): 707 | return super().__setitem__(key.lower(), value) 708 | 709 | def __contains__(self, key): 710 | return super().__contains__(key.lower()) 711 | 712 | 713 | class HTTPProxyProtocol: 714 | def __init__(self): 715 | self._header_fragment = b"" 716 | self.headers = [] 717 | self.need_proxy_data = True 718 | self.buffer = bytearray() 719 | 720 | def on_header(self, name, value): 721 | self._header_fragment += name 722 | 723 | if value is not None: 724 | self.headers.append( 725 | (self._header_fragment.decode().lower(), value.decode()) 726 | ) 727 | 728 | self._header_fragment = b"" 729 | 730 | def on_headers_complete(self): 731 | self.headers_dict = CIDict(self.headers) 732 | self.need_proxy_data = False 733 | 734 | def on_body(self, body: bytes): 735 | self.buffer.extend(body) 736 | 737 | def on_url(self, url: bytes): 738 | self.url = url 739 | 740 | 741 | class HTTPConnection(ServerBase): 742 | def __init__(self, auth=None, via=None): 743 | self.auth = auth 744 | self.via = via 745 | 746 | async def __call__(self, client, addr): 747 | self.laddr = addr 748 | try: 749 | async with client: 750 | await self.interact(client) 751 | except Exception as e: 752 | if verbose > 0: 753 | print(f"{self} error: {e}") 754 | if verbose > 1: 755 | traceback.print_exc() 756 | 757 | async def interact(self, client): 758 | protocol = HTTPProxyProtocol() 759 | parser = httptools.HttpRequestParser(protocol) 760 | s = b"" 761 | 762 | while protocol.need_proxy_data: 763 | data = await client.recv(65536) 764 | if not data: 765 | break 766 | s += data 767 | try: 768 | parser.feed_data(data) 769 | except httptools.HttpParserUpgrade as e: 770 | break 771 | 772 | version = parser.get_http_version() 773 | if version == "0.0": 774 | return 775 | if self.auth: 776 | pauth = protocol.headers_dict.get(b"Proxy-Authenticate", None) 777 | httpauth = b"Basic " + base64.b64encode(b":".join(self.auth)) 778 | if httpauth != pauth: 779 | await client.sendall( 780 | version.encode() + b" 407 Proxy Authentication Required\r\n" 781 | b"Connection: close\r\n" 782 | b'Proxy-Authenticate: Basic realm="simple"\r\n\r\n' 783 | ) 784 | raise Exception("Unauthorized HTTP Required") 785 | 786 | method = parser.get_method() 787 | if method == b"CONNECT": 788 | host, _, port = protocol.url.partition(b":") 789 | self.taddr = (host.decode(), int(port)) 790 | else: 791 | url = urllib.parse.urlparse(protocol.url) 792 | if not url.hostname: 793 | await client.sendall( 794 | b"HTTP/1.1 200 OK\r\n" 795 | b"Connection: close\r\n" 796 | b"Content-Type: text/plain\r\n" 797 | b"Content-Length: 2\r\n\r\n" 798 | b"ok" 799 | ) 800 | return 801 | self.taddr = (url.hostname.decode(), url.port or 80) 802 | newpath = url._replace(netloc=b"", scheme=b"").geturl() 803 | remote_conn = await self.connect_remote() 804 | async with remote_conn: 805 | if method == b"CONNECT": 806 | await client.sendall(b"HTTP/1.1 200 Connection: Established\r\n\r\n") 807 | else: 808 | header_lines = "\r\n".join( 809 | f"{k}: {v}" for k, v in protocol.headers if not k[:6] == "Proxy-" 810 | ) 811 | header_lines = header_lines.encode() 812 | remote_req_headers = b"%s %s HTTP/%s\r\n%s\r\n\r\n" % ( 813 | method, 814 | newpath, 815 | version.encode(), 816 | header_lines, 817 | ) 818 | await remote_conn.sendall(remote_req_headers) 819 | if protocol.buffer: 820 | await client.sendall(protocol.buffer) 821 | await self.relay(client, remote_conn) 822 | 823 | async def relay(self, conn, remote_conn): 824 | t1 = await spawn(self._relay(conn, remote_conn)) 825 | t2 = await spawn(self._relay(remote_conn, conn)) 826 | try: 827 | async with TaskGroup([t1, t2]) as g: 828 | task = await g.next_done() 829 | await task.join() 830 | await g.cancel_remaining() 831 | except CancelledError: 832 | pass 833 | 834 | async def _relay(self, rconn, wconn): 835 | try: 836 | while True: 837 | data = await rconn.recv(65536) 838 | if not data: 839 | return 840 | stats.incr("traffic", len(data)) 841 | await wconn.sendall(data) 842 | except CancelledError: 843 | pass 844 | except Exception as e: 845 | if verbose > 0: 846 | print(f"{self} error: {e}") 847 | if verbose > 1: 848 | traceback.print_exc() 849 | 850 | 851 | async def udp_server( 852 | host, port, handler_task, *, family=socket.AF_INET, reuse_address=True 853 | ): 854 | sock = socket.socket(family, socket.SOCK_DGRAM) 855 | try: 856 | sock.bind((host, port)) 857 | if reuse_address: 858 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) 859 | async with sock: 860 | await handler_task(sock) 861 | except Exception: 862 | sock._socket.close() 863 | raise 864 | 865 | 866 | def Sendto(): 867 | socks = weakref.WeakValueDictionary() 868 | 869 | async def sendto_from(bind_addr, data, addr): 870 | try: 871 | if bind_addr not in socks: 872 | sender = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 873 | sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 874 | sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) 875 | sender.bind(bind_addr) 876 | socks[bind_addr] = sender 877 | sender = socks[bind_addr] 878 | async with sender: 879 | await sender.sendto(data, addr) 880 | except OSError as e: 881 | if verbose > 0: 882 | print(e, bind_addr) 883 | 884 | return sendto_from 885 | 886 | 887 | sendto_from = Sendto() 888 | 889 | 890 | class UDPClient: 891 | def __init__(self): 892 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 893 | self._relay_task = None 894 | 895 | async def sendto(self, data, addr): 896 | await self.sock.sendto(data, addr) 897 | 898 | async def relay(self, addr, listen_addr, sendfunc=None): 899 | if self._relay_task is None: 900 | self._relay_task = await spawn(self._relay(addr, listen_addr, sendfunc)) 901 | 902 | async def _relay(self, addr, listen_addr, sendfunc): 903 | try: 904 | while True: 905 | data, raddr = await self.sock.recvfrom(8192) 906 | if verbose > 0: 907 | print( 908 | f"udp: {addr[0]}:{addr[1]} <-- " 909 | f"{listen_addr[0]}:{listen_addr[1]} <-- " 910 | f"{raddr[0]}:{raddr[1]}" 911 | ) 912 | if sendfunc is None: 913 | await sendto_from(raddr, data, addr) 914 | else: 915 | await sendfunc(data, raddr) 916 | except CancelledError: 917 | pass 918 | 919 | async def close(self): 920 | await self._relay_task.cancel() 921 | await self.sock.close() 922 | 923 | 924 | class SSUDPClient(UDPClient): 925 | def __init__(self, cipher_cls, password, host, port): 926 | self.cipher_cls = cipher_cls 927 | self.password = password 928 | self.raddr = (host, port) 929 | super().__init__() 930 | 931 | async def sendto(self, data, addr): 932 | self.taddr = addr 933 | encrypter = self.cipher_cls(self.password) 934 | payload = encrypter.iv + encrypter.encrypt(pack_addr(addr) + data) 935 | await self.sock.sendto(payload, self.raddr) 936 | 937 | def _unpack(self, data): 938 | iv = data[: self.cipher_cls.IV_LENGTH] 939 | cipher = self.cipher_cls(self.password, iv) 940 | data = cipher.decrypt(data[self.cipher_cls.IV_LENGTH :]) 941 | addr, payload = unpack_addr(data) 942 | return payload, addr 943 | 944 | async def _relay(self, addr, listen_addr, sendfunc): 945 | try: 946 | while True: 947 | data, _ = await self.sock.recvfrom(8192) 948 | payload, taddr = self._unpack(data) 949 | if verbose > 0: 950 | print( 951 | f"udp: {addr[0]}:{addr[1]} <-- " 952 | f"{listen_addr[0]}:{listen_addr[1]} <-- " 953 | f"{self.raddr[0]}:{self.raddr[1]} <-- " 954 | f"{self.taddr[0]}:{self.taddr[1]}" 955 | ) 956 | if sendfunc is None: 957 | await sendto_from(self.taddr, payload, addr) 958 | else: 959 | await sendfunc(payload, addr) 960 | except CancelledError: 961 | pass 962 | 963 | 964 | class TProxyUDPServer: 965 | def __init__(self, via=None): 966 | self.via = via 967 | self.removed = None 968 | 969 | def callback(key, value): 970 | self.removed = (key, value) 971 | 972 | import pylru 973 | 974 | self.addr2client = pylru.lrucache(256, callback) 975 | 976 | @staticmethod 977 | def get_origin_dst(ancdata): 978 | for cmsg_level, cmsg_type, cmsg_data in ancdata: 979 | if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVORIGDSTADDR: 980 | family, port, ip = struct.unpack("!HH4s8x", cmsg_data) 981 | return (socket.inet_ntoa(ip), port) 982 | 983 | async def __call__(self, sock): 984 | sock.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, True) 985 | sock.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) 986 | listen_addr = sock.getsockname() 987 | while True: 988 | data, ancdata, msg_flags, addr = await sock.recvmsg( 989 | 8192, socket.CMSG_SPACE(24) 990 | ) 991 | # info = await socket.getaddrinfo( 992 | # *addr, 0, socket.SOCK_DGRAM, socket.SOL_UDP) 993 | taddr = self.get_origin_dst(ancdata) 994 | if taddr is None: 995 | if verbose > 0: 996 | print("can not recognize the original destination") 997 | continue 998 | elif is_local(taddr[0]): 999 | if verbose > 0: 1000 | print(f"local addresses are forbidden: {taddr[0]}") 1001 | continue 1002 | if addr not in self.addr2client: 1003 | via_client = self.via() 1004 | self.addr2client[addr] = via_client 1005 | if self.removed is not None: 1006 | await self.removed[1].close() 1007 | self.removed = None 1008 | via_client = self.addr2client[addr] 1009 | vaddr = via_client.raddr 1010 | if verbose > 0: 1011 | print( 1012 | f"udp: {addr[0]}:{addr[1]} --> " 1013 | f"{listen_addr[0]}:{listen_addr[1]} --> " 1014 | f"{vaddr[0]}:{vaddr[1]} --> {taddr[0]}:{taddr[1]}" 1015 | ) 1016 | await via_client.sendto(data, taddr) 1017 | await via_client.relay(addr, listen_addr) 1018 | 1019 | 1020 | class TunnelUDPServer: 1021 | def __init__(self, target_addr, via=None): 1022 | self.taddr = target_addr 1023 | self.via = via 1024 | self.removed = None 1025 | 1026 | def callback(key, value): 1027 | self.removed = (key, value) 1028 | 1029 | import pylru 1030 | 1031 | self.addr2client = pylru.lrucache(256, callback) 1032 | 1033 | async def __call__(self, sock): 1034 | taddr = self.taddr 1035 | listen_addr = sock.getsockname() 1036 | while True: 1037 | data, addr = await sock.recvfrom(8192) 1038 | if addr not in self.addr2client: 1039 | via_client = self.via() 1040 | self.addr2client[addr] = via_client 1041 | if self.removed is not None: 1042 | await self.removed[1].close() 1043 | self.removed = None 1044 | via_client = self.addr2client[addr] 1045 | vaddr = via_client.raddr 1046 | if verbose > 0: 1047 | print( 1048 | f"udp: {addr[0]}:{addr[1]} --> " 1049 | f"{listen_addr[0]}:{listen_addr[1]} --> " 1050 | f"{vaddr[0]}:{vaddr[1]} --> {taddr[0]}:{taddr[1]}" 1051 | ) 1052 | await via_client.sendto(data, taddr) 1053 | await via_client.relay(addr, listen_addr, sock.sendto) 1054 | 1055 | 1056 | class SSUDPServer: 1057 | def __init__(self, cipher_cls, password): 1058 | self.via = UDPClient 1059 | self.cipher_cls = cipher_cls 1060 | self.password = password 1061 | self.removed = None 1062 | 1063 | def callback(key, value): 1064 | self.removed = (key, value) 1065 | 1066 | import pylru 1067 | 1068 | self.addr2client = pylru.lrucache(256, callback) 1069 | 1070 | async def __call__(self, sock): 1071 | listen_addr = sock.getsockname() 1072 | while True: 1073 | data, addr = await sock.recvfrom(8192) 1074 | if len(data) <= self.cipher_cls.IV_LENGTH: 1075 | continue 1076 | if addr not in self.addr2client: 1077 | via_client = self.via() 1078 | self.addr2client[addr] = via_client 1079 | if self.removed is not None: 1080 | await self.removed[1].close() 1081 | self.removed = None 1082 | via_client = self.addr2client[addr] 1083 | iv = data[: self.cipher_cls.IV_LENGTH] 1084 | decrypter = self.cipher_cls(self.password, iv=iv) 1085 | data = decrypter.decrypt(data[self.cipher_cls.IV_LENGTH :]) 1086 | taddr, payload = unpack_addr(data) 1087 | if verbose > 0: 1088 | print( 1089 | f"udp: {addr[0]}:{addr[1]} --> " 1090 | f"{listen_addr[0]}:{listen_addr[1]} --> " 1091 | f"{taddr[0]}:{taddr[1]}" 1092 | ) 1093 | await via_client.sendto(payload, taddr) 1094 | 1095 | async def sendto(data, taddr): 1096 | encrypter = self.cipher_cls(self.password) 1097 | payload = encrypter.encrypt(pack_addr(taddr) + data) 1098 | await sock.sendto(encrypter.iv + payload, addr) 1099 | 1100 | await via_client.relay(addr, listen_addr, sendto) 1101 | 1102 | 1103 | server_protos = { 1104 | "ss": SSConnection, 1105 | "http": HTTPConnection, 1106 | "https": HTTPConnection, 1107 | "oldhttp": OldHTTPConnection, 1108 | "socks": SocksConnection, 1109 | "red": RedirectConnection, 1110 | "ssudp": SSUDPServer, 1111 | "tproxyudp": TProxyUDPServer, 1112 | "tunneludp": TunnelUDPServer, 1113 | } 1114 | client_protos = { 1115 | "ss": SSClient, 1116 | "ssr": SSClient, 1117 | "ssudp": SSUDPClient, 1118 | "ssrudp": SSUDPClient, 1119 | "http": HTTPClient, 1120 | } 1121 | ciphers = { 1122 | "aes-256-cfb": AES256CFBCipher, 1123 | "aes-128-cfb": AES128CFBCipher, 1124 | "aes-192-cfb": AES192CFBCipher, 1125 | "chacha20": ChaCha20Cipher, 1126 | "salsa20": Salsa20Cipher, 1127 | "rc4": RC4Cipher, 1128 | } 1129 | 1130 | 1131 | def uri_compile(uri, is_server): 1132 | url = urllib.parse.urlparse(uri) 1133 | kw = {} 1134 | if url.scheme == "tunneludp": 1135 | if not url.fragment: 1136 | raise argparse.ArgumentTypeError( 1137 | "destitation must be assign in tunnel udp mode, " 1138 | "example tunneludp://:53#8.8.8.8:53" 1139 | ) 1140 | host, _, port = url.fragment.partition(":") 1141 | kw["target_addr"] = (host, int(port)) 1142 | if url.scheme in ("https",): 1143 | if not url.fragment: 1144 | raise argparse.ArgumentTypeError("#keyfile,certfile is needed") 1145 | keyfile, _, certfile = url.fragment.partition(",") 1146 | ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 1147 | ssl_context.load_cert_chain(certfile=certfile, keyfile=keyfile) 1148 | kw["ssl_context"] = ssl_context 1149 | proto = server_protos[url.scheme] if is_server else client_protos[url.scheme] 1150 | cipher, _, loc = url.netloc.rpartition("@") 1151 | if cipher: 1152 | cipher_cls, _, password = cipher.partition(":") 1153 | if url.scheme.startswith("ss"): 1154 | kw["cipher_cls"] = ciphers[cipher_cls] 1155 | kw["password"] = password.encode() 1156 | elif url.scheme in ("http", "https", "socks"): 1157 | kw["auth"] = (cipher_cls.encode(), password.encode()) 1158 | else: 1159 | pass 1160 | if loc: 1161 | kw["host"], _, port = loc.partition(":") 1162 | kw["port"] = int(port) if port else 1080 1163 | return types.SimpleNamespace(proto=proto, scheme=url.scheme, kw=kw) 1164 | 1165 | 1166 | def get_server(uri): 1167 | listen_uris, _, remote_uri = uri.partition("=") 1168 | listen_uris = listen_uris.split(",") 1169 | if not listen_uris: 1170 | raise ValueError("no server found") 1171 | if remote_uri: 1172 | remote = uri_compile(remote_uri, False) 1173 | via = partial(remote.proto, **remote.kw) 1174 | else: 1175 | via = None 1176 | server_list = [] 1177 | for listen_uri in listen_uris: 1178 | listen = uri_compile(listen_uri, True) 1179 | if via: 1180 | listen.kw["via"] = via 1181 | host = listen.kw.pop("host") 1182 | port = listen.kw.pop("port") 1183 | ssl_context = listen.kw.pop("ssl_context", None) 1184 | if listen.scheme in ("ss", "ssudp") and "cipher_cls" not in listen.kw: 1185 | raise argparse.ArgumentTypeError( 1186 | "you need to assign cryto algorithm and password: " 1187 | f"{listen.scheme}://{host}:{port}" 1188 | ) 1189 | if listen.scheme.endswith("udp"): 1190 | server = udp_server(host, port, listen.proto(**listen.kw)) 1191 | else: 1192 | server = tcp_server( 1193 | host, 1194 | port, 1195 | ProtoFactory(listen.proto, **listen.kw), 1196 | backlog=1024, 1197 | ssl=ssl_context, 1198 | ) 1199 | server_list.append((server, (host, port), listen.scheme)) 1200 | return server_list 1201 | 1202 | 1203 | async def multi_server(*servers): 1204 | tasks = [] 1205 | addrs = [] 1206 | for server_list in servers: 1207 | for server, addr, scheme in server_list: 1208 | task = await spawn(server) 1209 | tasks.append(task) 1210 | addrs.append((*addr, scheme)) 1211 | address = ", ".join(f"{scheme}://{host}:{port}" for host, port, scheme in addrs) 1212 | ss_filter = "or ".join(f"dport = {port}" for host, port, scheme in addrs) 1213 | pid = os.getpid() 1214 | if verbose > 0: 1215 | print(f"{__name__}/{__version__} listen on {address} pid: {pid}") 1216 | print(f"sudo lsof -p {pid} -P | grep -e TCP -e STREAM") 1217 | print(f'ss -o "( {ss_filter} )"') 1218 | tasks.append((await spawn(show_stats()))) 1219 | await curio.gather(tasks) 1220 | 1221 | 1222 | connections = weakref.WeakSet() 1223 | 1224 | 1225 | def ProtoFactory(cls, *args, **kwargs): 1226 | async def client_handler(client, addr): 1227 | handler = cls(*args, **kwargs) 1228 | connections.add(handler) 1229 | return await handler(client, addr) 1230 | 1231 | return client_handler 1232 | 1233 | 1234 | def human_bytes(val): 1235 | if val < 1024: 1236 | return f"{val:.0f}Bytes" 1237 | elif val < 1048576: 1238 | return f"{val/1024:.1f}KB" 1239 | else: 1240 | return f"{val/1048576:.1f}MB" 1241 | 1242 | 1243 | def human_speed(speed): 1244 | if speed < 1024: 1245 | return f"{speed:.0f} B/s" 1246 | elif speed < 1048576: 1247 | return f"{speed/1024:.1f} KB/s" 1248 | else: 1249 | return f"{speed/1048576:.1f} MB/s" 1250 | 1251 | 1252 | async def show_stats(): 1253 | pid = os.getpid() 1254 | print(f"kill -USR1 {pid} to show connections") 1255 | stats.incr("traffic", 0) 1256 | sig = SignalEvent(signal.SIGUSR1) 1257 | while True: 1258 | await sig.wait() 1259 | sig.clear() 1260 | n = len(connections) 1261 | data = stats.flush() 1262 | print( 1263 | f'{n} connections {human_bytes(data["traffic"])} ' 1264 | f'{human_speed(data["traffic"]/60)}' 1265 | ) 1266 | 1267 | 1268 | def main(): 1269 | parser = argparse.ArgumentParser( 1270 | description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter 1271 | ) 1272 | parser.add_argument( 1273 | "-v", dest="verbose", action="count", default=0, help="print verbose output" 1274 | ) 1275 | parser.add_argument( 1276 | "--version", action="version", version=f"%(prog)s {__version__}" 1277 | ) 1278 | parser.add_argument("server", nargs="+", type=get_server) 1279 | args = parser.parse_args() 1280 | global verbose 1281 | verbose = args.verbose 1282 | try: 1283 | resource.setrlimit(resource.RLIMIT_NOFILE, (50000, 50000)) 1284 | except Exception as e: 1285 | print("Require root permission to allocate resources") 1286 | kernel = curio.Kernel() 1287 | try: 1288 | kernel.run(multi_server(*args.server)) 1289 | except Exception as e: 1290 | traceback.print_exc() 1291 | for k, v in kernel._selector.get_map().items(): 1292 | print(k, v, file=sys.stderr) 1293 | for conn in connections: 1294 | print("|", conn, file=sys.stderr) 1295 | except KeyboardInterrupt: 1296 | kernel.run(shutdown=True) 1297 | print() 1298 | 1299 | 1300 | if __name__ == "__main__": 1301 | main() 1302 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [aliases] 5 | test=pytest 6 | 7 | [tool:pytest] 8 | addopts = -s --verbose tests --cov=shadowproxy --cov=tests --cov-report=term-missing --cov-report=xml 9 | 10 | [isort] 11 | line_length = 88 12 | multi_line_output = 3 13 | include_trailing_comma = true 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import re 3 | 4 | from setuptools import find_namespace_packages, setup 5 | 6 | VERSION_RE = re.compile(r"""__version__ = ['"]([0-9.]+)['"]""") 7 | BASE_PATH = os.path.dirname(__file__) 8 | 9 | 10 | with open(os.path.join(BASE_PATH, "shadowproxy", "__init__.py")) as f: 11 | try: 12 | version = VERSION_RE.search(f.read()).group(1) 13 | except IndexError: 14 | raise RuntimeError("Unable to determine version.") 15 | 16 | 17 | with open(os.path.join(BASE_PATH, "README.md")) as readme: 18 | long_description = readme.read() 19 | 20 | 21 | setup( 22 | name="shadowproxy", 23 | description="A proxy server that implements " 24 | "Socks5/Shadowsocks/Redirect/HTTP (tcp) " 25 | "and Shadowsocks/TProxy/Tunnel (udp) protocols.", 26 | long_description=long_description, 27 | long_description_content_type="text/markdown", 28 | license="MIT", 29 | version=version, 30 | author="Yingbo Gu", 31 | author_email="tensiongyb@gmail.com", 32 | maintainer="Yingbo Gu", 33 | maintainer_email="tensiongyb@gmail.com", 34 | url="https://github.com/guyingbo/shadowproxy", 35 | packages=find_namespace_packages(include=["shadowproxy*"]), 36 | install_requires=[ 37 | "pycryptodome>=3.4.3", 38 | "curio==0.9", 39 | "pylru>=1.0.9", 40 | # "microstats>=0.1.0", 41 | "iofree>=0.2.4", 42 | "httptools", 43 | "hkdf", 44 | ], 45 | entry_points={"console_scripts": ["shadowproxy = shadowproxy.__main__:main"]}, 46 | classifiers=[ 47 | "License :: OSI Approved :: MIT License", 48 | "Programming Language :: Python :: 3.6", 49 | "Programming Language :: Python :: 3.7", 50 | "Programming Language :: Python :: 3.8", 51 | ], 52 | setup_requires=["pytest-runner"], 53 | tests_require=["pytest", "coverage", "pytest-cov"], 54 | ) 55 | -------------------------------------------------------------------------------- /shadowproxy/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | An universal proxy server/client which support 3 | Socks/HTTP/Shadowsocks/Redirect (tcp) and 4 | Shadowsocks/TProxy/Tunnel (udp) protocols. 5 | 6 | uri syntax: 7 | 8 | {scheme}://[{userinfo}@][hostname]:{port}[/?[plugin={p;args}][via={uri}][target={t}]][#{fragment}] 9 | 10 | userinfo = cipher:password or base64(cipher:password) when scheme is ss, ssudp 11 | userinfo = username:password or base64(username:password) when scheme is socks, http. 12 | 13 | supported protocols: 14 | 15 | protocol server client scheme 16 | socks5 yes yes socks:// 17 | socks4 yes yes socks4:// 18 | ss yes yes ss:// 19 | ss aead yes yes ss:// 20 | http connect yes yes http:// 21 | http forward yes yes forward:// 22 | transparent yes no red:// 23 | 24 | examples: 25 | 26 | # http(s) proxy 27 | shadowproxy -v http://:8527 28 | 29 | # socks5 -> shadowsocks 30 | shadowproxy -v 'socks://:8527/?via=ss://aes-256-cfb:password@127.0.0.1:8888' 31 | 32 | # http -> shadowsocks 33 | shadowproxy -v 'http://:8527/?via=ss://aes-256-cfb:password@127.0.0.1:8888' 34 | 35 | # redir -> shadowsocks 36 | shadowproxy -v 'red://:12345/?via=ss://aes-256-cfb:password@127.0.0.1:8888' 37 | 38 | # shadowsocks server (tcp) 39 | shadowproxy -v ss://aes-256-cfb:password@:8888 40 | 41 | # shadowsocks server (udp) 42 | shadowproxy -v ssudp://aes-256-cfb:password@:8527 43 | 44 | # tunnel -> shadowsocks (udp) 45 | shadowproxy -v \ 46 | tunneludp://:8527/?target=8.8.8.8:53&via=ssudp://chacha20:pass@127.0.0.1:8888 47 | 48 | # tproxy -> shadowsocks (udp) 49 | sudo shadowproxy -v tproxyudp://:8527/?via=ssudp://chacha20:password@127.0.0.1:8888 50 | """ 51 | __version__ = "0.7.0" 52 | -------------------------------------------------------------------------------- /shadowproxy/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import base64 3 | import ipaddress 4 | import logging 5 | import os 6 | import resource 7 | import weakref 8 | from urllib import parse 9 | 10 | import curio 11 | from curio import socket, ssl 12 | from curio.network import run_server 13 | 14 | from . import __doc__ as desc 15 | from . import __version__, gvars 16 | from .ciphers import ciphers 17 | from .plugins import plugins 18 | from .proxies import server_protos, via_protos 19 | from .utils import ViaNamespace 20 | 21 | connections = weakref.WeakSet() 22 | 23 | 24 | def TcpProtoFactory(cls, **kwargs): 25 | async def client_handler(client, addr): 26 | handler = cls(**kwargs) 27 | connections.add(handler) 28 | return await handler(client, addr) 29 | 30 | return client_handler 31 | 32 | 33 | def get_ssl(url): 34 | ssl_context = None 35 | if url.scheme in ("https",): 36 | if not url.fragment: 37 | raise argparse.ArgumentTypeError("#keyfile,certfile is needed") 38 | keyfile, _, certfile = url.fragment.partition(",") 39 | ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 40 | ssl_context.load_cert_chain(certfile=certfile, keyfile=keyfile) 41 | return ssl_context 42 | 43 | 44 | def parse_addr(s): 45 | host, _, port = s.rpartition(":") 46 | port = -1 if not port else int(port) 47 | if not host: 48 | host = "0.0.0.0" 49 | elif len(host) >= 4 and host[0] == "[" and host[-1] == "]": 50 | host = host[1:-1] 51 | try: 52 | return (ipaddress.ip_address(host), port) 53 | except ValueError: 54 | return (host, port) 55 | 56 | 57 | def parse_source_ip(qs, kwargs): 58 | source_ip = qs["source_ip"][0] 59 | if source_ip in ("in", "same"): 60 | ip = ipaddress.ip_address(kwargs["bind_addr"][0]) 61 | if not ip.is_loopback: 62 | source_ip = str(ip) 63 | return (source_ip, 0) 64 | 65 | 66 | def get_server(uri, is_via=False): 67 | url = parse.urlparse(uri) 68 | kwargs = {} 69 | proto = via_protos[url.scheme] if is_via else server_protos[url.scheme] 70 | userinfo, _, loc = url.netloc.rpartition("@") 71 | if userinfo: 72 | if ":" not in userinfo: 73 | userinfo = base64.b64decode(userinfo).decode("ascii") 74 | cipher_name, _, password = userinfo.partition(":") 75 | if url.scheme.startswith("ss"): 76 | kwargs["cipher"] = ciphers[cipher_name](password) 77 | if not kwargs["cipher"].is_stream_cipher: 78 | proto = via_protos["aead"] if is_via else server_protos["aead"] 79 | elif url.scheme in ("http", "https", "socks", "forward"): 80 | kwargs["auth"] = (cipher_name.encode(), password.encode()) 81 | elif url.scheme in ("ss", "ssudp"): 82 | raise argparse.ArgumentTypeError( 83 | f"you need to assign cryto algorithm and password: {uri}" 84 | ) 85 | host, port = parse_addr(loc) 86 | if port == -1: 87 | port = gvars.default_ports.get(url.scheme, gvars.default_port) 88 | bind_addr = (str(host), port) 89 | kwargs["bind_addr"] = bind_addr 90 | if url.path not in ("", "/"): 91 | kwargs["path"] = url.path 92 | qs = parse.parse_qs(url.query) 93 | if url.scheme == "tunneludp": 94 | if "target" not in qs: 95 | raise argparse.ArgumentTypeError( 96 | "destitation must be assign in tunnel udp mode, " 97 | "example tunneludp://:53/?target=8.8.8.8:53" 98 | ) 99 | host, port = parse_addr(qs["target"][0]) 100 | kwargs["target_addr"] = (str(host), port) 101 | if "plugin" in qs: 102 | plugin_info = qs["plugin"][0] 103 | plugin_name, _, args = plugin_info.partition(";") 104 | args = [arg for arg in args.split(",") if arg] 105 | kwargs["plugin"] = plugins[plugin_name](*args) 106 | if "source_ip" in qs: 107 | kwargs["source_addr"] = parse_source_ip(qs, kwargs) 108 | if is_via: 109 | kwargs["uri"] = uri 110 | return ViaNamespace(ClientClass=proto, **kwargs) 111 | elif "via" in qs: 112 | kwargs["via"] = get_server(qs["via"][0], True) 113 | family = socket.AF_INET6 if ":" in bind_addr[0] else socket.AF_INET 114 | if url.scheme.endswith("udp"): 115 | server_sock = udp_server_socket(*bind_addr, family=family) 116 | real_ip, real_port, *_ = server_sock._socket.getsockname() 117 | server = run_udp_server(server_sock, proto(**kwargs)) 118 | else: 119 | server_sock = curio.tcp_server_socket(*bind_addr, backlog=1024, family=family) 120 | real_ip, real_port, *_ = server_sock._socket.getsockname() 121 | server = run_server( 122 | server_sock, TcpProtoFactory(proto, **kwargs), ssl=get_ssl(url) 123 | ) 124 | return server, (real_ip, real_port), url.scheme 125 | 126 | 127 | def get_client(uri): 128 | ns = get_server(uri, is_via=True) 129 | return ns.new() 130 | 131 | 132 | async def multi_server(*servers): 133 | addrs = [] 134 | async with curio.TaskGroup() as g: 135 | for server, addr, scheme in servers: 136 | await g.spawn(server) 137 | addrs.append((*addr, scheme)) 138 | 139 | # await g.spawn(show_stats()) 140 | address = ", ".join(f"{scheme}://{host}:{port}" for host, port, scheme in addrs) 141 | ss_filter = " or ".join(f"dport = {port}" for host, port, scheme in addrs) 142 | pid = os.getpid() 143 | gvars.logger.info(f"{__package__}/{__version__} listen on {address} pid: {pid}") 144 | gvars.logger.debug(f"sudo lsof -p {pid} -P | grep -e TCP -e STREAM") 145 | gvars.logger.debug(f'ss -o "( {ss_filter} )"') 146 | 147 | 148 | def udp_server_socket(host, port, *, family=socket.AF_INET, reuse_address=True): 149 | sock = socket.socket(family, socket.SOCK_DGRAM) 150 | try: 151 | if reuse_address: 152 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) 153 | sock.bind((host, port)) 154 | return sock 155 | except Exception: 156 | sock._socket.close() 157 | raise 158 | 159 | 160 | async def run_udp_server(sock, handler_task): 161 | try: 162 | async with sock: 163 | await handler_task(sock) 164 | except curio.errors.TaskCancelled: 165 | pass 166 | except Exception as e: 167 | gvars.logger.exception(f"error {e}") 168 | 169 | 170 | def main(arguments=None): 171 | parser = argparse.ArgumentParser( 172 | description=desc, formatter_class=argparse.RawDescriptionHelpFormatter 173 | ) 174 | parser.add_argument( 175 | "-v", dest="verbose", action="count", default=0, help="print verbose output" 176 | ) 177 | parser.add_argument( 178 | "--version", action="version", version=f"%(prog)s {__version__}" 179 | ) 180 | parser.add_argument("server", nargs="+", type=get_server) 181 | args = parser.parse_args(arguments) 182 | if args.verbose == 0: 183 | level = logging.ERROR 184 | elif args.verbose == 1: 185 | level = logging.INFO 186 | else: 187 | level = logging.DEBUG 188 | gvars.logger.setLevel(level) 189 | try: 190 | resource.setrlimit(resource.RLIMIT_NOFILE, (50000, 50000)) 191 | except Exception: 192 | gvars.logger.warning("Require root permission to allocate resources") 193 | kernel = curio.Kernel() 194 | try: 195 | kernel.run(multi_server(*args.server)) 196 | except Exception as e: 197 | gvars.logger.exception(str(e)) 198 | except KeyboardInterrupt: 199 | kernel.run(shutdown=True) 200 | 201 | 202 | if __name__ == "__main__": 203 | main() 204 | -------------------------------------------------------------------------------- /shadowproxy/ciphers.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import os 3 | from hashlib import md5, sha1 4 | 5 | import hkdf 6 | from Crypto.Cipher import AES, ARC4, ChaCha20, ChaCha20_Poly1305, Salsa20 7 | 8 | 9 | class BaseCipher: 10 | def __init__(self, password: str): 11 | self.master_key = self._get_key(password.encode("ascii", "ignore")) 12 | 13 | def _get_key(self, password: bytes, salt: bytes = b"") -> bytes: 14 | keybuf = [] 15 | while len(b"".join(keybuf)) < self.KEY_SIZE: 16 | keybuf.append( 17 | md5((keybuf[-1] if keybuf else b"") + password + salt).digest() 18 | ) 19 | return b"".join(keybuf)[: self.KEY_SIZE] 20 | 21 | 22 | class AEADCipher(BaseCipher, metaclass=abc.ABCMeta): 23 | info = b"ss-subkey" 24 | is_stream_cipher = False 25 | PACKET_LIMIT = 0x3FFF 26 | 27 | @property 28 | @abc.abstractmethod 29 | def KEY_SIZE(self): 30 | "" 31 | 32 | @property 33 | @abc.abstractmethod 34 | def SALT_SIZE(self): 35 | "" 36 | 37 | @property 38 | @abc.abstractmethod 39 | def NONCE_SIZE(self): 40 | "" 41 | 42 | @property 43 | @abc.abstractmethod 44 | def TAG_SIZE(self): 45 | "" 46 | 47 | def _derive_subkey(self, salt: bytes) -> bytes: 48 | return hkdf.Hkdf(salt, self.master_key, sha1).expand(self.info, self.KEY_SIZE) 49 | 50 | def random_salt(self) -> bytes: 51 | return os.urandom(self.SALT_SIZE) 52 | 53 | def make_encrypter(self, salt: bytes = None) -> (bytes, bytes): 54 | counter = 0 55 | salt = salt if salt is not None else self.random_salt() 56 | subkey = self._derive_subkey(salt) 57 | 58 | def encrypt(plaintext: bytes) -> bytes: 59 | nonlocal counter 60 | nonce = counter.to_bytes(self.NONCE_SIZE, "little") 61 | counter += 1 62 | encrypter = self.new_cipher(subkey, nonce) 63 | if len(plaintext) <= self.PACKET_LIMIT: 64 | return encrypter.encrypt_and_digest(plaintext) 65 | else: 66 | with memoryview(plaintext) as data: 67 | return encrypter.encrypt_and_digest( 68 | data[: self.PACKET_LIMIT] 69 | ) + encrypt(data[self.PACKET_LIMIT :]) 70 | 71 | return salt, encrypt 72 | 73 | def make_decrypter(self, salt: bytes): 74 | counter = 0 75 | subkey = self._derive_subkey(salt) 76 | 77 | def decrypt(ciphertext: bytes, tag: bytes) -> bytes: 78 | nonlocal counter 79 | nonce = counter.to_bytes(self.NONCE_SIZE, "little") 80 | counter += 1 81 | decrypter = self.new_cipher(subkey, nonce) 82 | return decrypter.decrypt_and_verify(ciphertext, tag) 83 | 84 | return decrypt 85 | 86 | @abc.abstractmethod 87 | def new_cipher(self, subkey: bytes, nonce: bytes): 88 | "" 89 | 90 | 91 | class AES128GCM(AEADCipher): 92 | KEY_SIZE = 16 93 | SALT_SIZE = 16 94 | NONCE_SIZE = 12 95 | TAG_SIZE = 16 96 | 97 | def new_cipher(self, subkey: bytes, nonce: bytes): 98 | return AES.new(subkey, AES.MODE_GCM, nonce=nonce, mac_len=self.TAG_SIZE) 99 | 100 | 101 | class AES192GCM(AES128GCM): 102 | KEY_SIZE = 24 103 | SALT_SIZE = 24 104 | NONCE_SIZE = 12 105 | TAG_SIZE = 16 106 | 107 | 108 | class AES256GCM(AES128GCM): 109 | KEY_SIZE = 32 110 | SALT_SIZE = 32 111 | NONCE_SIZE = 12 112 | TAG_SIZE = 16 113 | 114 | 115 | class ChaCha20IETFPoly1305(AEADCipher): 116 | KEY_SIZE = 32 117 | SALT_SIZE = 32 118 | NONCE_SIZE = 12 119 | TAG_SIZE = 16 120 | 121 | def new_cipher(self, subkey: bytes, nonce: bytes): 122 | return ChaCha20_Poly1305.new(key=subkey, nonce=nonce) 123 | 124 | 125 | class StreamCipher(BaseCipher, metaclass=abc.ABCMeta): 126 | is_stream_cipher = True 127 | 128 | @property 129 | @abc.abstractmethod 130 | def KEY_SIZE(self): 131 | "" 132 | 133 | @property 134 | @abc.abstractmethod 135 | def IV_SIZE(self): 136 | "" 137 | 138 | def random_iv(self): 139 | return os.urandom(self.IV_SIZE) 140 | 141 | def make_encrypter(self, iv: bytes = None): 142 | iv = iv if iv is not None else self.random_iv() 143 | cipher = self.new_cipher(self.master_key, iv) 144 | 145 | def encrypt(plaintext: bytes) -> bytes: 146 | return cipher.encrypt(plaintext) 147 | 148 | return iv, encrypt 149 | 150 | def make_decrypter(self, iv): 151 | cipher = self.new_cipher(self.master_key, iv) 152 | 153 | def decrypt(ciphertext: bytes) -> bytes: 154 | return cipher.decrypt(ciphertext) 155 | 156 | return decrypt 157 | 158 | @abc.abstractmethod 159 | def new_cipher(self, key: bytes, iv: bytes): 160 | "" 161 | 162 | 163 | class AES256CFB(StreamCipher): 164 | KEY_SIZE = 32 165 | IV_SIZE = 16 166 | 167 | def new_cipher(self, key: bytes, iv: bytes): 168 | return AES.new(key, mode=AES.MODE_CFB, iv=iv, segment_size=128) 169 | 170 | 171 | class AES128CFB(AES256CFB): 172 | KEY_SIZE = 16 173 | 174 | 175 | class AES192CFB(AES256CFB): 176 | KEY_SIZE = 24 177 | 178 | 179 | class ChaCha20Cipher(StreamCipher): 180 | KEY_SIZE = 32 181 | IV_SIZE = 8 182 | 183 | def new_cipher(self, key: bytes, iv: bytes): 184 | return ChaCha20.new(key=key, nonce=iv) 185 | 186 | 187 | class Salsa20Cipher(StreamCipher): 188 | KEY_SIZE = 32 189 | IV_SIZE = 8 190 | 191 | def new_cipher(self, key: bytes, iv: bytes): 192 | return Salsa20.new(key=key, nonce=iv) 193 | 194 | 195 | class RC4Cipher(StreamCipher): 196 | KEY_SIZE = 16 197 | IV_SIZE = 0 198 | 199 | def new_cipher(self, key: bytes, iv: bytes): 200 | return ARC4.new(key=key) 201 | 202 | 203 | ciphers = { 204 | "aes-256-cfb": AES256CFB, 205 | "aes-128-cfb": AES128CFB, 206 | "aes-192-cfb": AES192CFB, 207 | "chacha20": ChaCha20Cipher, 208 | "salsa20": Salsa20Cipher, 209 | "rc4": RC4Cipher, 210 | "aes-256-gcm": AES256GCM, 211 | "aes-192-gcm": AES192GCM, 212 | "aes-128-gcm": AES128GCM, 213 | "chacha20-ietf-poly1305": ChaCha20IETFPoly1305, 214 | } 215 | -------------------------------------------------------------------------------- /shadowproxy/gvars.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | PACKET_SIZE = 8192 5 | logger = logging.getLogger(__package__) 6 | logger.addHandler(logging.StreamHandler(sys.stdout)) 7 | default_ports = {"http": 80, "https": 443, "socks": 8527, "forward": 80, "red": 12345} 8 | default_port = 0 9 | -------------------------------------------------------------------------------- /shadowproxy/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | from .http_simple import HttpSimplePlugin 2 | from .tls1_2 import TLS1_2Plugin 3 | 4 | plugins = { 5 | "http_simple": HttpSimplePlugin, 6 | "tls1.2_ticket_auth": TLS1_2Plugin, 7 | "tls1.2": TLS1_2Plugin, 8 | } 9 | -------------------------------------------------------------------------------- /shadowproxy/plugins/base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class Plugin(abc.ABC): 5 | name = "plugin" 6 | 7 | @abc.abstractmethod 8 | async def init_server(self, client): 9 | "" 10 | 11 | @abc.abstractmethod 12 | async def init_client(self, client): 13 | "" 14 | -------------------------------------------------------------------------------- /shadowproxy/plugins/http_simple.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from ..protocols import http 4 | from ..utils import run_parser_curio, set_disposable_recv 5 | from .base import Plugin 6 | 7 | request_tmpl = ( 8 | b"GET / HTTP/1.1\r\n" 9 | b"Host: %s\r\n" 10 | b"User-Agent: Mozilla/5.0 (compatible; WOW64; MSIE 10.0; Windows NT 6.2)\r\n" 11 | b"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" 12 | b"Accept-Language: en-US,en;q=0.8\r\n" 13 | b"Accept-Encoding: gzip, deflate\r\n" 14 | b"DNT: 1\r\n" 15 | b"Connection: keep-alive\r\n\r\n" 16 | ) 17 | 18 | 19 | class HttpSimplePlugin(Plugin): 20 | name = "http_simple" 21 | 22 | async def init_server(self, client): 23 | parser = http.HTTPRequest.get_parser() 24 | request = await run_parser_curio(parser, client) 25 | head = bytes.fromhex(request.path[1:].replace(b"%", b"").decode("ascii")) 26 | await client.sendall( 27 | b"HTTP/1.1 200 OK\r\n" 28 | b"Connection: keep-alive\r\n" 29 | b"Content-Encoding: gzip\r\n" 30 | b"Content-Type: text/html\r\n" 31 | b"Date: " 32 | + datetime.now().strftime("%a, %d %b %Y %H:%M:%S GMT").encode() 33 | + b"\r\nServer: nginx\r\n" 34 | b"Vary: Accept-Encoding\r\n\r\n" 35 | ) 36 | redundant = head + parser.readall() 37 | set_disposable_recv(client, redundant) 38 | 39 | async def init_client(self, client): 40 | request = request_tmpl % client.target_address.encode() 41 | await client.sock.sendall(request) 42 | parser = http.HTTPResponse.get_parser() 43 | response = await run_parser_curio(parser, client.sock) 44 | assert ( 45 | response.code == b"200" 46 | ), f"bad status code {response.code} {response.status}" 47 | redundant = parser.readall() 48 | set_disposable_recv(client.sock, redundant) 49 | -------------------------------------------------------------------------------- /shadowproxy/plugins/tls1_2.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import hashlib 3 | import hmac 4 | import os 5 | import random 6 | import struct 7 | 8 | from ..utils import run_parser_curio, set_disposable_recv 9 | from .base import Plugin 10 | from .tls_parser import ( 11 | application_data, 12 | pack_auth_data, 13 | pack_uint16, 14 | sni, 15 | tls1_2_request, 16 | tls1_2_response, 17 | ) 18 | 19 | 20 | class TLS1_2Plugin(Plugin): 21 | name = "tls1.2" 22 | 23 | def __init__(self): 24 | self.tls_version = b"\x03\x03" 25 | self.hosts = (b"cloudfront.net", b"cloudfront.com") 26 | self.time_tolerance = 5 * 60 27 | 28 | async def init_server(self, client): 29 | self.response_parser = application_data.parser(self) 30 | tls_parser = tls1_2_request.parser(self) 31 | await run_parser_curio(tls_parser, client) 32 | redundant = tls_parser.readall() 33 | set_disposable_recv(client, redundant) 34 | 35 | def decode(self, data): 36 | self.response_parser.send(data) 37 | return self.response_parser.read_output_bytes() 38 | 39 | def encode(self, data): 40 | ret = b"" 41 | with memoryview(data) as data: 42 | while len(data) > 2048: 43 | size = min(random.randrange(4096) + 100, len(data)) 44 | ret += ( 45 | b"\x17" + self.tls_version + size.to_bytes(2, "big") + data[:size] 46 | ) 47 | data = data[size:] 48 | if len(data) > 0: 49 | ret += b"\x17" + self.tls_version + pack_uint16(data) 50 | return ret 51 | 52 | async def init_client(self, client): 53 | self.ticket_buf = {} 54 | self.response_parser = tls1_2_response.parser(self) 55 | self.session_id = os.urandom(32) 56 | data = ( 57 | self.tls_version 58 | + pack_auth_data(client.ns.cipher.master_key, self.session_id) 59 | + b"\x20" 60 | + self.session_id 61 | ) 62 | data += binascii.unhexlify( 63 | b"001cc02bc02fcca9cca8cc14cc13c00ac014c009c013009c0035002f000a" + b"0100" 64 | ) 65 | ext = binascii.unhexlify(b"ff01000100") 66 | host = random.choice(self.hosts) 67 | ext += sni(host) 68 | ext += b"\x00\x17\x00\x00" 69 | if host not in self.ticket_buf: 70 | self.ticket_buf[host] = os.urandom( 71 | (struct.unpack(">H", os.urandom(2))[0] % 17 + 8) * 16 72 | ) 73 | ext += ( 74 | b"\x00\x23" 75 | + struct.pack(">H", len(self.ticket_buf[host])) 76 | + self.ticket_buf[host] 77 | ) 78 | ext += binascii.unhexlify( 79 | b"000d001600140601060305010503040104030301030302010203" 80 | ) 81 | ext += binascii.unhexlify(b"000500050100000000") 82 | ext += binascii.unhexlify(b"00120000") 83 | ext += binascii.unhexlify(b"75500000") 84 | ext += binascii.unhexlify(b"000b00020100") 85 | ext += binascii.unhexlify(b"000a0006000400170018") 86 | data += pack_uint16(ext) 87 | data = b"\x01\x00" + pack_uint16(data) 88 | data = b"\x16\x03\x01" + pack_uint16(data) 89 | await client.sock.sendall(data) 90 | data = b"\x14" + self.tls_version + b"\x00\x01\x01" 91 | data += b"\x16" + self.tls_version + b"\x00\x20" + os.urandom(22) 92 | data += hmac.new( 93 | self.client.ns.cipher.master_key + self.session_id, data, hashlib.sha1 94 | ).digest()[:10] 95 | await client.sock.sendall(data) 96 | -------------------------------------------------------------------------------- /shadowproxy/plugins/tls_parser.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import hashlib 3 | import hmac 4 | import os 5 | import random 6 | import struct 7 | from time import time 8 | 9 | import iofree 10 | 11 | 12 | def pack_uint16(s): 13 | return len(s).to_bytes(2, "big") + s 14 | 15 | 16 | def sni(host): 17 | return b"\x00\x00" + pack_uint16(pack_uint16(pack_uint16(b"\x00" + host))) 18 | 19 | 20 | def pack_auth_data(key, session_id): 21 | utc_time = int(time()) & 0xFFFFFFFF 22 | data = struct.pack(">I", utc_time) + os.urandom(18) 23 | data += hmac.new(key + session_id, data, hashlib.sha1).digest()[:10] 24 | return data 25 | 26 | 27 | @iofree.parser 28 | def tls1_2_response(plugin): 29 | tls_version = plugin.tls_version 30 | with memoryview((yield from iofree.read(5))) as tls_plaintext_head: 31 | assert ( 32 | tls_plaintext_head[:3] == b"\x16\x03\x03" 33 | ), "invalid tls head: handshake(22) protocol_version(3.1)" 34 | length = int.from_bytes(tls_plaintext_head[-2:], "big") 35 | assert length == length & 0x3FFF, f"{length} is over 2^14" 36 | with memoryview((yield from iofree.read(length))) as fragment: 37 | assert fragment[0] == 2, f"expect server_hello(2), bug got: {fragment[0]}" 38 | handshake_length = int.from_bytes(fragment[1:4], "big") 39 | server_hello = fragment[4 : handshake_length + 4] 40 | assert server_hello[:2] == tls_version, "expect: server_version(3.3)" 41 | verify_id = server_hello[2:34] 42 | sha1 = hmac.new( 43 | plugin.client.ns.cipher.master_key + plugin.session_id, 44 | verify_id[:-10], 45 | hashlib.sha1, 46 | ).digest()[:10] 47 | assert sha1 == verify_id[-10:], "hmac verify failed" 48 | assert server_hello[34] == 32, f"expect 32, but got {server_hello[34]}" 49 | # verify_id = server_hello[35:67] 50 | # sha1 = hmac.new( 51 | # plugin.client.ns.cipher.master_key + plugin.session_id, 52 | # fragment[:-10], 53 | # hashlib.sha1, 54 | # ).digest()[:10] 55 | # assert sha1 == fragment[-10:], "hmac verify failed" 56 | while True: 57 | x = yield from iofree.peek(1) 58 | if x[0] != 22: 59 | break 60 | with memoryview((yield from iofree.read(5))) as ticket_head: 61 | length = int.from_bytes(ticket_head[-2:], "big") 62 | assert length == length & 0x3FFF, f"{length} is over 2^14" 63 | yield from iofree.read(length) 64 | yield from ChangeCipherReader( 65 | plugin, plugin.client.ns.cipher.master_key, plugin.session_id 66 | ) 67 | yield from application_data(plugin) 68 | 69 | 70 | @iofree.parser 71 | def tls1_2_request(plugin): 72 | parser = yield from iofree.get_parser() 73 | tls_version = plugin.tls_version 74 | with memoryview((yield from iofree.read(5))) as tls_plaintext_head: 75 | assert ( 76 | tls_plaintext_head[:3] == b"\x16\x03\x01" 77 | ), "invalid tls head: handshake(22) protocol_version(3.1)" 78 | length = int.from_bytes(tls_plaintext_head[-2:], "big") 79 | assert length == length & 0x3FFF, f"{length} is over 2^14" 80 | with memoryview((yield from iofree.read(length))) as fragment: 81 | assert fragment[0] == 1, "expect client_hello(1), but got {fragment[0]}" 82 | handshake_length = int.from_bytes(fragment[1:4], "big") 83 | client_hello = fragment[4 : handshake_length + 4] 84 | assert client_hello[:2] == tls_version, "expect: client_version(3.3)" 85 | verify_id = client_hello[2:34] 86 | # TODO: replay attact detect 87 | gmt_unix_time = int.from_bytes(verify_id[:4], "big") 88 | time_diff = (int(time()) & 0xFFFFFFFF) - gmt_unix_time 89 | assert abs(time_diff) < plugin.time_tolerance, f"expired request: {time_diff}" 90 | session_length = client_hello[34] 91 | assert session_length >= 32, "session length should be >= 32" 92 | session_id = client_hello[35 : 35 + session_length].tobytes() 93 | sha1 = hmac.new( 94 | plugin.server.cipher.master_key + session_id, verify_id[:22], hashlib.sha1 95 | ).digest()[:10] 96 | assert verify_id[22:] == sha1, "hmac verify failed" 97 | tail = client_hello[35 + session_length :] 98 | cipher_suites = tail[:2].tobytes() 99 | compression_methods = tail[2:3] 100 | (cipher_suites, compression_methods) 101 | random_bytes = pack_auth_data(plugin.server.cipher.master_key, session_id) 102 | server_hello = ( 103 | tls_version 104 | + random_bytes 105 | + session_length.to_bytes(1, "big") 106 | + session_id 107 | + binascii.unhexlify(b"c02f000005ff01000100") 108 | ) 109 | server_hello = b"\x02\x00" + pack_uint16(server_hello) 110 | server_hello = b"\x16" + tls_version + pack_uint16(server_hello) 111 | if random.randint(0, 8) < 1: 112 | ticket = os.urandom((struct.unpack(">H", os.urandom(2))[0] % 164) * 2 + 64) 113 | ticket = struct.pack(">H", len(ticket) + 4) + b"\x04\x00" + pack_uint16(ticket) 114 | server_hello += b"\x16" + tls_version + ticket 115 | change_cipher_spec = b"\x14" + tls_version + b"\x00\x01\x01" 116 | finish_len = random.choice([32, 40]) 117 | change_cipher_spec += ( 118 | b"\x16" 119 | + tls_version 120 | + struct.pack(">H", finish_len) 121 | + os.urandom(finish_len - 10) 122 | ) 123 | change_cipher_spec += hmac.new( 124 | plugin.server.cipher.master_key + session_id, change_cipher_spec, hashlib.sha1 125 | ).digest()[:10] 126 | parser.respond(data=server_hello + change_cipher_spec) 127 | yield from ChangeCipherReader(plugin, plugin.server.cipher.master_key, session_id) 128 | 129 | 130 | def ChangeCipherReader(plugin, key, session_id): 131 | with memoryview((yield from iofree.read(11))) as data: 132 | assert data[0] == 0x14, f"{data[0]} != change_cipher_spec(20) {data.tobytes()}" 133 | assert ( 134 | data[1:3] == plugin.tls_version 135 | ), f"{data[1:3].tobytes()} != version({plugin.tls_version})" 136 | assert data[3:6] == b"\x00\x01\x01", "bad ChangeCipherSpec" 137 | assert data[6] == 0x16, f"{data[6]} != Finish(22)" 138 | assert ( 139 | data[7:9] == plugin.tls_version 140 | ), f"{data[7:9]} != version({plugin.tls_version})" 141 | assert data[9] == 0x00, f"{data[9]} != Finish(0)" 142 | verify_len = int.from_bytes(data[9:11], "big") 143 | with memoryview((yield from iofree.read(verify_len))) as verify: 144 | sha1 = hmac.new( 145 | key + session_id, b"".join([data, verify[:-10]]), hashlib.sha1 146 | ).digest()[:10] 147 | assert sha1 == verify[-10:], "hmac verify failed" 148 | 149 | 150 | @iofree.parser 151 | def application_data(plugin): 152 | parser = yield from iofree.get_parser() 153 | while True: 154 | with memoryview((yield from iofree.read(5))) as data: 155 | assert ( 156 | data[0] == 0x17 157 | ), f"{data[0]} != application_data(23) {data.tobytes()}" 158 | assert ( 159 | data[1:3] == plugin.tls_version 160 | ), f"{data[1:3].tobytes()} != version({plugin.tls_version})" 161 | size = int.from_bytes(data[3:], "big") 162 | assert size == size & 0x3FFF, f"{size} is over 2^14" 163 | data = yield from iofree.read(size) 164 | parser.respond(result=data) 165 | -------------------------------------------------------------------------------- /shadowproxy/protocols/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyingbo/shadowproxy/15e5669136dc6d4720aa54dcc5ff1c6839304767/shadowproxy/protocols/__init__.py -------------------------------------------------------------------------------- /shadowproxy/protocols/exceptions.py: -------------------------------------------------------------------------------- 1 | class ProtocolError(Exception): 2 | "" 3 | -------------------------------------------------------------------------------- /shadowproxy/protocols/http.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from iofree import schema 4 | 5 | HTTP_LINE = re.compile(b"([^ ]+) +(.+?) +(HTTP/[^ ]+)") 6 | 7 | 8 | class HTTPResponse(schema.BinarySchema): 9 | head = schema.EndWith(b"\r\n\r\n") 10 | 11 | def __post_init__(self): 12 | first_line, *header_lines = self.head.split(b"\r\n") 13 | self.ver, self.code, *status = first_line.split(None, 2) 14 | self.status = status[0] if status else b"" 15 | self.header_lines = header_lines 16 | 17 | 18 | class HTTPRequest(schema.BinarySchema): 19 | head = schema.EndWith(b"\r\n\r\n") 20 | 21 | def __post_init__(self): 22 | first_line, *header_lines = self.head.split(b"\r\n") 23 | self.method, self.path, self.ver = HTTP_LINE.fullmatch(first_line).groups() 24 | self.headers = dict([line.split(b": ", 1) for line in header_lines]) 25 | -------------------------------------------------------------------------------- /shadowproxy/protocols/socks4.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import socket 3 | 4 | import iofree 5 | from iofree import schema 6 | 7 | 8 | class Cmd(enum.IntEnum): 9 | connect = 1 10 | bind = 2 11 | 12 | 13 | class Rep(enum.IntEnum): 14 | granted = 0x5A 15 | rejected = 0x5B 16 | un_reachable = 0x5C 17 | auth_failed = 0x5D 18 | 19 | 20 | class ClientRequest(schema.BinarySchema): 21 | ver = schema.MustEqual(schema.uint8, 4) 22 | cmd = schema.SizedIntEnum(schema.uint8, Cmd) 23 | dst_port = schema.uint16be 24 | dst_ip = schema.Convert( 25 | schema.Bytes(4), encode=socket.inet_aton, decode=socket.inet_ntoa 26 | ) 27 | user_id = schema.EndWith(b"\x00") 28 | 29 | 30 | class Response(schema.BinarySchema): 31 | vn = schema.MustEqual(schema.Bytes(1), b"\x00") 32 | rep = schema.SizedIntEnum(schema.uint8, Rep) 33 | dst_port = schema.uint16be 34 | dst_ip = schema.Convert( 35 | schema.Bytes(4), encode=socket.inet_aton, decode=socket.inet_ntoa 36 | ) 37 | 38 | 39 | domain = schema.EndWith(b"\x00") 40 | 41 | 42 | @iofree.parser 43 | def server(): 44 | parser = yield from iofree.get_parser() 45 | request = yield from ClientRequest.get_value() 46 | if request.dst_ip.startswith("0.0.0"): 47 | host = yield from domain.get_value() 48 | addr = (host, request.dst_port) 49 | else: 50 | addr = (request.dst_ip, request.dst_port) 51 | assert request.cmd is Cmd.connect 52 | parser.respond(result=addr) 53 | rep = yield from iofree.wait_event() 54 | parser.respond(data=Response(..., Rep(rep), 0, "0.0.0.0").binary) 55 | 56 | 57 | @iofree.parser 58 | def client(addr): 59 | host, port = addr 60 | parser = yield from iofree.get_parser() 61 | tail = b"" 62 | try: 63 | request = ClientRequest(..., Cmd.connect, port, host, b"\x01\x01") 64 | except OSError: 65 | request = ClientRequest(..., Cmd.connect, port, "0.0.0.1", b"\x01\x01") 66 | tail = domain(host.encode()) 67 | parser.respond(data=request.binary + tail) 68 | response = yield from Response.get_value() 69 | assert response.rep is Rep.granted 70 | parser.respond(result=response) 71 | -------------------------------------------------------------------------------- /shadowproxy/protocols/socks5.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import iofree 4 | from iofree.contrib import socks5 5 | 6 | from .exceptions import ProtocolError 7 | 8 | 9 | @iofree.parser 10 | def server(auth: typing.Tuple[str, str]): 11 | parser = yield from iofree.get_parser() 12 | handshake = yield from socks5.Handshake.get_value() 13 | addr = socks5.Addr(1, "0.0.0.0", 0) 14 | if auth: 15 | if socks5.AuthMethod.user_auth not in handshake.methods: 16 | parser.respond( 17 | data=socks5.Reply(..., socks5.Rep.not_allowed, ..., addr).binary, 18 | close=True, 19 | exc=ProtocolError("auth method not allowed"), 20 | ) 21 | return 22 | parser.respond( 23 | data=socks5.ServerSelection(..., socks5.AuthMethod.user_auth).binary 24 | ) 25 | user_auth = yield from socks5.UsernameAuth.get_value() 26 | if (user_auth.username.encode(), user_auth.password.encode()) != auth: 27 | parser.respond( 28 | data=socks5.Reply(..., socks5.Rep.not_allowed, ..., addr).binary, 29 | close=True, 30 | exc=ProtocolError("auth failed"), 31 | ) 32 | return 33 | parser.respond(data=socks5.UsernameAuthReply(..., ...).binary) 34 | else: 35 | parser.respond( 36 | data=socks5.ServerSelection(..., socks5.AuthMethod.no_auth).binary 37 | ) 38 | request = yield from socks5.ClientRequest.get_value() 39 | assert ( 40 | request.cmd is socks5.Cmd.connect 41 | ), f"only support connect command now, got {socks5.Cmd.connect!r}" 42 | parser.respond(result=request) 43 | rep = yield from iofree.wait_event() 44 | parser.respond(data=socks5.Reply(..., socks5.Rep(rep), ..., addr).binary) 45 | 46 | 47 | @iofree.parser 48 | def client(auth, target_addr): 49 | parser = yield from iofree.get_parser() 50 | parser.respond( 51 | data=socks5.Handshake( 52 | ..., [socks5.AuthMethod.no_auth, socks5.AuthMethod.user_auth] 53 | ).binary 54 | ) 55 | server_selection = yield from socks5.ServerSelection.get_value() 56 | if server_selection.method not in ( 57 | socks5.AuthMethod.no_auth, 58 | socks5.AuthMethod.user_auth, 59 | ): 60 | parser.respond(close=True, exc=ProtocolError("no method to choose")) 61 | if auth and (server_selection.method is socks5.AuthMethod.user_auth): 62 | parser.respond( 63 | data=socks5.UsernameAuth(..., auth[0].decode(), auth[1].decode()).binary 64 | ) 65 | yield from socks5.UsernameAuthReply.get_value() 66 | parser.respond( 67 | data=socks5.ClientRequest( 68 | ..., socks5.Cmd.connect, ..., socks5.Addr.from_tuple(target_addr) 69 | ).binary 70 | ) 71 | reply = yield from socks5.Reply.get_value() 72 | if reply.rep is not socks5.Rep.succeeded: 73 | parser.respond(close=True, exc=ProtocolError(f"bad reply: {reply}")) 74 | parser.respond(result=reply) 75 | 76 | 77 | def resp(): 78 | addr = socks5.Addr(3, "0.0.0.0", 0) 79 | return socks5.Reply(..., socks5.Rep.succeeded, ..., addr).binary 80 | -------------------------------------------------------------------------------- /shadowproxy/proxies/__init__.py: -------------------------------------------------------------------------------- 1 | from .aead.client import AEADClient 2 | from .aead.server import AEADProxy 3 | from .http.client import HTTPClient, HTTPForwardClient 4 | from .http.server import HTTPProxy 5 | from .shadowsocks.client import SSClient 6 | from .shadowsocks.server import SSProxy 7 | from .shadowsocks.udpclient import SSUDPClient 8 | from .shadowsocks.udpserver import SSUDPServer 9 | from .socks.client import Socks4Client, SocksClient 10 | from .socks.server import Socks4Proxy, SocksProxy 11 | from .transparent.server import TransparentProxy 12 | from .tunnel.udpserver import TunnelUDPServer 13 | 14 | server_protos = { 15 | "ss": SSProxy, 16 | "aead": AEADProxy, 17 | "socks": SocksProxy, 18 | "http": HTTPProxy, 19 | "red": TransparentProxy, 20 | "socks4": Socks4Proxy, 21 | "tunneludp": TunnelUDPServer, 22 | "ssudp": SSUDPServer, 23 | } 24 | via_protos = { 25 | "ss": SSClient, 26 | "aead": AEADClient, 27 | "http": HTTPClient, 28 | "forward": HTTPForwardClient, 29 | "socks": SocksClient, 30 | "socks4": Socks4Client, 31 | "ssudp": SSUDPClient, 32 | } 33 | -------------------------------------------------------------------------------- /shadowproxy/proxies/aead/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyingbo/shadowproxy/15e5669136dc6d4720aa54dcc5ff1c6839304767/shadowproxy/proxies/aead/__init__.py -------------------------------------------------------------------------------- /shadowproxy/proxies/aead/client.py: -------------------------------------------------------------------------------- 1 | from ...utils import pack_addr 2 | from ..base.client import ClientBase 3 | from .parser import aead_reader 4 | 5 | 6 | class AEADClient(ClientBase): 7 | proto = "AEAD" 8 | 9 | async def init(self): 10 | self.aead_parser = aead_reader.parser(self.ns.cipher) 11 | self.plugin = getattr(self.ns, "plugin", None) 12 | if self.plugin: 13 | self.plugin.client = self 14 | await self.plugin.init_client(self) 15 | salt, self.encrypt = self.ns.cipher.make_encrypter() 16 | data = pack_addr(self.target_addr) 17 | len_data = len(data).to_bytes(2, "big") 18 | to_send = salt + b"".join(self.encrypt(len_data)) + b"".join(self.encrypt(data)) 19 | await self.sock.sendall(to_send) 20 | 21 | async def recv(self, size): 22 | data = await self.sock.recv(size) 23 | if not data: 24 | return data 25 | if self.plugin and hasattr(self.plugin, "decode"): 26 | data = self.plugin.decode(data) 27 | if not data: 28 | return await self.recv(size) 29 | self.aead_parser.send(data) 30 | data = self.aead_parser.read_output_bytes() 31 | if not data: 32 | data = await self.recv(size) 33 | return data 34 | 35 | async def sendall(self, data): 36 | if not data: 37 | return 38 | len_data = len(data).to_bytes(2, "big") 39 | to_send = b"".join(self.encrypt(len_data)) + b"".join(self.encrypt(data)) 40 | await self.sock.sendall(to_send) 41 | -------------------------------------------------------------------------------- /shadowproxy/proxies/aead/parser.py: -------------------------------------------------------------------------------- 1 | import iofree 2 | 3 | 4 | @iofree.parser 5 | def aead_reader(cipher): 6 | parser = yield from iofree.get_parser() 7 | parser.cipher = cipher 8 | salt = yield from iofree.read(cipher.SALT_SIZE) 9 | parser.decrypt = cipher.make_decrypter(salt) 10 | while True: 11 | payload = yield from _read_some() 12 | parser.respond(result=payload) 13 | 14 | 15 | def _read_some(): 16 | parser = yield from iofree.get_parser() 17 | chunk0 = yield from iofree.read(2 + parser.cipher.TAG_SIZE) 18 | with memoryview(chunk0) as data: 19 | length_bytes = parser.decrypt(data[:2], data[2:]) 20 | length = int.from_bytes(length_bytes, "big") 21 | if length != length & 0x3FFF: # 16 * 1024 - 1 22 | raise Exception("length exceed limit") 23 | chunk1 = yield from iofree.read(length + parser.cipher.TAG_SIZE) 24 | with memoryview(chunk1) as data: 25 | payload = parser.decrypt(data[:length], data[length:]) 26 | return payload 27 | -------------------------------------------------------------------------------- /shadowproxy/proxies/aead/server.py: -------------------------------------------------------------------------------- 1 | from iofree.contrib.common import Addr 2 | 3 | from ...utils import run_parser_curio 4 | from ..base.server import ProxyBase 5 | from .parser import aead_reader 6 | 7 | 8 | class AEADProxy(ProxyBase): 9 | proto = "AEAD" 10 | 11 | def __init__(self, cipher, bind_addr, via=None, plugin=None, **kwargs): 12 | self.cipher = cipher 13 | self.bind_addr = bind_addr 14 | self.via = via 15 | self.plugin = plugin 16 | self.kwargs = kwargs 17 | self.aead_parser = aead_reader.parser(self.cipher) 18 | 19 | async def _run(self): 20 | if self.plugin: 21 | self.plugin.server = self 22 | self.proto += f"({self.plugin.name})" 23 | await self.plugin.init_server(self.client) 24 | 25 | addr_parser = Addr.get_parser() 26 | addr = await run_parser_curio(addr_parser, self) 27 | self.target_addr = (addr.host, addr.port) 28 | 29 | via_client = await self.connect_server(self.target_addr) 30 | 31 | async with via_client: 32 | redundant = addr_parser.readall() 33 | if redundant: 34 | await via_client.sendall(redundant) 35 | await self.relay(via_client) 36 | 37 | async def recv(self, size): 38 | data = await self.client.recv(size) 39 | if not data: 40 | return data 41 | if hasattr(self.plugin, "decode"): 42 | data = self.plugin.decode(data) 43 | if not data: 44 | return await self.recv(size) 45 | self.aead_parser.send(data) 46 | data = self.aead_parser.read_output_bytes() 47 | if not data: 48 | data = await self.recv(size) 49 | return data 50 | 51 | async def sendall(self, data): 52 | packet = b"" 53 | if not hasattr(self, "encrypt"): 54 | packet, self.encrypt = self.cipher.make_encrypter() 55 | length = len(data) 56 | packet += b"".join(self.encrypt(length.to_bytes(2, "big"))) 57 | packet += b"".join(self.encrypt(data)) 58 | if hasattr(self.plugin, "encode"): 59 | packet = self.plugin.encode(packet) 60 | await self.client.sendall(packet) 61 | -------------------------------------------------------------------------------- /shadowproxy/proxies/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyingbo/shadowproxy/15e5669136dc6d4720aa54dcc5ff1c6839304767/shadowproxy/proxies/base/__init__.py -------------------------------------------------------------------------------- /shadowproxy/proxies/base/client.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from time import time 3 | from urllib import parse 4 | 5 | from ... import gvars 6 | from ...utils import open_connection 7 | 8 | 9 | class HTTPResponse: 10 | def __init__(self, client): 11 | self.client = client 12 | self.done = False 13 | self.header_size = 0 14 | self.body_size = 0 15 | self.speed = 0 16 | self.start = time() 17 | 18 | @property 19 | def size(self): 20 | return self.header_size + self.body_size 21 | 22 | def on_header(self, name: bytes, value: bytes): 23 | self.header_size += len(name) + len(value) 24 | 25 | def on_message_complete(self): 26 | self.done = True 27 | seconds = time() - self.start 28 | self.speed = int(self.size / 1024 / seconds) # KB/s 29 | 30 | def on_body(self, body: bytes): 31 | self.body_size += len(body) 32 | 33 | 34 | class ClientBase(abc.ABC): 35 | sock = None 36 | target_addr = ("unknown", -1) 37 | 38 | def __init__(self, namespace): 39 | self.ns = namespace 40 | 41 | def __repr__(self): 42 | return f"{self.__class__.__name__}({self})" 43 | 44 | def __str__(self): 45 | return f"{self.bind_address} -- {self.target_address}" 46 | 47 | async def __aenter__(self): 48 | return self 49 | 50 | async def __aexit__(self, et, e, tb): 51 | await self.close() 52 | 53 | async def close(self): 54 | if self.sock: 55 | await self.sock.close() 56 | self.sock = None 57 | 58 | @property 59 | @abc.abstractmethod 60 | def proto(self): 61 | "" 62 | 63 | @property 64 | def bind_address(self) -> str: 65 | return f"{self.ns.bind_addr[0]}:{self.ns.bind_addr[1]}" 66 | 67 | @property 68 | def target_address(self) -> str: 69 | return f"{self.target_addr[0]}:{self.target_addr[1]}" 70 | 71 | async def connect(self, target_addr, source_addr=None): 72 | self.target_addr = target_addr 73 | if self.sock: 74 | return 75 | self.sock = await open_connection(*self.ns.bind_addr, source_addr=source_addr) 76 | 77 | @abc.abstractmethod 78 | async def init(self): 79 | "" 80 | 81 | async def recv(self, size): 82 | return await self.sock.recv(size) 83 | 84 | async def sendall(self, data): 85 | return await self.sock.sendall(data) 86 | 87 | async def http_request( 88 | self, uri: str, method: str = "GET", headers: list = None, response_cls=None 89 | ): 90 | import httptools 91 | 92 | response_cls = response_cls or HTTPResponse 93 | url = parse.urlparse(uri) 94 | host, _, port = url.netloc.partition(":") 95 | try: 96 | port = int(port) 97 | except ValueError: 98 | if url.scheme == "http": 99 | port = 80 100 | elif url.scheme == "https": 101 | port = 443 102 | else: 103 | raise Exception(f"unknown scheme: {url.scheme}") 104 | target_addr = (host, port) 105 | await self.connect(target_addr) 106 | await self.init() 107 | 108 | header_list = [f"Host: {self.target_address}".encode()] 109 | if headers: 110 | for header in headers: 111 | if isinstance(header, str): 112 | header = header.encode() 113 | header_list.append(header) 114 | ver = b"HTTP/1.1" 115 | method = method.upper().encode() 116 | url = url.geturl().encode() 117 | data = b"%b %b %b\r\n%b\r\n\r\n" % (method, url, ver, b"\r\n".join(header_list)) 118 | await self.sendall(data) 119 | response = response_cls(self) 120 | parser = httptools.HttpResponseParser(response) 121 | while not response.done: 122 | data = await self.recv(gvars.PACKET_SIZE) 123 | if not data: 124 | raise Exception("Incomplete response") 125 | parser.feed_data(data) 126 | return response 127 | -------------------------------------------------------------------------------- /shadowproxy/proxies/base/server.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | import curio 4 | 5 | from ... import gvars 6 | from ...utils import is_global, open_connection 7 | 8 | 9 | class ProxyBase(abc.ABC): 10 | target_addr = ("unknown", -1) 11 | client = None 12 | via = None 13 | 14 | @property 15 | @abc.abstractmethod 16 | def proto(self): 17 | "" 18 | 19 | @abc.abstractmethod 20 | async def _run(self): 21 | "" 22 | 23 | @property 24 | def target_address(self) -> str: 25 | return f"{self.target_addr[0]}:{self.target_addr[1]}" 26 | 27 | @property 28 | def client_address(self) -> str: 29 | return f"{self.client_addr[0]}:{self.client_addr[1]}" 30 | 31 | @property 32 | def via_address(self) -> str: 33 | if hasattr(self, "_via_address"): 34 | return self._via_address 35 | if getattr(self, "via", None): 36 | return self.via.bind_address 37 | return "" 38 | 39 | @property 40 | def remote_address(self) -> str: 41 | if getattr(self, "via", None): 42 | return self.via.bind_address 43 | return self.target_address 44 | 45 | @property 46 | def bind_address(self) -> str: 47 | return f"{self.bind_addr[0]}:{self.bind_addr[1]}" 48 | 49 | def __repr__(self): 50 | return f"{self.__class__.__name__}({self})" 51 | 52 | def __str__(self): 53 | via_address = f" -- {self.via_address}" if self.via_address else "" 54 | return ( 55 | f"{self.client_address} -- {self.proto} -- {self.bind_address}" 56 | f"{via_address} -- {self.target_address}" 57 | ) 58 | 59 | async def connect_server(self, target_addr): 60 | assert is_global( 61 | target_addr[0] 62 | ), f"non global target address is forbidden {target_addr}" 63 | source_addr = self.kwargs.get("source_addr") 64 | if self.via: 65 | via_client = self.via.new() 66 | self._via_address = f"{via_client.proto} -- {via_client.bind_address}" 67 | await via_client.connect(target_addr, source_addr) 68 | await via_client.init() 69 | else: 70 | via_client = await open_connection(*target_addr, source_addr=source_addr) 71 | gvars.logger.info(self) 72 | return via_client 73 | 74 | async def __call__(self, client, addr): 75 | self.client = client 76 | self.client_addr = addr 77 | try: 78 | async with client: 79 | await self._run() 80 | except curio.errors.TaskCancelled: 81 | pass 82 | except Exception as e: 83 | gvars.logger.debug(f"{self} {e}") 84 | 85 | async def relay(self, via_client): 86 | try: 87 | async with curio.TaskGroup() as g: 88 | await g.spawn(self._relay(via_client)) 89 | await g.spawn(self._reverse_relay(via_client)) 90 | await g.next_done(cancel_remaining=True) 91 | except curio.TaskGroupError as e: 92 | gvars.logger.debug(f"group error: {e}") 93 | 94 | async def _relay(self, to): 95 | recv = getattr(self, "recv", self.client.recv) 96 | while True: 97 | try: 98 | data = await recv(gvars.PACKET_SIZE) 99 | except (ConnectionResetError, BrokenPipeError) as e: 100 | gvars.logger.debug(f"{self} recv from {self.client_address} {e}") 101 | return 102 | if not data: 103 | break 104 | try: 105 | await to.sendall(data) 106 | except (ConnectionResetError, BrokenPipeError) as e: 107 | gvars.logger.debug(f"{self} send to {self.remote_address} {e}") 108 | return 109 | 110 | async def _reverse_relay(self, from_): 111 | sendall = getattr(self, "sendall", self.client.sendall) 112 | while True: 113 | try: 114 | data = await from_.recv(gvars.PACKET_SIZE) 115 | except (ConnectionResetError, BrokenPipeError) as e: 116 | gvars.logger.debug(f"{self} recv from {self.remote_address} {e}") 117 | return 118 | if not data: 119 | break 120 | try: 121 | await sendall(data) 122 | except (ConnectionResetError, BrokenPipeError) as e: 123 | gvars.logger.debug(f"{self} send to {self.client_address} {e}") 124 | return 125 | -------------------------------------------------------------------------------- /shadowproxy/proxies/base/udpclient.py: -------------------------------------------------------------------------------- 1 | import curio 2 | from curio import socket 3 | 4 | from ... import gvars 5 | 6 | 7 | class UDPClient: 8 | def __init__(self, ns=None): 9 | self.ns = ns 10 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 11 | if "source_addr" in self.ns: 12 | self.sock.bind(self.ns["source_addr"]) 13 | self._task = None 14 | 15 | async def sendto(self, data, addr): 16 | await self.sock.sendto(data, addr) 17 | 18 | async def close(self): 19 | if self._task: 20 | self._task.cancel() 21 | await self.sock.close() 22 | 23 | async def relay(self, addr, sendfrom): 24 | if self._task is None: 25 | self._task = await curio.spawn(self._relay, addr, sendfrom) 26 | 27 | async def _relay(self, addr, sendfrom): 28 | try: 29 | while True: 30 | data, raddr = await self.sock.recvfrom(gvars.PACKET_SIZE) 31 | if raddr != addr: 32 | continue 33 | await sendfrom(data, addr) 34 | except curio.errors.CancelledError: 35 | pass 36 | -------------------------------------------------------------------------------- /shadowproxy/proxies/base/udpserver.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class UDPServerBase(abc.ABC): 5 | @property 6 | @abc.abstractmethod 7 | def proto(self): 8 | "" 9 | 10 | @abc.abstractmethod 11 | async def __call__(self, sock): 12 | "" 13 | -------------------------------------------------------------------------------- /shadowproxy/proxies/http/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyingbo/shadowproxy/15e5669136dc6d4720aa54dcc5ff1c6839304767/shadowproxy/proxies/http/__init__.py -------------------------------------------------------------------------------- /shadowproxy/proxies/http/client.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | from ... import __version__ 4 | from ...protocols import http 5 | from ...utils import run_parser_curio, set_disposable_recv 6 | from ..base.client import ClientBase 7 | 8 | 9 | class HTTPClient(ClientBase): 10 | proto = "HTTP(CONNECT)" 11 | 12 | async def init(self): 13 | headers_str = ( 14 | f"CONNECT {self.target_address} HTTP/1.1\r\n" 15 | f"Host: {self.target_address}\r\n" 16 | f"User-Agent: shadowproxy/{__version__}\r\n" 17 | "Proxy-Connection: Keep-Alive\r\n" 18 | ) 19 | auth = getattr(self.ns, "auth", None) 20 | if auth: 21 | headers_str += "Proxy-Authorization: Basic {}\r\n".format( 22 | base64.b64encode(b":".join(auth)).decode() 23 | ) 24 | headers_str += "\r\n" 25 | await self.sock.sendall(headers_str.encode()) 26 | 27 | parser = http.HTTPResponse.get_parser() 28 | response = await run_parser_curio(parser, self.sock) 29 | 30 | assert ( 31 | response.code == b"200" 32 | ), f"bad status code: {parser.code} {parser.status}" 33 | redundant = parser.readall() 34 | set_disposable_recv(self.sock, redundant) 35 | 36 | 37 | class HTTPForwardClient(HTTPClient): 38 | proto = "HTTP(Forward)" 39 | 40 | async def init(self): 41 | if self.target_addr[1] == 443: 42 | await super().init() 43 | else: 44 | headers = [] 45 | headers.append(b"Proxy-Connection: Keep-Alive") 46 | auth = getattr(self.ns, "auth", None) 47 | if auth: 48 | headers.append( 49 | b"Proxy-Authorization: Basic %s" % base64.b64encode(b":".join(auth)) 50 | ) 51 | self.extra_headers = headers 52 | 53 | async def http_request( 54 | self, uri: str, method: str = "GET", headers: list = None, response_cls=None 55 | ): 56 | headers = headers or [] 57 | headers.append(b"Proxy-Connection: Keep-Alive") 58 | auth = getattr(self.ns, "auth", None) 59 | if auth: 60 | headers.append( 61 | b"Proxy-Authorization: Basic %s" % base64.b64encode(b":".join(auth)) 62 | ) 63 | return await super().http_request(uri, method, headers, response_cls) 64 | -------------------------------------------------------------------------------- /shadowproxy/proxies/http/server.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from urllib import parse 3 | 4 | from ...protocols import http 5 | from ...utils import run_parser_curio 6 | from ..base.server import ProxyBase 7 | from .client import HTTPForwardClient 8 | 9 | 10 | class HTTPProxy(ProxyBase): 11 | proto = "HTTP" 12 | 13 | def __init__(self, bind_addr, auth=None, via=None, **kwargs): 14 | self.bind_addr = bind_addr 15 | self.auth = auth 16 | self.via = via 17 | self.bind_addr = bind_addr 18 | self.kwargs = kwargs 19 | 20 | async def _run(self): 21 | parser = http.HTTPRequest.get_parser() 22 | request = await run_parser_curio(parser, self.client) 23 | if self.auth: 24 | pauth = request.headers.get(b"Proxy-Authorization", None) 25 | httpauth = b"Basic " + base64.b64encode(b":".join(self.auth)) 26 | if httpauth != pauth: 27 | await self.client.sendall( 28 | request.ver + b" 407 Proxy Authentication Required\r\n" 29 | b"Connection: close\r\n" 30 | b'Proxy-Authenticate: Basic realm="Shadowproxy Auth"\r\n\r\n' 31 | ) 32 | raise Exception("Unauthorized HTTP Request") 33 | if request.method == b"CONNECT": 34 | self.proto = "HTTP(CONNECT)" 35 | host, _, port = request.path.partition(b":") 36 | self.target_addr = (host.decode(), int(port)) 37 | else: 38 | self.proto = "HTTP(PASS)" 39 | url = parse.urlparse(request.path) 40 | if not url.hostname: 41 | await self.client.sendall( 42 | b"HTTP/1.1 200 OK\r\n" 43 | b"Connection: close\r\n" 44 | b"Content-Type: text/plain\r\n" 45 | b"Content-Length: 2\r\n\r\n" 46 | b"ok" 47 | ) 48 | return 49 | self.target_addr = (url.hostname.decode(), url.port or 80) 50 | newpath = url._replace(netloc=b"", scheme=b"").geturl() 51 | via_client = await self.connect_server(self.target_addr) 52 | async with via_client: 53 | if request.method == b"CONNECT": 54 | await self.client.sendall( 55 | b"HTTP/1.1 200 Connection: Established\r\n\r\n" 56 | ) 57 | remote_req_headers = b"" 58 | else: 59 | headers_list = [ 60 | b"%s: %s" % (k, v) 61 | for k, v in request.headers.items() 62 | if not k.startswith(b"Proxy-") 63 | ] 64 | if isinstance(via_client, HTTPForwardClient): 65 | headers_list.extend(via_client.extra_headers) 66 | newpath = url.geturl() 67 | lines = b"\r\n".join(headers_list) 68 | remote_req_headers = b"%s %s %s\r\n%s\r\n\r\n" % ( 69 | request.method, 70 | newpath, 71 | request.ver, 72 | lines, 73 | ) 74 | redundant = parser.readall() 75 | to_send = remote_req_headers + redundant 76 | if to_send: 77 | await via_client.sendall(to_send) 78 | await self.relay(via_client) 79 | -------------------------------------------------------------------------------- /shadowproxy/proxies/shadowsocks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyingbo/shadowproxy/15e5669136dc6d4720aa54dcc5ff1c6839304767/shadowproxy/proxies/shadowsocks/__init__.py -------------------------------------------------------------------------------- /shadowproxy/proxies/shadowsocks/client.py: -------------------------------------------------------------------------------- 1 | from ...utils import pack_addr 2 | from ..base.client import ClientBase 3 | from .parser import ss_reader 4 | 5 | 6 | class SSClient(ClientBase): 7 | proto = "SS" 8 | 9 | async def init(self): 10 | self.ss_parser = ss_reader.parser(self.ns.cipher) 11 | self.plugin = getattr(self.ns, "plugin", None) 12 | if self.plugin: 13 | self.plugin.client = self 14 | await self.plugin.init_client(self) 15 | 16 | async def recv(self, size): 17 | data = await self.sock.recv(size) 18 | if not data: 19 | return data 20 | if self.plugin and hasattr(self.plugin, "decode"): 21 | data = self.plugin.decode(data) 22 | if not data: 23 | return await self.recv(size) 24 | self.ss_parser.send(data) 25 | data = self.ss_parser.read_output_bytes() 26 | if not data: 27 | data = await self.recv(size) 28 | return data 29 | 30 | async def sendall(self, data): 31 | to_send = b"" 32 | if not hasattr(self, "encrypt"): 33 | iv, self.encrypt = self.ns.cipher.make_encrypter() 34 | to_send = iv + self.encrypt(pack_addr(self.target_addr)) 35 | to_send += self.encrypt(data) 36 | plugin = getattr(self.ns, "plugin", None) 37 | if plugin and hasattr(plugin, "encode"): 38 | to_send = plugin.encode(to_send) 39 | await self.sock.sendall(to_send) 40 | -------------------------------------------------------------------------------- /shadowproxy/proxies/shadowsocks/parser.py: -------------------------------------------------------------------------------- 1 | import iofree 2 | 3 | 4 | @iofree.parser 5 | def ss_reader(cipher): 6 | parser = yield from iofree.get_parser() 7 | iv = yield from iofree.read(cipher.IV_SIZE) 8 | decrypt = cipher.make_decrypter(iv) 9 | while True: 10 | data = yield from iofree.read_more() 11 | parser.respond(result=decrypt(data)) 12 | -------------------------------------------------------------------------------- /shadowproxy/proxies/shadowsocks/server.py: -------------------------------------------------------------------------------- 1 | from iofree.contrib.common import Addr 2 | 3 | from ...utils import run_parser_curio 4 | from ..base.server import ProxyBase 5 | from .parser import ss_reader 6 | 7 | 8 | class SSProxy(ProxyBase): 9 | proto = "SS" 10 | 11 | def __init__(self, cipher, bind_addr, via=None, plugin=None, **kwargs): 12 | self.cipher = cipher 13 | self.bind_addr = bind_addr 14 | self.via = via 15 | self.plugin = plugin 16 | self.kwargs = kwargs 17 | self.ss_parser = ss_reader.parser(self.cipher) 18 | 19 | async def _run(self): 20 | if self.plugin: 21 | self.plugin.server = self 22 | self.proto += f"({self.plugin.name})" 23 | await self.plugin.init_server(self.client) 24 | 25 | addr_parser = Addr.get_parser() 26 | addr = await run_parser_curio(addr_parser, self) 27 | self.target_addr = (addr.host, addr.port) 28 | via_client = await self.connect_server(self.target_addr) 29 | 30 | async with via_client: 31 | redundant = addr_parser.readall() 32 | if redundant: 33 | await via_client.sendall(redundant) 34 | await self.relay(via_client) 35 | 36 | async def recv(self, size): 37 | data = await self.client.recv(size) 38 | if not data: 39 | return data 40 | if hasattr(self.plugin, "decode"): 41 | data = self.plugin.decode(data) 42 | if not data: 43 | return await self.recv(size) 44 | self.ss_parser.send(data) 45 | return self.ss_parser.read_output_bytes() 46 | 47 | async def sendall(self, data): 48 | iv = b"" 49 | if not hasattr(self, "encrypt"): 50 | iv, self.encrypt = self.cipher.make_encrypter() 51 | to_send = iv + self.encrypt(data) 52 | if hasattr(self.plugin, "encode"): 53 | to_send = self.plugin.encode(to_send) 54 | await self.client.sendall(to_send) 55 | -------------------------------------------------------------------------------- /shadowproxy/proxies/shadowsocks/udpclient.py: -------------------------------------------------------------------------------- 1 | import curio 2 | 3 | from ... import gvars 4 | from ...utils import pack_addr, unpack_addr 5 | from ..base.udpclient import UDPClient 6 | 7 | 8 | class SSUDPClient(UDPClient): 9 | proto = "SS(UDP)" 10 | 11 | async def sendto(self, data, addr): 12 | self.target_addr = addr 13 | iv, encrypt = self.ns.cipher.make_encrypter() 14 | payload = iv + encrypt(pack_addr(addr) + data) 15 | await self.sock.sendto(payload, self.ns.bind_addr) 16 | 17 | def _unpack(self, data): 18 | iv = data[: self.ns.cipher.IV_SIZE] 19 | decrypt = self.ns.cipher.make_decrypter(iv) 20 | data = decrypt(data[self.ns.cipher.IV_SIZE :]) 21 | addr, payload = unpack_addr(data) 22 | return addr, payload 23 | 24 | async def _relay(self, addr, sendfrom): 25 | try: 26 | while True: 27 | data, raddr = await self.sock.recvfrom(gvars.PACKET_SIZE) 28 | _, payload = self._unpack(data) 29 | await sendfrom(payload, addr) 30 | except curio.errors.CancelledError: 31 | pass 32 | -------------------------------------------------------------------------------- /shadowproxy/proxies/shadowsocks/udpserver.py: -------------------------------------------------------------------------------- 1 | import pylru 2 | 3 | from ... import gvars 4 | from ...utils import ViaNamespace, pack_addr, show, unpack_addr 5 | from ..base.udpclient import UDPClient 6 | from ..base.udpserver import UDPServerBase 7 | 8 | 9 | class SSUDPServer(UDPServerBase): 10 | proto = "SS(UDP)" 11 | 12 | def __init__(self, cipher, bind_addr, via=None, **kwargs): 13 | self.cipher = cipher 14 | self.bind_addr = bind_addr 15 | self.via = via or ViaNamespace(ClientClass=UDPClient) 16 | self.removed = None 17 | self.kwargs = kwargs 18 | 19 | def callback(key, value): 20 | self.removed = (key, value) 21 | 22 | self.via_clients = pylru.lrucache(256, callback) 23 | 24 | async def __call__(self, sock): 25 | listen_addr = sock.getsockname() 26 | while True: 27 | data, addr = await sock.recvfrom(gvars.PACKET_SIZE) 28 | if len(data) <= self.cipher.IV_SIZE: 29 | continue 30 | if addr not in self.via_clients: 31 | via_client = self.via.new() 32 | self.via_clients[addr] = via_client 33 | if self.removed is not None: 34 | await self.removed[1].close() 35 | self.removed = None 36 | via_client = self.via_clients[addr] 37 | 38 | iv = data[: self.cipher.IV_SIZE] 39 | decrypt = self.cipher.make_decrypter(iv) 40 | data = decrypt(data[self.cipher.IV_SIZE :]) 41 | target_addr, payload = unpack_addr(data) 42 | if hasattr(via_client.ns, "bind_addr"): 43 | extra = f" -> {via_client.proto} -> {show(via_client.ns.bind_addr)}" 44 | extra_back = ( 45 | f" <- {via_client.proto} <- {show(via_client.ns.bind_addr)}" 46 | ) 47 | else: 48 | extra = "" 49 | extra_back = "" 50 | msg = ( 51 | f"{show(addr)} -> {self.proto} -> " 52 | f"{show(listen_addr)}{extra} -> {show(target_addr)}" 53 | ) 54 | gvars.logger.info(msg) 55 | await via_client.sendto(payload, target_addr) 56 | 57 | async def sendfrom(data, from_addr): 58 | iv, encrypt = self.cipher.make_encrypter() 59 | payload = encrypt(pack_addr(target_addr) + data) 60 | msg = ( 61 | f"{show(addr)} <- {self.proto} <- " 62 | f"{show(listen_addr)}{extra_back} <- {show(from_addr)}" 63 | ) 64 | gvars.logger.info(msg) 65 | await sock.sendto(iv + payload, addr) 66 | 67 | await via_client.relay(target_addr, sendfrom) 68 | 69 | for via_client in self.via_clients.values(): 70 | await via_client.close() 71 | -------------------------------------------------------------------------------- /shadowproxy/proxies/socks/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # references: 3 | # rfc1928(socks5): https://www.ietf.org/rfc/rfc1928.txt 4 | # asyncio-socks5 https://github.com/RobberPhex/asyncio-socks5 5 | # handshake 6 | # +----+----------+----------+ 7 | # |VER | NMETHODS | METHODS | 8 | # +----+----------+----------+ 9 | # | 1 | 1 | 1 to 255 | 10 | # +----+----------+----------+ 11 | # request 12 | # +----+-----+-------+------+----------+----------+ 13 | # |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | 14 | # +----+-----+-------+------+----------+----------+ 15 | # | 1 | 1 | X'00' | 1 | Variable | 2 | 16 | # +----+-----+-------+------+----------+----------+ 17 | # reply 18 | # +----+-----+-------+------+----------+----------+ 19 | # |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | 20 | # +----+-----+-------+------+----------+----------+ 21 | # | 1 | 1 | X'00' | 1 | Variable | 2 | 22 | # +----+-----+-------+------+----------+----------+ 23 | # udp relay request and reply 24 | # +----+------+------+----------+----------+----------+ 25 | # |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | 26 | # +----+------+------+----------+----------+----------+ 27 | # | 2 | 1 | 1 | Variable | 2 | Variable | 28 | # +----+------+------+----------+----------+----------+ 29 | -------------------------------------------------------------------------------- /shadowproxy/proxies/socks/client.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from curio import socket 4 | 5 | from ...protocols import socks4, socks5 6 | from ...utils import run_parser_curio, set_disposable_recv 7 | from ..base.client import ClientBase 8 | 9 | 10 | class SocksClient(ClientBase): 11 | proto = "SOCKS" 12 | 13 | async def init(self): 14 | auth = getattr(self.ns, "auth", None) 15 | client_parser = socks5.client.parser(auth, self.target_addr) 16 | await run_parser_curio(client_parser, self.sock) 17 | redundant = client_parser.readall() 18 | set_disposable_recv(self.sock, redundant) 19 | 20 | 21 | class Socks4Client(ClientBase): 22 | proto = "SOCKS4" 23 | 24 | async def init(self): 25 | info = await socket.getaddrinfo( 26 | *self.target_addr, socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP 27 | ) 28 | addr = random.choice(info)[-1] 29 | socks4_client_parser = socks4.client.parser(addr) 30 | await run_parser_curio(socks4_client_parser, self.sock) 31 | redundant = socks4_client_parser.readall() 32 | set_disposable_recv(self.sock, redundant) 33 | -------------------------------------------------------------------------------- /shadowproxy/proxies/socks/server.py: -------------------------------------------------------------------------------- 1 | from ...protocols import socks4, socks5 2 | from ...utils import run_parser_curio 3 | from ..base.server import ProxyBase 4 | 5 | 6 | class SocksProxy(ProxyBase): 7 | proto = "SOCKS" 8 | 9 | def __init__(self, bind_addr, auth=None, via=None, plugin=None, **kwargs): 10 | self.bind_addr = bind_addr 11 | self.auth = auth 12 | self.via = via 13 | self.plugin = plugin 14 | self.kwargs = kwargs 15 | 16 | async def _run(self): 17 | socks5_parser = socks5.server.parser(self.auth) 18 | request = await run_parser_curio(socks5_parser, self.client) 19 | self.target_addr = (request.addr.host, request.addr.port) 20 | via_client = await self.connect_server(self.target_addr) 21 | # await self.client.sendall(socks5.resp()) 22 | socks5_parser.send_event(0) 23 | await run_parser_curio(socks5_parser, self.client) 24 | 25 | async with via_client: 26 | redundant = socks5_parser.readall() 27 | if redundant: 28 | await via_client.sendall(redundant) 29 | await self.relay(via_client) 30 | 31 | 32 | class Socks4Proxy(ProxyBase): 33 | proto = "SOCKS4" 34 | 35 | def __init__(self, bind_addr, auth=None, via=None, plugin=None, **kwargs): 36 | self.bind_addr = bind_addr 37 | self.auth = auth 38 | self.via = via 39 | self.plugin = plugin 40 | self.kwargs = kwargs 41 | 42 | async def _run(self): 43 | socks4_parser = socks4.server.parser() 44 | self.target_addr = await run_parser_curio(socks4_parser, self.client) 45 | via_client = await self.connect_server(self.target_addr) 46 | socks4_parser.send_event(0x5A) 47 | await run_parser_curio(socks4_parser, self.client) 48 | 49 | async with via_client: 50 | redundant = socks4_parser.readall() 51 | if redundant: 52 | await via_client.sendall(redundant) 53 | await self.relay(via_client) 54 | -------------------------------------------------------------------------------- /shadowproxy/proxies/transparent/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyingbo/shadowproxy/15e5669136dc6d4720aa54dcc5ff1c6839304767/shadowproxy/proxies/transparent/__init__.py -------------------------------------------------------------------------------- /shadowproxy/proxies/transparent/server.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | from curio import socket 4 | 5 | from ... import gvars 6 | from ..base.server import ProxyBase 7 | 8 | SO_ORIGINAL_DST = 80 9 | 10 | 11 | class TransparentProxy(ProxyBase): 12 | proto = "REDIRECT" 13 | 14 | def __init__(self, bind_addr, via=None, plugin=None, **kwargs): 15 | self.bind_addr = bind_addr 16 | self.via = via 17 | self.plugin = plugin 18 | self.kwargs = kwargs 19 | 20 | async def _run(self): 21 | try: 22 | buf = self.client._socket.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, 16) 23 | port, host = struct.unpack("!2xH4s8x", buf) 24 | self.target_addr = (socket.inet_ntoa(host), port) 25 | except Exception: 26 | gvars.logger.exception(f"{self} isn't a redirect proxy") 27 | 28 | via_client = await self.connect_server(self.target_addr) 29 | async with via_client: 30 | await self.relay(via_client) 31 | -------------------------------------------------------------------------------- /shadowproxy/proxies/transparent/udpserver.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import weakref 3 | 4 | import pylru 5 | from curio import socket 6 | 7 | from ...utils import ViaNamespace, is_global 8 | from ..base.udpclient import UDPClient 9 | from ..base.udpserver import UDPServerBase 10 | 11 | IP_TRANSPARENT = 19 12 | IP_ORIGDSTADDR = 20 13 | IP_RECVORIGDSTADDR = IP_ORIGDSTADDR 14 | 15 | 16 | class TransparentUDPServer(UDPServerBase): 17 | proto = "RED(UDP)" 18 | 19 | def __init__(self, bind_addr, via=None, **kwargs): 20 | self.bind_addr = bind_addr 21 | self.via = via or ViaNamespace(ClientClass=UDPClient) 22 | self.removed = None 23 | self.kwargs = kwargs 24 | 25 | def callback(key, value): 26 | self.removed = (key, value) 27 | 28 | self.via_clients = pylru.lrucache(256, callback) 29 | self.bind_socks = weakref.WeakValueDictionary() 30 | 31 | @staticmethod 32 | def get_origin_dst(ancdata): 33 | for cmsg_level, cmsg_type, cmsg_data in ancdata: 34 | if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVORIGDSTADDR: 35 | family, port, ip = struct.unpack("!HH4s8x", cmsg_data) 36 | return (socket.inet_ntoa(ip), port) 37 | 38 | async def __call__(self, sock): 39 | sock.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, True) 40 | sock.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) 41 | while True: 42 | data, ancdata, msg_flags, addr = await sock.recvmsg( 43 | 8192, socket.CMSG_SPACE(24) 44 | ) 45 | target_addr = self.get_origin_dst(ancdata) 46 | if target_addr is None: 47 | continue 48 | elif not is_global(target_addr[0]): 49 | continue 50 | if addr not in self.via_clients: 51 | via_client = self.via.new() 52 | self.via_clients[addr] = via_client 53 | if self.removed is not None: 54 | await self.removed[1].close() 55 | self.removed = None 56 | via_client = self.via_clients[addr] 57 | await via_client.sendto(data, target_addr) 58 | 59 | async def sendfrom(data, from_addr): 60 | if from_addr not in self.bind_socks: 61 | sender = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 62 | sender.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 63 | sender.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1) 64 | sender.bind(from_addr) 65 | self.bind_socks[from_addr] = sender 66 | sender = self.bind_socks[from_addr] 67 | await sender.sendto(data, addr) 68 | 69 | await via_client.relay(target_addr, sendfrom) 70 | 71 | for via_client in self.via_clients.values(): 72 | await via_client.close() 73 | for sender in self.bind_socks.values(): 74 | await sender.close() 75 | -------------------------------------------------------------------------------- /shadowproxy/proxies/tunnel/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guyingbo/shadowproxy/15e5669136dc6d4720aa54dcc5ff1c6839304767/shadowproxy/proxies/tunnel/__init__.py -------------------------------------------------------------------------------- /shadowproxy/proxies/tunnel/udpserver.py: -------------------------------------------------------------------------------- 1 | import pylru 2 | 3 | from ... import gvars 4 | from ...utils import ViaNamespace 5 | from ..base.udpclient import UDPClient 6 | from ..base.udpserver import UDPServerBase 7 | 8 | 9 | class TunnelUDPServer(UDPServerBase): 10 | proto = "TUNNEL(UDP)" 11 | 12 | def __init__(self, target_addr, bind_addr, via=None, **kwargs): 13 | self.target_addr = target_addr 14 | self.bind_addr = bind_addr 15 | self.via = via or ViaNamespace(ClientClass=UDPClient) 16 | self.removed = None 17 | self.kwargs = kwargs 18 | 19 | def callback(key, value): 20 | self.removed = (key, value) 21 | 22 | self.via_clients = pylru.lrucache(256, callback) 23 | 24 | async def __call__(self, sock): 25 | while True: 26 | data, addr = await sock.recvfrom(gvars.PACKET_SIZE) 27 | if addr not in self.via_clients: 28 | via_client = self.via.new() 29 | self.via_clients[addr] = via_client 30 | if self.removed is not None: 31 | await self.removed[1].close() 32 | self.removed = None 33 | via_client = self.via_clients[addr] 34 | 35 | async def sendfrom(data, from_addr): 36 | await sock.sendto(data, addr) 37 | 38 | await via_client.sendto(data, self.target_addr) 39 | await via_client.relay(self.target_addr, sendfrom) 40 | 41 | for via_client in self.via_clients.values(): 42 | await via_client.close() 43 | -------------------------------------------------------------------------------- /shadowproxy/utils.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | import socket 3 | 4 | import curio 5 | 6 | import iofree 7 | 8 | from . import gvars 9 | 10 | 11 | async def run_parser_curio(parser, sock): 12 | parser.send(b"") 13 | while True: 14 | for to_send, close, exc, result in parser: 15 | if to_send: 16 | await sock.sendall(to_send) 17 | if close: 18 | await sock.close() 19 | if exc: 20 | raise exc 21 | if result is not iofree._no_result: 22 | return result 23 | data = await sock.recv(gvars.PACKET_SIZE) 24 | if not data: 25 | raise iofree.ParseError("need data") 26 | parser.send(data) 27 | 28 | 29 | def is_global(host: str) -> bool: 30 | if host == "localhost": 31 | return False 32 | try: 33 | address = ipaddress.ip_address(host) 34 | except ValueError: 35 | return True 36 | return address.is_global 37 | 38 | 39 | def pack_bytes(data: bytes, length: int = 1) -> bytes: 40 | return len(data).to_bytes(length, "big") + data 41 | 42 | 43 | def pack_addr(addr) -> bytes: 44 | host, port = addr 45 | try: # IPV4 46 | packed = b"\x01" + socket.inet_aton(host) 47 | except OSError: 48 | try: # IPV6 49 | packed = b"\x04" + socket.inet_pton(socket.AF_INET6, host) 50 | except OSError: # hostname 51 | packed = host.encode("ascii") 52 | packed = b"\x03" + len(packed).to_bytes(1, "big") + packed 53 | return packed + port.to_bytes(2, "big") 54 | 55 | 56 | def unpack_addr(data: bytes): 57 | atyp = data[0] 58 | if atyp == 1: # IPV4 59 | end = 5 60 | ipv4 = data[1:end] 61 | host = socket.inet_ntoa(ipv4) 62 | elif atyp == 4: # IPV6 63 | end = 17 64 | ipv6 = data[1:end] 65 | host = socket.inet_ntop(socket.AF_INET6, ipv6) 66 | elif atyp == 3: # hostname 67 | length = data[1] 68 | end = 2 + length 69 | host = data[2:end].decode("ascii") 70 | else: 71 | raise Exception(f"unknow atyp: {atyp}") 72 | port = int.from_bytes(data[end : end + 2], "big") 73 | return (host, port), data[end + 2 :] 74 | 75 | 76 | def human_bytes(val: int) -> str: 77 | if val < 1024: 78 | return f"{val:.0f}Bytes" 79 | elif val < 1048576: 80 | return f"{val/1024:.1f}KB" 81 | else: 82 | return f"{val/1048576:.1f}MB" 83 | 84 | 85 | def human_speed(speed: int) -> str: 86 | if speed < 1024: 87 | return f"{speed:.0f} B/s" 88 | elif speed < 1048576: 89 | return f"{speed/1024:.1f} KB/s" 90 | else: 91 | return f"{speed/1048576:.1f} MB/s" 92 | 93 | 94 | async def open_connection(host, port, **kwargs): 95 | for i in range(2, -1, -1): 96 | try: 97 | return await curio.open_connection(host, port, **kwargs) 98 | except socket.gaierror: 99 | if i == 0: 100 | gvars.logger.debug(f"dns query failed: {host}") 101 | raise 102 | 103 | 104 | def set_disposable_recv(sock, redundant): 105 | if redundant: 106 | recv = sock.recv 107 | 108 | async def disposable_recv(size): 109 | sock.recv = recv 110 | return redundant 111 | 112 | sock.recv = disposable_recv 113 | 114 | 115 | class ViaNamespace(dict): 116 | def __getattr__(self, k): 117 | try: 118 | return self[k] 119 | except KeyError: 120 | raise AttributeError 121 | 122 | @property 123 | def bind_address(self): 124 | return f"{self.bind_addr[0]}:{self.bind_addr[1]}" 125 | 126 | def new(self): 127 | return self.ClientClass(self) 128 | 129 | 130 | def show(addr): 131 | return f"{addr[0]}:{addr[1]}" 132 | -------------------------------------------------------------------------------- /tests/test_ciphers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | 4 | from shadowproxy.ciphers import StreamCipher, ciphers 5 | 6 | 7 | def test_ciphers(): 8 | for Cipher in ciphers.values(): 9 | cipher = Cipher("password") 10 | if isinstance(cipher, StreamCipher): 11 | iv, encrypt = cipher.make_encrypter() 12 | decrypt = cipher.make_decrypter(iv) 13 | for i in range(100): 14 | plaintext = os.urandom(random.randint(1, 100)) 15 | ciphertext = encrypt(plaintext) 16 | assert decrypt(ciphertext) == plaintext 17 | else: 18 | salt, encrypt = cipher.make_encrypter() 19 | decrypt = cipher.make_decrypter(salt) 20 | 21 | for i in range(100): 22 | plaintext = os.urandom(random.randint(1, 100)) 23 | ciphertext, tag = encrypt(plaintext) 24 | assert decrypt(ciphertext, tag) == plaintext 25 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import signal 4 | 5 | import pytest 6 | 7 | from shadowproxy.__main__ import get_server, main 8 | 9 | 10 | def test_cli(): 11 | with pytest.raises(argparse.ArgumentTypeError): 12 | get_server("ss://") 13 | 14 | 15 | def test_main(): 16 | def handler(*args): 17 | os.kill(os.getpid(), signal.SIGINT) 18 | 19 | signal.signal(signal.SIGALRM, handler) 20 | signal.alarm(3) 21 | main(["-v", "ss://chacha20:1@:0"]) 22 | -------------------------------------------------------------------------------- /tests/test_http_request.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | import curio 4 | 5 | from shadowproxy import gvars 6 | from shadowproxy.__main__ import get_client, get_server 7 | 8 | gvars.logger.setLevel(10) 9 | # url_https = "https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js" 10 | # url_http = "http://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js" 11 | url_https = "https://docs.python.org/3/_static/pygments.css" 12 | url_http = "http://docs.python.org/3/_static/pygments.css" 13 | # url_https = "https://httpbin.org/ip" 14 | # url_http = "http://httpbin.org/ip" 15 | 16 | 17 | async def make_request(client, url=None): 18 | if url is None: 19 | url = url_http 20 | headers = ["User-Agent: curl/7.54.0", "Accept: */*"] 21 | async with client: 22 | async with curio.timeout_after(40): 23 | response = await client.http_request(url, headers=headers) 24 | assert response.size > 0 25 | 26 | 27 | async def main(coro, *server_coros): 28 | async with curio.TaskGroup() as g: 29 | for server_coro in server_coros: 30 | await g.spawn(server_coro) 31 | task = await g.spawn(coro) 32 | await task.join() 33 | await g.cancel_remaining() 34 | 35 | 36 | def my_test_ipv6(): 37 | server, bind_addr, _ = get_server("http://user:password@[::1]:0") 38 | bind_address = f"{bind_addr[0]}:{bind_addr[1]}" 39 | client = get_client(f"http://user:password@{bind_address}") 40 | curio.run(main(make_request(client), server)) 41 | 42 | 43 | def test_http(): 44 | server, bind_addr, _ = get_server("http://user:password@127.0.0.1:0") 45 | bind_address = f"{bind_addr[0]}:{bind_addr[1]}" 46 | client = get_client(f"http://user:password@{bind_address}") 47 | curio.run(main(make_request(client), server)) 48 | 49 | 50 | def test_http_forward(): 51 | server, bind_addr, _ = get_server("http://user:password@127.0.0.1:0") 52 | bind_address = f"{bind_addr[0]}:{bind_addr[1]}" 53 | client = get_client(f"forward://user:password@{bind_address}") 54 | curio.run(main(make_request(client, url_http), server)) 55 | 56 | 57 | def test_socks5(): 58 | server, bind_addr, _ = get_server("socks://127.0.0.1:0") 59 | bind_address = f"{bind_addr[0]}:{bind_addr[1]}" 60 | client = get_client(f"socks://{bind_address}") 61 | curio.run(main(make_request(client), server)) 62 | 63 | 64 | def test_socks4(): 65 | server, bind_addr, _ = get_server("socks4://127.0.0.1:0") 66 | bind_address = f"{bind_addr[0]}:{bind_addr[1]}" 67 | client = get_client(f"socks4://{bind_address}") 68 | curio.run(main(make_request(client), server)) 69 | 70 | 71 | def test_socks5_with_auth(): 72 | server, bind_addr, _ = get_server("socks://user:password@127.0.0.1:0") 73 | bind_address = f"{bind_addr[0]}:{bind_addr[1]}" 74 | client = get_client(f"socks://user:password@{bind_address}") 75 | curio.run(main(make_request(client), server)) 76 | 77 | 78 | def test_ss(): 79 | server, bind_addr, _ = get_server("ss://aes-256-cfb:123456@127.0.0.1:0") 80 | bind_address = f"{bind_addr[0]}:{bind_addr[1]}" 81 | client = get_client(f"ss://aes-256-cfb:123456@{bind_address}") 82 | curio.run(main(make_request(client), server)) 83 | 84 | 85 | def test_ss_http_simple(): 86 | server, bind_addr, _ = get_server( 87 | "ss://chacha20:123456@127.0.0.1:0/?plugin=http_simple" 88 | ) 89 | bind_address = f"{bind_addr[0]}:{bind_addr[1]}" 90 | client = get_client(f"ss://chacha20:123456@{bind_address}/?plugin=http_simple") 91 | curio.run(main(make_request(client), server)) 92 | 93 | 94 | def test_ss_over_tls(): 95 | server, bind_addr, _ = get_server("ss://chacha20:1@127.0.0.1:0/?plugin=tls1.2") 96 | bind_address = f"{bind_addr[0]}:{bind_addr[1]}" 97 | client = get_client(f"ss://chacha20:1@{bind_address}/?plugin=tls1.2") 98 | curio.run(main(make_request(client), server)) 99 | 100 | 101 | def test_aead(): 102 | server, bind_addr, _ = get_server("ss://aes-128-gcm:123456@127.0.0.1:0") 103 | bind_address = f"{bind_addr[0]}:{bind_addr[1]}" 104 | client = get_client(f"ss://aes-128-gcm:123456@{bind_address}") 105 | curio.run(main(make_request(client), server)) 106 | 107 | 108 | async def job(): 109 | assert subprocess.run(["curl", "-I", "https://1.1.1.1/"]).returncode == 0 110 | 111 | 112 | def test_transparent(): 113 | server, bind_addr, _ = get_server("red://0.0.0.0:12345") 114 | curio.run(main(job, server)) 115 | 116 | 117 | def test_via(): 118 | via_server, bind_addr, _ = get_server("ss://chacha20:1@127.0.0.1:0") 119 | via_address = f"{bind_addr[0]}:{bind_addr[1]}" 120 | server, bind_addr, _ = get_server( 121 | f"socks://127.0.0.1:0/?via=ss://chacha20:1@{via_address}" 122 | ) 123 | bind_address = f"{bind_addr[0]}:{bind_addr[1]}" 124 | client = get_client(f"socks://{bind_address}") 125 | curio.run(main(make_request(client), server, via_server)) 126 | 127 | 128 | def test_http_via(): 129 | via_server, bind_addr, _ = get_server("http://:0") 130 | via_address = f"{bind_addr[0]}:{bind_addr[1]}" 131 | server, bind_addr, _ = get_server(f"http://127.0.0.1:0/?via=http://{via_address}") 132 | bind_address = f"{bind_addr[0]}:{bind_addr[1]}" 133 | client = get_client(f"http://{bind_address}") 134 | curio.run(main(make_request(client), server, via_server)) 135 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from ipaddress import ip_address 3 | 4 | import curio 5 | import pytest 6 | 7 | from shadowproxy.utils import ( 8 | human_bytes, 9 | human_speed, 10 | is_global, 11 | open_connection, 12 | pack_addr, 13 | unpack_addr, 14 | ) 15 | 16 | 17 | def test_is_global(): 18 | assert not is_global("127.0.0.1") 19 | assert not is_global("192.168.20.168") 20 | assert is_global("211.13.20.168") 21 | assert is_global("google.com") 22 | 23 | 24 | def test_pack_addr(): 25 | assert pack_addr(("127.0.0.1", 8080)) == b"\x01\x7f\x00\x00\x01\x1f\x90" 26 | ipv6 = "1050:0:0:0:5:600:300c:326b" 27 | data = pack_addr((ipv6, 80)) 28 | assert ip_address(unpack_addr(data)[0][0]) == ip_address(ipv6) 29 | 30 | 31 | def test_unpack_addr(): 32 | addr = ("232.32.9.86", 49238) 33 | assert unpack_addr(pack_addr(addr))[0] == addr 34 | addr = ("google.com", 80) 35 | assert unpack_addr(pack_addr(addr))[0] == addr 36 | with pytest.raises(Exception): 37 | unpack_addr(b"\x02") 38 | 39 | 40 | def test_human_bytes(): 41 | assert human_bytes(10) == "10Bytes" 42 | assert human_bytes(1025) == "1.0KB" 43 | assert human_bytes(1024 * 1024 + 1) == "1.0MB" 44 | 45 | 46 | def test_human_speed(): 47 | assert human_speed(10) == "10 B/s" 48 | assert human_speed(1024) == "1.0 KB/s" 49 | assert human_speed(1024 * 1024 + 1) == "1.0 MB/s" 50 | 51 | 52 | def test_open_connection(): 53 | with pytest.raises(socket.gaierror): 54 | curio.run(open_connection("does-not-exists.com", 80)) 55 | -------------------------------------------------------------------------------- /tests/test_protocols.py: -------------------------------------------------------------------------------- 1 | import os 2 | import secrets 3 | 4 | from shadowproxy.ciphers import AES128GCM, AES256CFB 5 | from shadowproxy.proxies.aead.parser import aead_reader 6 | from shadowproxy.proxies.shadowsocks.parser import ss_reader 7 | 8 | 9 | def test_ss(): 10 | cipher = AES256CFB(secrets.token_urlsafe(20)) 11 | iv, encrypt = cipher.make_encrypter() 12 | length = len(iv) // 2 13 | parser = ss_reader.parser(cipher) 14 | parser.send(iv[:length]) 15 | assert parser.read_output_bytes() == b"" 16 | data = os.urandom(20) 17 | parser.send(iv[length:] + encrypt(data)) 18 | assert parser.read_output_bytes() == data 19 | 20 | 21 | def test_ss2(): 22 | cipher = AES256CFB(secrets.token_urlsafe(20)) 23 | iv, encrypt = cipher.make_encrypter() 24 | parser = ss_reader.parser(cipher) 25 | parser.send(iv) 26 | assert parser.read_output_bytes() == b"" 27 | assert parser.read_output_bytes() == b"" 28 | data = os.urandom(20) 29 | parser.send(encrypt(data)) 30 | assert parser.read_output_bytes() == data 31 | 32 | 33 | def test_aead(): 34 | cipher = AES128GCM(secrets.token_urlsafe(20)) 35 | salt, encrypt = cipher.make_encrypter() 36 | length = len(salt) // 2 37 | aead = aead_reader.parser(cipher) 38 | aead.send(salt[:length]) 39 | assert aead.read_output_bytes() == b"" 40 | data = os.urandom(20) 41 | aead.send(salt[length:] + b"".join(encrypt(len(data).to_bytes(2, "big")))) 42 | assert aead.read_output_bytes() == b"" 43 | aead.send(b"".join(encrypt(data))) 44 | assert aead.read_output_bytes() == data 45 | -------------------------------------------------------------------------------- /tests/test_udp.py: -------------------------------------------------------------------------------- 1 | import curio 2 | from curio import subprocess 3 | 4 | from shadowproxy import gvars 5 | from shadowproxy.__main__ import get_server 6 | 7 | gvars.logger.setLevel(10) 8 | 9 | 10 | async def make_request(bind_addr): 11 | async with curio.timeout_after(60): 12 | r = await subprocess.run( 13 | ["dig", "+short", f"@{bind_addr[0]}", "-p", f"{bind_addr[1]}", "baidu.com"] 14 | ) 15 | assert r.returncode == 0 16 | 17 | 18 | async def main(bind_addr, *server_coros): 19 | async with curio.TaskGroup() as g: 20 | for server_coro in server_coros: 21 | await g.spawn(server_coro) 22 | task = await g.spawn(make_request, bind_addr) 23 | await task.join() 24 | await g.cancel_remaining() 25 | 26 | 27 | def test_tunneludp(): 28 | server, bind_addr, _ = get_server( 29 | "tunneludp://127.0.0.1:0?target=1.1.1.1:53&source_ip=in" 30 | ) 31 | curio.run(main(bind_addr, server)) 32 | 33 | 34 | def test_ssudp(): 35 | server, bind_addr, _ = get_server("ssudp://chacha20:1@127.0.0.1:0") 36 | address = f"{bind_addr[0]}:{bind_addr[1]}" 37 | server2, bind_addr2, _ = get_server( 38 | f"tunneludp://127.0.0.1:0/?target=1.1.1.1:53&via=ssudp://chacha20:1@{address}" 39 | ) 40 | curio.run(main(bind_addr2, server, server2)) 41 | --------------------------------------------------------------------------------