├── lightsocks ├── __init__.py ├── core │ ├── __init__.py │ ├── test_cipher.py │ ├── password.py │ ├── cipher.py │ ├── test_password.py │ ├── securesocket.py │ └── test_securesocket.py ├── utils │ ├── __init__.py │ ├── net.py │ └── config.py ├── local.py ├── test_local.py ├── server.py └── test_server.py ├── requirements.txt ├── .coveralls.yml ├── .travis.yml ├── .coveragerc ├── LICENSE ├── .gitignore ├── README.md ├── lsserver.py └── lslocal.py /lightsocks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lightsocks/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lightsocks/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | typing==3.6.2 2 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | repo_token: RDQCSHcbveePZLzPOQFjJabfC5DnaPpez 3 | -------------------------------------------------------------------------------- /lightsocks/utils/net.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | 4 | Address = namedtuple('Address', 'ip port') 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | os: 3 | - linux 4 | python: 5 | - "3.6" 6 | - "3.6-dev" 7 | - "3.7-dev" 8 | install: 9 | - pip install -r requirements.txt 10 | - pip install coveralls 11 | before_script: 12 | - sudo sysctl -w net.ipv6.conf.lo.disable_ipv6=0 net.ipv6.conf.default.disable_ipv6=0 net.ipv6.conf.all.disable_ipv6=0 13 | script: 14 | - coverage run -m unittest discover -v 15 | after_success: 16 | - coveralls 17 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | **/test_*.py 4 | 5 | [report] 6 | # Regexes for lines to exclude from consideration 7 | exclude_lines = 8 | # Have to re-enable the standard pragma 9 | pragma: no cover 10 | 11 | # Don't complain about missing debug-only code: 12 | def __repr__ 13 | if self\.debug 14 | 15 | # Don't complain if tests don't hit defensive assertion code: 16 | raise AssertionError 17 | raise NotImplementedError 18 | 19 | # Don't complain if non-runnable code isn't run: 20 | if 0: 21 | if __name__ == .__main__.: 22 | 23 | ignore_errors = True 24 | 25 | [html] 26 | directory = coverage_html_report 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 林玮 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 | -------------------------------------------------------------------------------- /lightsocks/core/test_cipher.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import random 3 | 4 | from lightsocks.core.cipher import Cipher 5 | from lightsocks.core.password import IDENTITY_PASSWORD, randomPassword 6 | 7 | 8 | class TestCipher(unittest.TestCase): 9 | def test_encryption(self): 10 | password = randomPassword() 11 | 12 | original_data = bytearray() 13 | for _ in range(0xffff): 14 | original_data.append(random.randint(0, 255)) 15 | 16 | cipher = Cipher.NewCipher(password) 17 | data = original_data.copy() 18 | 19 | cipher.encode(data) 20 | self.assertNotEqual(data, original_data) 21 | cipher.decode(data) 22 | self.assertEqual(data, original_data) 23 | 24 | def test_no_encryption(self): 25 | password = IDENTITY_PASSWORD.copy() 26 | 27 | original_data = bytearray() 28 | for _ in range(0xffff): 29 | original_data.append(random.randint(0, 255)) 30 | 31 | cipher = Cipher.NewCipher(password) 32 | data = original_data.copy() 33 | 34 | cipher.encode(data) 35 | self.assertEqual(data, original_data) 36 | cipher.decode(data) 37 | self.assertEqual(data, original_data) 38 | -------------------------------------------------------------------------------- /lightsocks/core/password.py: -------------------------------------------------------------------------------- 1 | """ 2 | this module is for producing a valid password 3 | that for Cipher to encode and decode the data flow. 4 | """ 5 | import random 6 | import base64 7 | 8 | PASSWORD_LENGTH = 256 9 | IDENTITY_PASSWORD = bytearray(range(256)) 10 | 11 | 12 | class InvalidPasswordError(Exception): 13 | """不合法的密码""" 14 | 15 | 16 | def validatePassword(password: bytearray) -> bool: 17 | return len(password) == PASSWORD_LENGTH and len( 18 | set(password)) == PASSWORD_LENGTH 19 | 20 | 21 | def loadsPassword(passwordString: str) -> bytearray: 22 | try: 23 | password = base64.urlsafe_b64decode( 24 | passwordString.encode('utf8', errors='strict')) 25 | password = bytearray(password) 26 | except: 27 | raise InvalidPasswordError 28 | 29 | if not validatePassword(password): 30 | raise InvalidPasswordError 31 | 32 | return password 33 | 34 | 35 | def dumpsPassword(password: bytearray) -> str: 36 | if not validatePassword(password): 37 | raise InvalidPasswordError 38 | return base64.urlsafe_b64encode(password).decode('utf8', errors='strict') 39 | 40 | 41 | def randomPassword() -> bytearray: 42 | password = IDENTITY_PASSWORD.copy() 43 | random.shuffle(password) 44 | return password 45 | -------------------------------------------------------------------------------- /lightsocks/core/cipher.py: -------------------------------------------------------------------------------- 1 | class Cipher: 2 | """ 3 | Cipher class is for the encipherment of data flow. 4 | One octet is in the range 0 ~ 255 (2 ^ 8). 5 | To do encryption, it just maps one byte to another one. 6 | Example: 7 | encodePassword 8 | | index | 0x00 | 0x01 | 0x02 | 0x03 | ... | 0xff | || 0x02ff0a04 9 | | ----- | ---- | ---- | ---- | ---- | --- | ---- | || 10 | | value | 0x01 | 0x02 | 0x03 | 0x04 | ... | 0x00 | \/ 0x03000b05 11 | decodePassword 12 | | index | 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | ... | || 0x03000b05 13 | | ----- | ---- | ---- | ---- | ---- | ---- | --- | || 14 | | value | 0xff | 0x00 | 0x01 | 0x02 | 0x03 | ... | \/ 0x02ff0a04 15 | It just shifts one step to make a simply encryption, encode and decode. 16 | """ 17 | 18 | def __init__(self, encodePassword: bytearray, 19 | decodePassword: bytearray) -> None: 20 | self.encodePassword = encodePassword.copy() 21 | self.decodePassword = decodePassword.copy() 22 | 23 | def encode(self, bs: bytearray): 24 | for i, v in enumerate(bs): 25 | bs[i] = self.encodePassword[v] 26 | 27 | def decode(self, bs: bytearray): 28 | for i, v in enumerate(bs): 29 | bs[i] = self.decodePassword[v] 30 | 31 | @classmethod 32 | def NewCipher(cls, encodePassword: bytearray): 33 | decodePassword = encodePassword.copy() 34 | for i, v in enumerate(encodePassword): 35 | decodePassword[v] = i 36 | return cls(encodePassword, decodePassword) 37 | -------------------------------------------------------------------------------- /lightsocks/core/test_password.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import random 3 | import base64 4 | 5 | from lightsocks.core.password import (IDENTITY_PASSWORD, randomPassword, 6 | validatePassword, loadsPassword, 7 | dumpsPassword, InvalidPasswordError) 8 | 9 | 10 | class TestCipher(unittest.TestCase): 11 | def test_randomPassword(self): 12 | for idx in range(0xff): 13 | with self.subTest(idx): 14 | password = randomPassword() 15 | isValid = validatePassword(password) 16 | self.assertTrue(isValid) 17 | 18 | def test_dumps_and_loads_succeed(self): 19 | password = randomPassword() 20 | 21 | string = dumpsPassword(password) 22 | 23 | loaded_password = loadsPassword(string) 24 | 25 | self.assertEqual(password, loaded_password) 26 | 27 | def test_dumps_and_loads_fail(self): 28 | password = randomPassword() 29 | password[random.randint(1, 255)] = 0 30 | 31 | with self.assertRaises(InvalidPasswordError): 32 | dumpsPassword(password) 33 | 34 | string = base64.encodebytes(password).decode('utf8', errors='strict') 35 | 36 | with self.assertRaises(InvalidPasswordError): 37 | loadsPassword(string) 38 | 39 | password = randomPassword() 40 | password = password[:-2] 41 | 42 | with self.assertRaises(InvalidPasswordError): 43 | dumpsPassword(password) 44 | 45 | string = dumpsPassword(IDENTITY_PASSWORD) 46 | string = string[:-3] 47 | 48 | with self.assertRaises(InvalidPasswordError): 49 | loadsPassword(string) 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # vscode 104 | .vscode/ 105 | 106 | # gitignore 107 | .gitignore 108 | -------------------------------------------------------------------------------- /lightsocks/core/securesocket.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import socket 3 | import asyncio 4 | 5 | from .cipher import Cipher 6 | 7 | BUFFER_SIZE = 1024 8 | Connection = socket.socket 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class SecureSocket: 13 | """ 14 | SecureSocket is a socket, 15 | that has the ability to decode read and encode write. 16 | """ 17 | def __init__(self, loop: asyncio.AbstractEventLoop, 18 | cipher: Cipher) -> None: 19 | self.loop = loop or asyncio.get_event_loop() 20 | self.cipher = cipher 21 | 22 | async def decodeRead(self, conn: Connection): 23 | data = await self.loop.sock_recv(conn, BUFFER_SIZE) 24 | 25 | logger.debug('%s:%d decodeRead %r', *conn.getsockname(), data) 26 | 27 | bs = bytearray(data) 28 | self.cipher.decode(bs) 29 | return bs 30 | 31 | async def encodeWrite(self, conn: Connection, bs: bytearray): 32 | logger.debug('%s:%d encodeWrite %s', *conn.getsockname(), bytes(bs)) 33 | 34 | bs = bs.copy() 35 | 36 | self.cipher.encode(bs) 37 | await self.loop.sock_sendall(conn, bs) 38 | 39 | async def encodeCopy(self, dst: Connection, src: Connection): 40 | """ 41 | It encodes the data flow from the src and sends to dst. 42 | """ 43 | logger.debug('encodeCopy %s:%d => %s:%d', 44 | *src.getsockname(), *dst.getsockname()) 45 | 46 | while True: 47 | data = await self.loop.sock_recv(src, BUFFER_SIZE) 48 | if not data: 49 | break 50 | 51 | await self.encodeWrite(dst, bytearray(data)) 52 | 53 | async def decodeCopy(self, dst: Connection, src: Connection): 54 | """ 55 | It decodes the data flow from the src and sends to dst. 56 | """ 57 | logger.debug('decodeCopy %s:%d => %s:%d', 58 | *src.getsockname(), *dst.getsockname()) 59 | 60 | while True: 61 | bs = await self.decodeRead(src) 62 | if not bs: 63 | break 64 | 65 | await self.loop.sock_sendall(dst, bs) 66 | -------------------------------------------------------------------------------- /lightsocks/utils/config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import typing 3 | from collections import namedtuple 4 | from urllib.parse import urlparse 5 | 6 | from lightsocks.core.password import (InvalidPasswordError, dumpsPassword, 7 | loadsPassword) 8 | 9 | Config = namedtuple('Config', 10 | 'serverAddr serverPort localAddr localPort password') 11 | 12 | 13 | class InvalidURLError(Exception): 14 | """无效的config URL""" 15 | 16 | 17 | class InvalidFileError(Exception): 18 | """无效的配置文件""" 19 | 20 | 21 | def loadURL(url: str) -> Config: 22 | url = urlparse(url) 23 | serverAddr = url.hostname 24 | serverPort = url.port 25 | password = url.fragment 26 | 27 | try: 28 | # 验证 密码 有效性 29 | password = loadsPassword(password) 30 | except InvalidPasswordError: 31 | raise InvalidURLError 32 | 33 | # TODO: 验证 Addr 有效性 34 | 35 | # TODO: 验证 Port 有效性 36 | return Config( 37 | serverAddr=serverAddr, 38 | serverPort=serverPort, 39 | localAddr='127.0.0.1', 40 | localPort=1080, 41 | password=password) 42 | 43 | 44 | def dumpURL(config: Config) -> str: 45 | config = config._replace(password=dumpsPassword(config.password)) 46 | 47 | url_temp = 'http://{serverAddr}:{serverPort}/#{password}' 48 | 49 | url = url_temp.format_map(config._asdict()) 50 | 51 | return url 52 | 53 | 54 | def dumps(config: Config) -> str: 55 | config = config._replace(password=dumpsPassword(config.password)) 56 | return json.dumps(config._asdict(), indent=2) 57 | 58 | 59 | def loads(string: str) -> Config: 60 | try: 61 | data = json.loads(string) 62 | config = Config(**data) 63 | 64 | config = config._replace(password=loadsPassword(config.password)) 65 | 66 | # TODO: 验证 Addr 有效性 67 | 68 | # TODO: 验证 Port 有效性 69 | except Exception: 70 | raise InvalidFileError 71 | 72 | return config 73 | 74 | 75 | def dump(f: typing.TextIO, config: Config) -> None: 76 | config = config._replace(password=dumpsPassword(config.password)) 77 | 78 | json.dump(config._asdict(), f, indent=2) 79 | 80 | 81 | def load(f: typing.TextIO) -> Config: 82 | try: 83 | data = json.load(f) 84 | config = Config(**data) 85 | 86 | config = config._replace(password=loadsPassword(config.password)) 87 | 88 | # TODO: 验证 Addr 有效性 89 | 90 | # TODO: 验证 Port 有效性 91 | except Exception: 92 | raise InvalidFileError 93 | 94 | return config 95 | -------------------------------------------------------------------------------- /lightsocks/core/test_securesocket.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import socket 3 | import unittest 4 | 5 | from lightsocks.core.cipher import Cipher 6 | from lightsocks.core.password import randomPassword 7 | from lightsocks.core.securesocket import SecureSocket 8 | 9 | 10 | class TestSecuresocket(unittest.TestCase): 11 | def setUp(self): 12 | self.ls_local, self.ls_server = socket.socketpair() 13 | password = randomPassword() 14 | self.loop = asyncio.new_event_loop() 15 | self.cipher = Cipher.NewCipher(password) 16 | self.securesocket = SecureSocket(loop=self.loop, cipher=self.cipher) 17 | self.msg = bytearray(b'hello world') 18 | self.encripted_msg = self.msg.copy() 19 | self.cipher.encode(self.encripted_msg) 20 | 21 | def tearDown(self): 22 | self.loop.close() 23 | self.ls_local.close() 24 | self.ls_server.close() 25 | 26 | def test_decodeRead(self): 27 | 28 | self.ls_local.send(self.encripted_msg) 29 | self.ls_server.setblocking(False) 30 | 31 | received_msg = self.loop.run_until_complete( 32 | self.securesocket.decodeRead(self.ls_server)) 33 | 34 | self.assertEqual(received_msg, self.msg) 35 | 36 | def test_encodeWrite(self): 37 | 38 | self.ls_local.setblocking(False) 39 | self.loop.run_until_complete( 40 | self.securesocket.encodeWrite(self.ls_local, self.msg)) 41 | 42 | received_msg = self.ls_server.recv(1024) 43 | 44 | self.assertEqual(bytearray(received_msg), self.encripted_msg) 45 | 46 | def test_decodeCopy(self): 47 | dstServer, ls_server_conn = socket.socketpair() 48 | ls_server_conn.setblocking(False) 49 | self.ls_server.setblocking(False) 50 | 51 | self.ls_local.sendall(self.encripted_msg * 10) 52 | self.ls_local.close() 53 | 54 | self.loop.run_until_complete( 55 | self.securesocket.decodeCopy(ls_server_conn, self.ls_server)) 56 | received_msg = dstServer.recv(1024) 57 | 58 | self.assertEqual(bytearray(received_msg), self.msg * 10) 59 | 60 | dstServer.close() 61 | ls_server_conn.close() 62 | 63 | def test_encodeCopy(self): 64 | user_client, ls_local_conn = socket.socketpair() 65 | ls_local_conn.setblocking(False) 66 | self.ls_local.setblocking(False) 67 | 68 | user_client.sendall(self.msg * 10) 69 | user_client.close() 70 | 71 | self.loop.run_until_complete( 72 | self.securesocket.encodeCopy(self.ls_local, ls_local_conn)) 73 | received_msg = self.ls_server.recv(1024) 74 | 75 | self.assertEqual(bytearray(received_msg), self.encripted_msg * 10) 76 | 77 | ls_local_conn.close() 78 | -------------------------------------------------------------------------------- /lightsocks/local.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import socket 3 | import asyncio 4 | import logging 5 | 6 | from lightsocks.utils import net 7 | from lightsocks.core.cipher import Cipher 8 | from lightsocks.core.securesocket import SecureSocket 9 | 10 | Connection = socket.socket 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class LsLocal(SecureSocket): 15 | def __init__(self, 16 | loop: asyncio.AbstractEventLoop, 17 | password: bytearray, 18 | listenAddr: net.Address, 19 | remoteAddr: net.Address) -> None: 20 | super().__init__(loop=loop, cipher=Cipher.NewCipher(password)) 21 | self.listenAddr = listenAddr 22 | self.remoteAddr = remoteAddr 23 | 24 | async def listen(self, didListen: typing.Callable=None): 25 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener: 26 | listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 27 | listener.bind(self.listenAddr) 28 | listener.listen(socket.SOMAXCONN) 29 | listener.setblocking(False) 30 | 31 | logger.info('Listen to %s:%d' % self.listenAddr) 32 | if didListen: 33 | didListen(listener.getsockname()) 34 | 35 | while True: 36 | connection, address = await self.loop.sock_accept(listener) 37 | logger.info('Receive %s:%d', *address) 38 | asyncio.ensure_future(self.handleConn(connection)) 39 | 40 | async def handleConn(self, connection: Connection): 41 | remoteServer = await self.dialRemote() 42 | 43 | def cleanUp(task): 44 | """ 45 | Close the socket when they succeeded or had an exception. 46 | """ 47 | remoteServer.close() 48 | connection.close() 49 | 50 | local2remote = asyncio.ensure_future( 51 | self.decodeCopy(connection, remoteServer)) 52 | remote2local = asyncio.ensure_future( 53 | self.encodeCopy(remoteServer, connection)) 54 | task = asyncio.ensure_future( 55 | asyncio.gather( 56 | local2remote, 57 | remote2local, 58 | loop=self.loop, 59 | return_exceptions=True)) 60 | task.add_done_callback(cleanUp) 61 | 62 | async def dialRemote(self): 63 | """ 64 | Create a socket that connects to the Remote Server. 65 | """ 66 | try: 67 | remoteConn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 68 | remoteConn.setblocking(False) 69 | await self.loop.sock_connect(remoteConn, self.remoteAddr) 70 | except Exception as err: 71 | raise ConnectionError('链接到远程服务器 %s:%d 失败:\n%r' % (*self.remoteAddr, 72 | err)) 73 | return remoteConn 74 | -------------------------------------------------------------------------------- /lightsocks/test_local.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import socket 3 | import unittest 4 | 5 | from lightsocks.core.cipher import Cipher 6 | from lightsocks.core.password import randomPassword 7 | from lightsocks.local import LsLocal 8 | from lightsocks.utils import net 9 | 10 | 11 | class TestLsLocal(unittest.TestCase): 12 | def setUp(self): 13 | self.listenAddr = net.Address('127.0.0.1', 11111) 14 | self.remoteAddr = net.Address('127.0.0.1', 22222) 15 | 16 | self.remoteServer = socket.socket() 17 | self.remoteServer.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 18 | self.remoteServer.bind(self.remoteAddr) 19 | self.remoteServer.listen(socket.SOMAXCONN) 20 | self.remoteServer.setblocking(False) 21 | 22 | password = randomPassword() 23 | self.cipher = Cipher.NewCipher(password) 24 | self.loop = asyncio.new_event_loop() 25 | self.local = LsLocal( 26 | loop=self.loop, 27 | password=password, 28 | listenAddr=self.listenAddr, 29 | remoteAddr=self.remoteAddr) 30 | 31 | self.msg = bytearray(b'hello world') 32 | self.encrypted_msg = self.msg.copy() 33 | self.cipher.encode(self.encrypted_msg) 34 | 35 | def tearDown(self): 36 | self.remoteServer.close() 37 | self.loop.close() 38 | 39 | def test_dialRemote(self): 40 | async def test(): 41 | with await self.local.dialRemote() as connection: 42 | await self.loop.sock_sendall(connection, self.msg) 43 | remoteConn, _ = await self.loop.sock_accept(self.remoteServer) 44 | received_msg = await self.loop.sock_recv(remoteConn, 1024) 45 | remoteConn.close() 46 | self.assertEqual(received_msg, self.msg) 47 | 48 | self.loop.run_until_complete(test()) 49 | 50 | with self.assertRaises(ConnectionError): 51 | self.local.remoteAddr = net.Address('127.0.0.1', 0) 52 | self.loop.run_until_complete(self.local.dialRemote()) 53 | 54 | def test_run(self): 55 | def didListen(address): 56 | 57 | self.assertEqual(address[0], self.listenAddr.ip) 58 | self.assertEqual(address[1], self.listenAddr.port) 59 | 60 | user_client = socket.create_connection(self.listenAddr) 61 | user_client.send(b'hello world') 62 | user_client.close() 63 | 64 | async def call_later(): 65 | conn, _ = await self.loop.sock_accept(self.remoteServer) 66 | with conn: 67 | received_msg = await self.loop.sock_recv(conn, 1024) 68 | 69 | await asyncio.sleep(0.001) 70 | await asyncio.sleep(0.001) 71 | 72 | self.assertEqual(received_msg, self.encrypted_msg) 73 | 74 | self.loop.stop() 75 | 76 | asyncio.ensure_future(call_later(), loop=self.loop) 77 | 78 | with self.assertRaises(RuntimeError): 79 | self.loop.run_until_complete(self.local.listen(didListen)) 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![license](https://img.shields.io/github/license/linw1995/lightsocks-python.svg)](https://github.com/linw1995/lightsocks-python/blob/master/LICENSE) 2 | [![GitHub last commit](https://img.shields.io/github/last-commit/linw1995/lightsocks-python.svg)](https://github.com/linw1995/lightsocks-python) 3 | [![Build Status](https://travis-ci.org/linw1995/lightsocks-python.svg?branch=master)](https://travis-ci.org/linw1995/lightsocks-python) 4 | [![Coverage Status](https://coveralls.io/repos/github/linw1995/lightsocks-python/badge.svg)](https://coveralls.io/github/linw1995/lightsocks-python) 5 | 6 | # [Lightsocks-Python](https://github.com/linw1995/lightsocks-python) 7 | 8 | 一个轻量级网络混淆代理,基于 SOCKS5 协议,可用来代替 Shadowsocks。 9 | 10 | - 只专注于混淆,用最简单高效的混淆算法达到目的; 11 | - Py3.6 asyncio实现; 12 | 13 | > 本项目为 [你也能写个 Shadowsocks](https://github.com/gwuhaolin/blog/issues/12) 的 Python 实现 14 | > 作者实现了 GO 版本 **[Lightsocks](https://github.com/gwuhaolin/lightsocks)** 15 | 16 | ## 安装 17 | 18 | python版本为最新的3.6 19 | 20 | ```bash 21 | git clone https://github.com/linw1995/lightsocks-python 22 | cd lightsocks-python 23 | pip install -r requirements.txt 24 | ``` 25 | 26 | ## 使用 27 | 28 | ### lsserver 29 | 30 | 用于运行在代理服务器的客户端,会还原混淆数据 31 | 32 | ```bash 33 | $ python lsserver.py -h 34 | usage: lsserver.py [-h] [--version] [--save CONFIG] [-c CONFIG] 35 | [-s SERVER_ADDR] [-p SERVER_PORT] [-k PASSWORD] [--random] 36 | 37 | A light tunnel proxy that helps you bypass firewalls 38 | 39 | optional arguments: 40 | -h, --help show this help message and exit 41 | --version show version information 42 | 43 | Proxy options: 44 | --save CONFIG path to dump config 45 | -c CONFIG path to config file 46 | -s SERVER_ADDR server address, default: 0.0.0.0 47 | -p SERVER_PORT server port, default: 8388 48 | -k PASSWORD password 49 | --random generate a random password to use 50 | ``` 51 | 52 | ```bash 53 | $ python lsserver.py --random --save config.json 54 | generate random password 55 | dump config file into 'config.json' 56 | Listen to 0.0.0.0:8388 57 | 58 | Please use: 59 | 60 | lslocal -u "http://hostname:port/#vJjC3tW5l4nG7C3dHZ7hc77cIYrE2q0ikrWQw2MsRa9rqVlDU9vFTF5Hu6PX367kV6qRPU_z-Y_0sio4DAVV-1bmFrfoYoEHmmWkH9L1UDLZqOv8oYvPbe-miAg5Ow58aheFPitEeTX2bmhYC8nQFf1kA5lxpyc0Ljc2W2Du7TESlFIB8aJ7kz-DnczTXcsUv1oYlhpR-AbKf_DI8jMN_tRNdF-szgIJEQrqZ7alvfrNhCCVQNZ-EIIpSOOfXI7nnMC42B48h3egGzBsSpvpaXCNRhME4mEmePd2HFSrD0ty0SUAhjpvTv9BweUZgHrHKLG6Qi-zjLC0JEngI3VmfQ==" 61 | 62 | to config lslocal 63 | ``` 64 | 65 | ### lslocal 66 | 67 | 用于运行在本地电脑的客户端,用于桥接本地浏览器和远程代理服务,传输前会混淆数据 68 | 69 | ```bash 70 | $ python lslocal.py -h 71 | usage: lslocal.py [-h] [--version] [--save CONFIG] [-c CONFIG] [-u URL] 72 | [-s SERVER_ADDR] [-p SERVER_PORT] [-b LOCAL_ADDR] 73 | [-l LOCAL_PORT] [-k PASSWORD] 74 | 75 | A light tunnel proxy that helps you bypass firewalls 76 | 77 | optional arguments: 78 | -h, --help show this help message and exit 79 | --version show version information 80 | 81 | Proxy options: 82 | --save CONFIG path to dump config 83 | -c CONFIG path to config file 84 | -u URL url contains server address, port and password 85 | -s SERVER_ADDR server address 86 | -p SERVER_PORT server port, default: 8388 87 | -b LOCAL_ADDR local binding address, default: 127.0.0.1 88 | -l LOCAL_PORT local port, default: 1080 89 | -k PASSWORD password 90 | ``` 91 | 92 | ```bash 93 | $ python lslocal.py -u "http://remoteAddr:remotePort/#password" --save config.json 94 | dump config file into 'config.json' 95 | Listen to 127.0.0.1:1080 96 | ``` 97 | -------------------------------------------------------------------------------- /lsserver.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import sys 4 | 5 | from lightsocks.core.password import (InvalidPasswordError, dumpsPassword, 6 | loadsPassword, randomPassword) 7 | from lightsocks.server import LsServer 8 | from lightsocks.utils import config as lsConfig 9 | from lightsocks.utils import net 10 | 11 | 12 | def run_server(config: lsConfig.Config): 13 | loop = asyncio.get_event_loop() 14 | 15 | listenAddr = net.Address(config.serverAddr, config.serverPort) 16 | server = LsServer( 17 | loop=loop, password=config.password, listenAddr=listenAddr) 18 | 19 | def didListen(address): 20 | print('Listen to %s:%d\n' % address) 21 | print('Please use:\n') 22 | print('''lslocal -u "http://hostname:port/#''' 23 | f'''{dumpsPassword(config.password)}"''') 24 | print('\nto config lslocal') 25 | 26 | asyncio.ensure_future(server.listen(didListen)) 27 | loop.run_forever() 28 | 29 | 30 | def main(): 31 | parser = argparse.ArgumentParser( 32 | description='A light tunnel proxy that helps you bypass firewalls') 33 | parser.add_argument( 34 | '--version', 35 | action='store_true', 36 | default=False, 37 | help='show version information') 38 | 39 | proxy_options = parser.add_argument_group('Proxy options') 40 | 41 | proxy_options.add_argument( 42 | '--save', metavar='CONFIG', help='path to dump config') 43 | proxy_options.add_argument( 44 | '-c', metavar='CONFIG', help='path to config file') 45 | proxy_options.add_argument( 46 | '-s', metavar='SERVER_ADDR', help='server address, default: 0.0.0.0') 47 | proxy_options.add_argument( 48 | '-p', 49 | metavar='SERVER_PORT', 50 | type=int, 51 | help='server port, default: 8388') 52 | proxy_options.add_argument('-k', metavar='PASSWORD', help='password') 53 | proxy_options.add_argument( 54 | '--random', 55 | action='store_true', 56 | default=False, 57 | help='generate a random password to use') 58 | 59 | args = parser.parse_args() 60 | 61 | if args.version: 62 | print('lightsocks 0.1.0') 63 | sys.exit(0) 64 | 65 | config = lsConfig.Config(None, None, None, None, None) 66 | if args.c: 67 | try: 68 | with open(args.c, encoding='utf-8') as f: 69 | file_config = lsConfig.load(f) 70 | except lsConfig.InvalidFileError: 71 | parser.print_usage() 72 | print(f'invalid config file {args.c!r}') 73 | sys.exit(1) 74 | except FileNotFoundError: 75 | parser.print_usage() 76 | print(f'config file {args.c!r} not found') 77 | sys.exit(1) 78 | config = config._replace(**file_config._asdict()) 79 | 80 | if args.s: 81 | serverAddr = args.s 82 | # TODO: 验证 serverAddr 有效性 83 | config = config._replace(serverAddr=serverAddr) 84 | 85 | if args.p: 86 | serverPort = args.p 87 | # TODO: 验证 serverPort 有效性 88 | config = config._replace(serverPort=serverPort) 89 | 90 | if args.k: 91 | try: 92 | password = loadsPassword(args.k) 93 | config = config._replace(password=password) 94 | except InvalidPasswordError: 95 | parser.print_usage() 96 | print('invalid password') 97 | sys.exit(1) 98 | 99 | if config.serverAddr is None: 100 | config = config._replace(serverAddr='0.0.0.0') 101 | 102 | if config.serverPort is None: 103 | config = config._replace(serverPort=8388) 104 | 105 | if config.password is None and not args.random: 106 | parser.print_usage() 107 | print('need PASSWORD, please use [-k PASSWORD] or ' 108 | 'use [--random] to generate a random password') 109 | sys.exit(1) 110 | 111 | if args.random: 112 | print('generate random password') 113 | config = config._replace(password=randomPassword()) 114 | 115 | if args.save: 116 | print(f'dump config file into {args.save!r}') 117 | with open(args.save, 'w', encoding='utf-8') as f: 118 | lsConfig.dump(f, config) 119 | 120 | run_server(config) 121 | 122 | 123 | if __name__ == '__main__': 124 | main() 125 | -------------------------------------------------------------------------------- /lslocal.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import sys 4 | 5 | from lightsocks.core.password import InvalidPasswordError, loadsPassword 6 | from lightsocks.local import LsLocal 7 | from lightsocks.utils import config as lsConfig 8 | from lightsocks.utils import net 9 | 10 | 11 | def run_server(config: lsConfig.Config): 12 | loop = asyncio.get_event_loop() 13 | 14 | listenAddr = net.Address(config.localAddr, config.localPort) 15 | remoteAddr = net.Address(config.serverAddr, config.serverPort) 16 | server = LsLocal( 17 | loop=loop, 18 | password=config.password, 19 | listenAddr=listenAddr, 20 | remoteAddr=remoteAddr) 21 | 22 | def didListen(address): 23 | print('Listen to %s:%d\n' % address) 24 | 25 | asyncio.ensure_future(server.listen(didListen)) 26 | loop.run_forever() 27 | 28 | 29 | def main(): 30 | parser = argparse.ArgumentParser( 31 | description='A light tunnel proxy that helps you bypass firewalls') 32 | parser.add_argument( 33 | '--version', 34 | action='store_true', 35 | default=False, 36 | help='show version information') 37 | 38 | proxy_options = parser.add_argument_group('Proxy options') 39 | 40 | proxy_options.add_argument( 41 | '--save', metavar='CONFIG', help='path to dump config') 42 | proxy_options.add_argument( 43 | '-c', metavar='CONFIG', help='path to config file') 44 | proxy_options.add_argument( 45 | '-u', 46 | metavar='URL', 47 | help='url contains server address, port and password') 48 | proxy_options.add_argument( 49 | '-s', metavar='SERVER_ADDR', help='server address') 50 | proxy_options.add_argument( 51 | '-p', 52 | metavar='SERVER_PORT', 53 | type=int, 54 | help='server port, default: 8388') 55 | proxy_options.add_argument( 56 | '-b', 57 | metavar='LOCAL_ADDR', 58 | help='local binding address, default: 127.0.0.1') 59 | proxy_options.add_argument( 60 | '-l', metavar='LOCAL_PORT', type=int, help='local port, default: 1080') 61 | proxy_options.add_argument('-k', metavar='PASSWORD', help='password') 62 | 63 | args = parser.parse_args() 64 | 65 | if args.version: 66 | print('lightsocks 0.1.0') 67 | sys.exit(0) 68 | 69 | config = lsConfig.Config(None, None, None, None, None) 70 | if args.c: 71 | try: 72 | with open(args.c, encoding='utf-8') as f: 73 | file_config = lsConfig.load(f) 74 | except lsConfig.InvalidFileError: 75 | parser.print_usage() 76 | print(f'invalid config file {args.c!r}') 77 | sys.exit(1) 78 | except FileNotFoundError: 79 | parser.print_usage() 80 | print(f'config file {args.c!r} not found') 81 | sys.exit(1) 82 | config = config._replace(**file_config._asdict()) 83 | 84 | if args.u: 85 | try: 86 | url_config = lsConfig.loadURL(args.u) 87 | except lsConfig.InvalidURLError: 88 | parser.print_usage() 89 | print(f'invalid config URL {args.u!r}') 90 | sys.exit(1) 91 | config = config._replace(**url_config._asdict()) 92 | 93 | if args.s: 94 | serverAddr = args.s 95 | # TODO: 验证 serverAddr 有效性 96 | config = config._replace(serverAddr=serverAddr) 97 | 98 | if args.p: 99 | serverPort = args.p 100 | # TODO: 验证 serverPort 有效性 101 | config = config._replace(serverPort=serverPort) 102 | 103 | if args.b: 104 | localAddr = args.b 105 | # TODO: 验证 localPort 有效性 106 | config = config._replace(localAddr=localAddr) 107 | 108 | if args.l: 109 | localPort = args.l 110 | # TODO: 验证 localPort 有效性 111 | config = config._replace(localPort=localPort) 112 | 113 | if args.k: 114 | try: 115 | password = loadsPassword(args.k) 116 | config = config._replace(password=password) 117 | except InvalidPasswordError: 118 | parser.print_usage() 119 | print('invalid password') 120 | sys.exit(1) 121 | 122 | if config.localAddr is None: 123 | config = config._replace(localAddr='127.0.0.1') 124 | 125 | if config.localPort is None: 126 | config = config._replace(localPort=1080) 127 | 128 | if config.serverPort is None: 129 | config = config._replace(serverPort=8388) 130 | 131 | if config.password is None: 132 | parser.print_usage() 133 | print('need PASSWORD, please use [-k PASSWORD]') 134 | sys.exit(1) 135 | 136 | if config.serverAddr is None: 137 | parser.print_usage() 138 | print('need SERVER_ADDR, please use [-s SERVER_ADDR]') 139 | 140 | if args.save: 141 | print(f'dump config file into {args.save!r}') 142 | with open(args.save, 'w', encoding='utf-8') as f: 143 | lsConfig.dump(f, config) 144 | 145 | run_server(config) 146 | 147 | 148 | if __name__ == '__main__': 149 | main() 150 | -------------------------------------------------------------------------------- /lightsocks/server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import typing 3 | import socket 4 | import asyncio 5 | 6 | from lightsocks.utils import net 7 | from lightsocks.core.cipher import Cipher 8 | from lightsocks.core.securesocket import SecureSocket 9 | 10 | Connection = socket.socket 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class LsServer(SecureSocket): 15 | def __init__(self, 16 | loop: asyncio.AbstractEventLoop, 17 | password: bytearray, 18 | listenAddr: net.Address) -> None: 19 | super().__init__(loop=loop, cipher=Cipher.NewCipher(password)) 20 | self.listenAddr = listenAddr 21 | 22 | async def listen(self, didListen: typing.Callable=None): 23 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener: 24 | listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 25 | listener.setblocking(False) 26 | listener.bind(self.listenAddr) 27 | listener.listen(socket.SOMAXCONN) 28 | 29 | logger.info('Listen to %s:%d' % self.listenAddr) 30 | if didListen: 31 | didListen(listener.getsockname()) 32 | 33 | while True: 34 | connection, address = await self.loop.sock_accept(listener) 35 | logger.info('Receive %s:%d', *address) 36 | asyncio.ensure_future(self.handleConn(connection)) 37 | 38 | async def handleConn(self, connection: Connection): 39 | """ 40 | Handle the connection from LsLocal. 41 | """ 42 | """ 43 | SOCKS Protocol Version 5 https://www.ietf.org/rfc/rfc1928.txt 44 | The localConn connects to the dstServer, and sends a ver 45 | identifier/method selection message: 46 | +----+----------+----------+ 47 | |VER | NMETHODS | METHODS | 48 | +----+----------+----------+ 49 | | 1 | 1 | 1 to 255 | 50 | +----+----------+----------+ 51 | The VER field is set to X'05' for this ver of the protocol. The 52 | NMETHODS field contains the number of method identifier octets that 53 | appear in the METHODS field. 54 | """ 55 | buf = await self.decodeRead(connection) 56 | if not buf or buf[0] != 0x05: 57 | connection.close() 58 | return 59 | """ 60 | The dstServer selects from one of the methods given in METHODS, and 61 | sends a METHOD selection message: 62 | +----+--------+ 63 | |VER | METHOD | 64 | +----+--------+ 65 | | 1 | 1 | 66 | +----+--------+ 67 | If the selected METHOD is X'FF', none of the methods listed by the 68 | client are acceptable, and the client MUST close the connection. 69 | 70 | The values currently defined for METHOD are: 71 | 72 | o X'00' NO AUTHENTICATION REQUIRED 73 | o X'01' GSSAPI 74 | o X'02' USERNAME/PASSWORD 75 | o X'03' to X'7F' IANA ASSIGNED 76 | o X'80' to X'FE' RESERVED FOR PRIVATE METHODS 77 | o X'FF' NO ACCEPTABLE METHODS 78 | 79 | The client and server then enter a method-specific sub-negotiation. 80 | """ 81 | await self.encodeWrite(connection, bytearray((0x05, 0x00))) 82 | """ 83 | The SOCKS request is formed as follows: 84 | +----+-----+-------+------+----------+----------+ 85 | |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | 86 | +----+-----+-------+------+----------+----------+ 87 | | 1 | 1 | X'00' | 1 | Variable | 2 | 88 | +----+-----+-------+------+----------+----------+ 89 | Where: 90 | 91 | o VER protocol version: X'05' 92 | o CMD 93 | o CONNECT X'01' 94 | o BIND X'02' 95 | o UDP ASSOCIATE X'03' 96 | o RSV RESERVED 97 | o ATYP address type of following address 98 | o IP V4 address: X'01' 99 | o DOMAINNAME: X'03' 100 | o IP V6 address: X'04' 101 | o DST.ADDR desired destination address 102 | o DST.PORT desired destination port in network octet 103 | order 104 | """ 105 | buf = await self.decodeRead(connection) 106 | if len(buf) < 7: 107 | connection.close() 108 | return 109 | 110 | if buf[1] != 0x01: 111 | connection.close() 112 | return 113 | 114 | dstIP = None 115 | 116 | dstPort = buf[-2:] 117 | dstPort = int(dstPort.hex(), 16) 118 | 119 | dstFamily = None 120 | 121 | if buf[3] == 0x01: 122 | # ipv4 123 | dstIP = socket.inet_ntop(socket.AF_INET, buf[4:4 + 4]) 124 | dstAddress = net.Address(ip=dstIP, port=dstPort) 125 | dstFamily = socket.AF_INET 126 | elif buf[3] == 0x03: 127 | # domain 128 | dstIP = buf[5:-2].decode() 129 | dstAddress = net.Address(ip=dstIP, port=dstPort) 130 | elif buf[3] == 0x04: 131 | # ipv6 132 | dstIP = socket.inet_ntop(socket.AF_INET6, buf[4:4 + 16]) 133 | dstAddress = (dstIP, dstPort, 0, 0) 134 | dstFamily = socket.AF_INET6 135 | else: 136 | connection.close() 137 | return 138 | 139 | dstServer = None 140 | if dstFamily: 141 | try: 142 | dstServer = socket.socket( 143 | family=dstFamily, type=socket.SOCK_STREAM) 144 | dstServer.setblocking(False) 145 | await self.loop.sock_connect(dstServer, dstAddress) 146 | except OSError: 147 | if dstServer is not None: 148 | dstServer.close() 149 | dstServer = None 150 | else: 151 | host, port = dstAddress 152 | for res in await self.loop.getaddrinfo(host, port): 153 | dstFamily, socktype, proto, _, dstAddress = res 154 | try: 155 | dstServer = socket.socket(dstFamily, socktype, proto) 156 | dstServer.setblocking(False) 157 | await self.loop.sock_connect(dstServer, dstAddress) 158 | break 159 | except OSError: 160 | if dstServer is not None: 161 | dstServer.close() 162 | dstServer = None 163 | 164 | if dstFamily is None: 165 | return 166 | """ 167 | The SOCKS request information is sent by the client as soon as it has 168 | established a connection to the SOCKS server, and completed the 169 | authentication negotiations. The server evaluates the request, and 170 | returns a reply formed as follows: 171 | 172 | +----+-----+-------+------+----------+----------+ 173 | |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | 174 | +----+-----+-------+------+----------+----------+ 175 | | 1 | 1 | X'00' | 1 | Variable | 2 | 176 | +----+-----+-------+------+----------+----------+ 177 | 178 | Where: 179 | 180 | o VER protocol version: X'05' 181 | o REP Reply field: 182 | o X'00' succeeded 183 | o X'01' general SOCKS server failure 184 | o X'02' connection not allowed by ruleset 185 | o X'03' Network unreachable 186 | o X'04' Host unreachable 187 | o X'05' Connection refused 188 | o X'06' TTL expired 189 | o X'07' Command not supported 190 | o X'08' Address type not supported 191 | o X'09' to X'FF' unassigned 192 | o RSV RESERVED 193 | o ATYP address type of following address 194 | """ 195 | await self.encodeWrite(connection, 196 | bytearray((0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 197 | 0x00, 0x00, 0x00, 0x00))) 198 | 199 | def cleanUp(task): 200 | """ 201 | Close the socket when they succeeded or had an exception. 202 | """ 203 | dstServer.close() 204 | connection.close() 205 | 206 | conn2dst = asyncio.ensure_future( 207 | self.decodeCopy(dstServer, connection)) 208 | dst2conn = asyncio.ensure_future( 209 | self.encodeCopy(connection, dstServer)) 210 | task = asyncio.ensure_future( 211 | asyncio.gather( 212 | conn2dst, dst2conn, loop=self.loop, return_exceptions=True)) 213 | task.add_done_callback(cleanUp) 214 | -------------------------------------------------------------------------------- /lightsocks/test_server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import socket 3 | import unittest 4 | 5 | from lightsocks.core.cipher import Cipher 6 | from lightsocks.core.password import randomPassword 7 | from lightsocks.server import LsServer 8 | from lightsocks.utils import net 9 | 10 | 11 | def getValidAddr(): 12 | with socket.socket() as sock: 13 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 14 | sock.bind(('', 0)) 15 | rv = sock.getsockname() 16 | return rv 17 | 18 | 19 | class TestLsServer(unittest.TestCase): 20 | def setUp(self): 21 | self.listenAddr = net.Address('127.0.0.1', getValidAddr()[1]) 22 | 23 | password = randomPassword() 24 | self.cipher = Cipher.NewCipher(password) 25 | self.loop = asyncio.new_event_loop() 26 | self.server = LsServer( 27 | loop=self.loop, password=password, listenAddr=self.listenAddr) 28 | 29 | def tearDown(self): 30 | self.loop.close() 31 | 32 | def test_run_succeed_ipv4(self): 33 | def didListen(address): 34 | self.assertEqual(address[0], self.listenAddr.ip) 35 | self.assertEqual(address[1], self.listenAddr.port) 36 | 37 | async def call_later(): 38 | localServer = socket.create_connection(self.listenAddr) 39 | localServer.setblocking(False) 40 | 41 | dstServer = socket.socket() 42 | dstAddress = getValidAddr() 43 | dstServer.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 44 | dstServer.bind(dstAddress) 45 | dstServer.listen(socket.SOMAXCONN) 46 | dstServer.setblocking(False) 47 | 48 | msg = bytearray((0x05, )) 49 | self.cipher.encode(msg) 50 | await self.loop.sock_sendall(localServer, msg) 51 | 52 | received_msg = await self.loop.sock_recv(localServer, 1024) 53 | received_msg = bytearray(received_msg) 54 | self.cipher.decode(received_msg) 55 | 56 | self.assertEqual(received_msg, bytearray((0x05, 0x00))) 57 | 58 | msg = bytearray([0x05, 0x01, 0x01, 0x01]) 59 | msg.extend(socket.inet_pton(socket.AF_INET, '127.0.0.1')) 60 | msg.extend(dstAddress[1].to_bytes(2, 'big')) 61 | self.cipher.encode(msg) 62 | await self.loop.sock_sendall(localServer, msg) 63 | 64 | received_msg = await self.loop.sock_recv(localServer, 1024) 65 | received_msg = bytearray(received_msg) 66 | self.cipher.decode(received_msg) 67 | 68 | self.assertEqual(received_msg, 69 | bytearray((0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 70 | 0x00, 0x00, 0x00, 0x00))) 71 | 72 | msg = bytearray(b'hello world') 73 | self.cipher.encode(msg) 74 | await self.loop.sock_sendall(localServer, msg) 75 | 76 | localServer.close() 77 | 78 | dstServer_conn, _ = await self.loop.sock_accept(dstServer) 79 | received_msg = await self.loop.sock_recv(dstServer_conn, 1024) 80 | 81 | self.assertEqual(received_msg, bytearray(b'hello world')) 82 | 83 | dstServer_conn.close() 84 | dstServer.close() 85 | 86 | await asyncio.sleep(0.001) 87 | await asyncio.sleep(0.001) 88 | 89 | self.loop.stop() 90 | 91 | asyncio.ensure_future(call_later(), loop=self.loop) 92 | 93 | with self.assertRaises(RuntimeError): 94 | self.loop.run_until_complete(self.server.listen(didListen)) 95 | 96 | def test_run_succeed_domain(self): 97 | def didListen(address): 98 | self.assertEqual(address[0], self.listenAddr.ip) 99 | self.assertEqual(address[1], self.listenAddr.port) 100 | 101 | async def call_later(): 102 | localServer = socket.create_connection(self.listenAddr) 103 | localServer.setblocking(False) 104 | 105 | dstServer = socket.socket() 106 | dstAddress = getValidAddr() 107 | dstServer.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 108 | dstServer.bind(dstAddress) 109 | dstServer.listen(socket.SOMAXCONN) 110 | dstServer.setblocking(False) 111 | 112 | msg = bytearray([0x05]) 113 | self.cipher.encode(msg) 114 | await self.loop.sock_sendall(localServer, msg) 115 | 116 | received_msg = await self.loop.sock_recv(localServer, 1024) 117 | received_msg = bytearray(received_msg) 118 | self.cipher.decode(received_msg) 119 | 120 | self.assertEqual(received_msg, bytearray([0x05, 0x00])) 121 | 122 | msg = bytearray((0x05, 0x01, 0x01, 0x03, 0x11)) 123 | msg.extend(b'127.0.0.1') 124 | msg.extend(dstAddress[1].to_bytes(2, 'big')) 125 | self.cipher.encode(msg) 126 | await self.loop.sock_sendall(localServer, msg) 127 | 128 | received_msg = await self.loop.sock_recv(localServer, 1024) 129 | received_msg = bytearray(received_msg) 130 | self.cipher.decode(received_msg) 131 | 132 | self.assertEqual(received_msg, 133 | bytearray([ 134 | 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 135 | 0x00, 0x00, 0x00 136 | ])) 137 | 138 | msg = bytearray(b'hello world') 139 | self.cipher.encode(msg) 140 | await self.loop.sock_sendall(localServer, msg) 141 | 142 | localServer.close() 143 | 144 | dstServer_conn, _ = await self.loop.sock_accept(dstServer) 145 | received_msg = await self.loop.sock_recv(dstServer_conn, 1024) 146 | 147 | self.assertEqual(received_msg, bytearray(b'hello world')) 148 | 149 | dstServer_conn.close() 150 | dstServer.close() 151 | 152 | await asyncio.sleep(0.001) 153 | await asyncio.sleep(0.001) 154 | 155 | self.loop.stop() 156 | 157 | asyncio.ensure_future(call_later(), loop=self.loop) 158 | 159 | with self.assertRaises(RuntimeError): 160 | self.loop.run_until_complete(self.server.listen(didListen)) 161 | 162 | def test_run_succeed_ipv6(self): 163 | def didListen(address): 164 | 165 | self.assertEqual(address[0], self.listenAddr.ip) 166 | self.assertEqual(address[1], self.listenAddr.port) 167 | 168 | async def call_later(): 169 | localServer = socket.create_connection(self.listenAddr) 170 | localServer.setblocking(False) 171 | 172 | dstServer = socket.socket(socket.AF_INET6) 173 | dstPort = getValidAddr()[1] 174 | dstServer.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 175 | dstServer.bind(('', dstPort)) 176 | dstServer.listen(socket.SOMAXCONN) 177 | dstServer.setblocking(False) 178 | 179 | msg = bytearray([0x05]) 180 | self.cipher.encode(msg) 181 | await self.loop.sock_sendall(localServer, msg) 182 | 183 | received_msg = await self.loop.sock_recv(localServer, 1024) 184 | received_msg = bytearray(received_msg) 185 | self.cipher.decode(received_msg) 186 | 187 | self.assertEqual(received_msg, bytearray([0x05, 0x00])) 188 | 189 | msg = bytearray((0x05, 0x01, 0x01, 0x04)) 190 | msg.extend(socket.inet_pton(socket.AF_INET6, '::1')) 191 | msg.extend(dstPort.to_bytes(2, 'big')) 192 | self.cipher.encode(msg) 193 | await self.loop.sock_sendall(localServer, msg) 194 | 195 | received_msg = await self.loop.sock_recv(localServer, 1024) 196 | received_msg = bytearray(received_msg) 197 | self.cipher.decode(received_msg) 198 | 199 | self.assertEqual(received_msg, 200 | bytearray([ 201 | 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 202 | 0x00, 0x00, 0x00 203 | ])) 204 | 205 | msg = bytearray(b'hello world') 206 | self.cipher.encode(msg) 207 | await self.loop.sock_sendall(localServer, msg) 208 | 209 | localServer.close() 210 | 211 | dstServer_conn, _ = await self.loop.sock_accept(dstServer) 212 | dstServer_conn.setblocking(False) 213 | received_msg = await self.loop.sock_recv(dstServer_conn, 1024) 214 | 215 | self.assertEqual(received_msg, bytearray(b'hello world')) 216 | 217 | dstServer_conn.close() 218 | dstServer.close() 219 | 220 | await asyncio.sleep(0.001) 221 | await asyncio.sleep(0.001) 222 | 223 | self.loop.stop() 224 | 225 | asyncio.ensure_future(call_later(), loop=self.loop) 226 | 227 | with self.assertRaises(RuntimeError): 228 | self.loop.run_until_complete(self.server.listen(didListen)) 229 | 230 | def test_run_fail(self): 231 | def didListen(address): 232 | 233 | self.assertEqual(address[0], self.listenAddr.ip) 234 | self.assertEqual(address[1], self.listenAddr.port) 235 | 236 | async def call_later(): 237 | with self.subTest('只支持sock5'): 238 | localServer = socket.create_connection(self.listenAddr) 239 | localServer.setblocking(False) 240 | 241 | msg = bytearray((0x04, )) 242 | self.cipher.encode(msg) 243 | await self.loop.sock_sendall(localServer, msg) 244 | 245 | received_msg = await self.loop.sock_recv(localServer, 1024) 246 | self.assertFalse(received_msg) 247 | 248 | localServer.close() 249 | 250 | with self.subTest('包格式错误'): 251 | localServer = socket.create_connection(self.listenAddr) 252 | localServer.setblocking(False) 253 | 254 | msg = bytearray((0x05, )) 255 | self.cipher.encode(msg) 256 | await self.loop.sock_sendall(localServer, msg) 257 | 258 | received_msg = await self.loop.sock_recv(localServer, 1024) 259 | received_msg = bytearray(received_msg) 260 | self.cipher.decode(received_msg) 261 | 262 | self.assertEqual(received_msg, bytearray((0x05, 0x00))) 263 | 264 | msg = bytearray((0x05, 0x01, 0x01)) 265 | self.cipher.encode(msg) 266 | await self.loop.sock_sendall(localServer, msg) 267 | 268 | received_msg = await self.loop.sock_recv(localServer, 1024) 269 | self.assertFalse(received_msg) 270 | 271 | localServer.close() 272 | 273 | with self.subTest('错误的CMD类型'): 274 | localServer = socket.create_connection(self.listenAddr) 275 | localServer.setblocking(False) 276 | 277 | msg = bytearray((0x05, )) 278 | self.cipher.encode(msg) 279 | await self.loop.sock_sendall(localServer, msg) 280 | 281 | received_msg = await self.loop.sock_recv(localServer, 1024) 282 | received_msg = bytearray(received_msg) 283 | self.cipher.decode(received_msg) 284 | 285 | self.assertEqual(received_msg, bytearray((0x05, 0x00))) 286 | 287 | msg = bytearray((0x05, 0xff, 0x01, 0x02, 0xff, 0xff, 0xff)) 288 | self.cipher.encode(msg) 289 | await self.loop.sock_sendall(localServer, msg) 290 | 291 | received_msg = await self.loop.sock_recv(localServer, 1024) 292 | self.assertFalse(received_msg) 293 | 294 | localServer.close() 295 | 296 | with self.subTest('错误的ATYP类型'): 297 | localServer = socket.create_connection(self.listenAddr) 298 | localServer.setblocking(False) 299 | 300 | msg = bytearray((0x05, )) 301 | self.cipher.encode(msg) 302 | await self.loop.sock_sendall(localServer, msg) 303 | 304 | received_msg = await self.loop.sock_recv(localServer, 1024) 305 | received_msg = bytearray(received_msg) 306 | self.cipher.decode(received_msg) 307 | 308 | self.assertEqual(received_msg, bytearray((0x05, 0x00))) 309 | 310 | msg = bytearray((0x05, 0x01, 0x01, 0x02, 0xff, 0xff, 0xff)) 311 | self.cipher.encode(msg) 312 | await self.loop.sock_sendall(localServer, msg) 313 | 314 | received_msg = await self.loop.sock_recv(localServer, 1024) 315 | self.assertFalse(received_msg) 316 | 317 | localServer.close() 318 | 319 | await asyncio.sleep(0.1) 320 | self.loop.stop() 321 | 322 | asyncio.ensure_future(call_later(), loop=self.loop) 323 | 324 | with self.assertRaises(RuntimeError): 325 | self.loop.run_until_complete(self.server.listen(didListen)) 326 | --------------------------------------------------------------------------------