├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── requirements.txt ├── setup.py ├── sizzler ├── __init__.py ├── __main__.py ├── config │ ├── __init__.py │ └── parser.py ├── crypto │ ├── __init__.py │ ├── crypto.py │ └── padding.py ├── transport │ ├── __init__.py │ ├── _transport.py │ ├── _wssession.py │ ├── router.py │ ├── wsclient.py │ └── wsserver.py ├── tun.py └── util │ ├── __init__.py │ ├── cmdline.py │ └── root.py └── supervisor.conf.example /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.egg-info 4 | *.sw? 5 | *.pyc 6 | *~ 7 | *.yaml 8 | sizzler.pypirc 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Sizzler 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | upload-test: setup.py 2 | python3 setup.py sdist 3 | twine upload -r pypitest --skip-existing --config-file sizzler.pypirc dist/* 4 | 5 | upload-release: setup.py 6 | python3 setup.py sdist 7 | twine upload -r pypi --skip-existing --config-file sizzler.pypirc dist/* 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Sizzler: VPN over WebSocket 2 | =========================== 3 | 4 | Sizzler is a Linux tool, which sets up a virtual network interface on a 5 | computer, and transmit the data sent/received from it to another computer 6 | running the same program. 7 | 8 | The transmission utilizes WebSocket, a common technology used in modern 9 | websites. Therefore all other technologies for optimizing WebSocket connections 10 | apply also for Sizzler: firewalls allowing WebSockets will also allow Sizzler 11 | connections; reverse proxies for accelerating accesses may also work. 12 | 13 | The network interface set up by Sizzler behaves like a normal network 14 | interface. Since transmitted are IP packets, not only TCP but also UDP and ICMP 15 | are supported. 16 | 17 | Sizzler is MIT licensed. 18 | 19 | # Install 20 | 21 | Use PyPI to install: 22 | 23 | sudo pip3 install sizzler 24 | 25 | # Usage 26 | 27 | `sizzler` can be run in command line: 28 | 29 | * `sizzler -h` for help 30 | * `sudo sizzler -c CONFIG_FILE`, supply a config file in [YAML format][YAML] 31 | and start the program in client mode. **Sizzler requires root priviledge!** 32 | But it will drop that right after virtual network interface is set up and 33 | run. 34 | * `sudo sizzler -s CONFIG_FILE`, just like above, but in server mode. 35 | * `sizzler -e` will print an example config file to standard output. 36 | 37 | [YAML]: https://en.wikipedia.org/wiki/YAML 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYaml 2 | PyNaCl 3 | websockets 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | from setuptools import setup, find_packages 5 | 6 | # Utility function to read the README file. 7 | # Used for the long_description. It's nice, because now 1) we have a top level 8 | # README file and 2) it's easier to type in the README file than to put a raw 9 | # string in below ... 10 | def read(fname): 11 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 12 | 13 | setup( 14 | name = "Sizzler", 15 | version = "0.0.4", 16 | author = "Sogisha", 17 | author_email = "sogisha@protonmail.com", 18 | description = ("A VPN over WebSocket"), 19 | license = "MIT", 20 | keywords = "asyncio VPN WebSocket TUN", 21 | url = "http://github.com/scmagi/sizzler", 22 | packages=find_packages(), 23 | install_requires=[ 24 | "PyYaml", 25 | "PyNaCl", 26 | "websockets", 27 | ], 28 | long_description=read('README.md'), 29 | entry_points=""" 30 | [console_scripts] 31 | sizzler=sizzler.__main__:main 32 | """ 33 | ) 34 | -------------------------------------------------------------------------------- /sizzler/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scmagi/sizzler/6f1b0afb0786e56488012c7e8e16b97194f275cd/sizzler/__init__.py -------------------------------------------------------------------------------- /sizzler/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | ------------------------------------------------------------------------------ 5 | Check running environment: 6 | * ensure running under Python 3.5+; 7 | * ensure 3rd party packages installed. 8 | """ 9 | 10 | import sys 11 | if sys.version_info < (3, 5): 12 | print("Error: you have to run Sizzler with Python 3.5 or higher version.") 13 | exit(1) 14 | 15 | try: 16 | import websockets 17 | import nacl 18 | import yaml 19 | except: 20 | print("Error: one or more 3rd party package(s) not installed.") 21 | print("To fix this, run:\n sudo pip3 install -r requirements.txt") 22 | exit(1) 23 | 24 | import os 25 | import asyncio 26 | import logging 27 | 28 | from .util.root import RootPriviledgeManager 29 | from .util.cmdline import parseCommandLineArguments 30 | from .config.parser import loadConfigFile 31 | from .tun import SizzlerVirtualNetworkInterface 32 | from .transport.wsserver import WebsocketServer 33 | from .transport.wsclient import WebsocketClient 34 | 35 | def main(): 36 | 37 | """ 38 | -------------------------------------------------------------------------- 39 | Parse command line arguments. 40 | """ 41 | 42 | argv = parseCommandLineArguments(sys.argv[1:]) 43 | 44 | ROLE = "server" if argv.server else "client" 45 | CONFIG = loadConfigFile(argv.server if ROLE == "server" else argv.client) 46 | logging.basicConfig(level=argv.loglevel.upper()) 47 | 48 | """ 49 | -------------------------------------------------------------------------- 50 | We need root priviledge. 51 | """ 52 | 53 | priviledgeManager = RootPriviledgeManager() 54 | if not priviledgeManager.isRoot(): 55 | print("Error: you need to run sizzler with root priviledge.") 56 | exit(1) 57 | 58 | """ 59 | -------------------------------------------------------------------------- 60 | With root priviledge, we have to set up TUN device as soon as possible. 61 | """ 62 | 63 | tun = SizzlerVirtualNetworkInterface( 64 | ip=CONFIG["ip"]["client" if ROLE == "client" else "server"], 65 | dstip=CONFIG["ip"]["server" if ROLE == "client" else "client"] 66 | ) 67 | 68 | """ 69 | -------------------------------------------------------------------------- 70 | Now root is no longer required. 71 | """ 72 | 73 | try: 74 | priviledgeManager.dropRoot() 75 | assert not priviledgeManager.isRoot() 76 | except Exception as e: 77 | print("Error: failed dropping root priviledge.") 78 | print(e) 79 | exit(1) 80 | 81 | """ 82 | -------------------------------------------------------------------------- 83 | Start the server or client. 84 | """ 85 | 86 | if ROLE == "client": 87 | transport = WebsocketClient(uris=CONFIG["client"], key=CONFIG["key"]) 88 | 89 | else: 90 | transport = WebsocketServer( 91 | host=CONFIG["server"]["host"], 92 | port=CONFIG["server"]["port"], 93 | key=CONFIG["key"] 94 | ) 95 | 96 | tun.connect(transport) 97 | 98 | """ 99 | -------------------------------------------------------------------------- 100 | Start event loop. 101 | """ 102 | 103 | loop = asyncio.get_event_loop() 104 | loop.run_until_complete(asyncio.gather(tun, transport)) 105 | 106 | 107 | if __name__ == "__main__": 108 | main() 109 | -------------------------------------------------------------------------------- /sizzler/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scmagi/sizzler/6f1b0afb0786e56488012c7e8e16b97194f275cd/sizzler/config/__init__.py -------------------------------------------------------------------------------- /sizzler/config/parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import yaml 4 | 5 | def loadConfigFile(filename): 6 | try: 7 | config = yaml.load(open(filename, "r").read()) 8 | except: 9 | raise Exception("Cannot read given config file.") 10 | 11 | try: 12 | assert type(config["key"]) == str 13 | assert type(config["ip"]["server"]) == str 14 | assert type(config["ip"]["client"]) == str 15 | 16 | except: 17 | raise Exception("Malformed config file.") 18 | 19 | return config 20 | -------------------------------------------------------------------------------- /sizzler/crypto/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scmagi/sizzler/6f1b0afb0786e56488012c7e8e16b97194f275cd/sizzler/crypto/__init__.py -------------------------------------------------------------------------------- /sizzler/crypto/crypto.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import asyncio 4 | import hashlib 5 | import nacl.secret 6 | 7 | def __getEncryptor(box): 8 | loop = asyncio.get_event_loop() 9 | async def encrypt(data): 10 | future = loop.run_in_executor(None, box.encrypt, data) 11 | return await future 12 | return encrypt 13 | 14 | def __getDecryptor(box): 15 | loop = asyncio.get_event_loop() 16 | async def decrypt(data): 17 | def _wrapDecrypt(data): 18 | try: 19 | return box.decrypt(data) 20 | except: 21 | return None 22 | future = loop.run_in_executor(None, _wrapDecrypt, data) 23 | return await future 24 | return decrypt 25 | 26 | 27 | 28 | def getCrypto(key): 29 | if type(key) == str: key = key.encode('utf-8') 30 | assert type(key) == bytes 31 | 32 | encryptKey = hashlib.sha512(key).digest() 33 | authkey = hashlib.sha512(encryptKey).digest() 34 | 35 | encryptKey = encryptKey[:nacl.secret.SecretBox.KEY_SIZE] 36 | 37 | box = nacl.secret.SecretBox(encryptKey) 38 | 39 | return __getEncryptor(box), __getDecryptor(box) 40 | 41 | 42 | 43 | if __name__ == "__main__": 44 | async def main(): 45 | encryptor, decryptor = getCrypto("test") 46 | d = await encryptor(b"plaintext") 47 | print(d) 48 | d = await decryptor(d) 49 | print(d) 50 | 51 | loop = asyncio.get_event_loop() 52 | loop.run_until_complete(main()) 53 | 54 | -------------------------------------------------------------------------------- /sizzler/crypto/padding.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import asyncio 4 | import os 5 | import random 6 | import struct 7 | import time 8 | 9 | 10 | # tell calculation how many bytes will be added after encryption with respect 11 | # to input before padding 12 | 13 | PADDING_FORMAT_OVERHEAD = 2 + 8 14 | ENCRYPTION_OVERHEAD = 40 15 | 16 | PADDING_TOTAL_OVERHEAD = ENCRYPTION_OVERHEAD + PADDING_FORMAT_OVERHEAD 17 | 18 | # How many nonces may be (theoretically) issued per second, limits the max. 19 | # network speed! 20 | NONCES_RESOLUTION = 1e6 # < if packet size = 4kB, limits to 4GB/s(???) 21 | DELETE_NONCES_BEFORE = 300 * NONCES_RESOLUTION 22 | 23 | 24 | class NonceManagement: 25 | 26 | def __init__(self): 27 | self.nonces = [] 28 | 29 | def new(self): 30 | return int(time.time() * NONCES_RESOLUTION) 31 | 32 | def verify(self, nonce): 33 | if not self.nonces: 34 | self.nonces.append(nonce) 35 | self.oldest = nonce - DELETE_NONCES_BEFORE 36 | return True 37 | if nonce < self.oldest or nonce in self.nonces: 38 | print("Nonce failure: Replay attack or unexpected bug!") 39 | return False 40 | self.nonces.append(nonce) 41 | return True 42 | 43 | def __await__(self): 44 | while True: 45 | # recalculate acceptable nonce time 46 | if self.nonces: 47 | self.oldest = max(self.nonces) - DELETE_NONCES_BEFORE 48 | # clear nonces cache 49 | self.nonces = [e for e in self.nonces if e >= self.oldest] 50 | yield from asyncio.sleep(30) 51 | 52 | 53 | class RandomPadding: 54 | 55 | def __init__(self, targetSize=4096): 56 | assert targetSize > PADDING_TOTAL_OVERHEAD 57 | self.maxAfterPaddingLength = targetSize - PADDING_TOTAL_OVERHEAD 58 | self.paddingTemplate = os.urandom(65536) 59 | self.nonces = NonceManagement() 60 | 61 | def __packHead(self, dataLength): 62 | # put `dataLength` and nonce(timestamp-based) into a header 63 | return struct.pack("= self.maxAfterPaddingLength: 77 | return self.__packHead(dataLength) + data 78 | else: 79 | targetLength = random.randint( 80 | dataLength, self.maxAfterPaddingLength 81 | ) 82 | paddingLength = targetLength - dataLength 83 | padding = self.paddingTemplate[:paddingLength] 84 | return self.__packHead(dataLength) + data + padding 85 | 86 | def unpad(self, data): 87 | dataLength = self.__unpackHead(data[:PADDING_FORMAT_OVERHEAD]) 88 | if not dataLength: return None 89 | if dataLength > len(data) - PADDING_FORMAT_OVERHEAD: return None 90 | return data[PADDING_FORMAT_OVERHEAD:][:dataLength] 91 | 92 | def __await__(self): 93 | async def job1(): 94 | while True: 95 | self.paddingTemplate = os.urandom(65536) 96 | await asyncio.sleep(5) # change random padding every 5 sec 97 | yield from asyncio.gather(job1(), self.nonces) 98 | 99 | 100 | if __name__ == "__main__": 101 | async def main(): 102 | p = RandomPadding(100) 103 | print(p.pad(b"aaa"*10)) 104 | 105 | 106 | 107 | loop = asyncio.get_event_loop() 108 | loop.run_until_complete(main()) 109 | 110 | -------------------------------------------------------------------------------- /sizzler/transport/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scmagi/sizzler/6f1b0afb0786e56488012c7e8e16b97194f275cd/sizzler/transport/__init__.py -------------------------------------------------------------------------------- /sizzler/transport/_transport.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | class SizzlerTransport: 4 | 5 | def __init__(self): 6 | self.connections = 0 7 | self.toWSQueue, self.fromWSQueue = None, None 8 | 9 | def increaseConnectionsCount(self): 10 | self.connections += 1 11 | 12 | def decreaseConnectionsCount(self): 13 | self.connections -= 1 14 | -------------------------------------------------------------------------------- /sizzler/transport/_wssession.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import time 4 | import asyncio 5 | import hashlib 6 | from logging import info, debug, critical, exception 7 | 8 | from ..crypto.crypto import getCrypto 9 | from ..crypto.padding import RandomPadding 10 | 11 | 12 | wsid = 0 13 | 14 | TIMEDIFF_TOLERANCE = 300 15 | CONNECTION_TIMEOUT = 30 16 | PADDING_MAX = 2048 17 | 18 | class WebsocketSession: 19 | 20 | wsid = 0 21 | 22 | def __init__( 23 | self, 24 | websocket, 25 | path, 26 | key, 27 | fromWSQueue, 28 | toWSQueue 29 | ): 30 | global wsid 31 | wsid += 1 32 | self.wsid = wsid 33 | self.websocket = websocket 34 | self.fromWSQueue = fromWSQueue 35 | self.toWSQueue = toWSQueue 36 | self.encryptor, self.decryptor = getCrypto(key) 37 | self.padder = RandomPadding(PADDING_MAX) 38 | 39 | # get path, which is the unique ID for this connection 40 | try: 41 | f = path.find("?") 42 | assert f >= 0 43 | self.uniqueID = hashlib.sha512( 44 | path[f:].encode("ascii") 45 | ).hexdigest() 46 | except: 47 | raise Exception("Connection %d without valid ID." % self.wsid) 48 | 49 | # parameters for heartbeating 50 | self.peerAuthenticated = False 51 | self.lastHeartbeat = time.time() 52 | 53 | 54 | def __beforeSend(self, data=None, heartbeat=None): 55 | # Pack plaintext with headers etc. Returns packed data if they are 56 | # ok for outgoing traffic, or None. 57 | ret = None 58 | if data: 59 | ret = b"d-" + data 60 | if heartbeat: 61 | ret = ("h-%s-%s" % (self.uniqueID, time.time())).encode('ascii') 62 | return self.padder.pad(ret) 63 | 64 | def __afterReceive(self, raw): 65 | # unpack decrypted PLAINTEXT and extract headers etc. 66 | # returns data needed to be written to TUN if any, otherwise None. 67 | raw = self.padder.unpad(raw) 68 | if not raw: return None 69 | if raw.startswith(b"d-"): 70 | return raw[2:] 71 | if raw.startswith(b"h-"): 72 | self.__heartbeatReceived(raw) 73 | return None 74 | 75 | # ---- Heartbeat to remote, and evaluation of remote sent heartbeats. 76 | 77 | def __heartbeatReceived(self, raw): 78 | # If a remote heartbeat received, record its timestamp. 79 | try: 80 | heartbeatSlices = raw.decode('ascii').split('-') 81 | assert heartbeatSlices[0] == "h" 82 | assert heartbeatSlices[1] == self.uniqueID 83 | timestamp = float(heartbeatSlices[2]) 84 | nowtime = time.time() 85 | if timestamp <= nowtime + TIMEDIFF_TOLERANCE: 86 | self.lastHeartbeat = max(self.lastHeartbeat, timestamp) 87 | self.peerAuthenticated = True 88 | except: 89 | warning("Warning: invalid heartbeat!") 90 | 91 | async def __sendLocalHeartbeat(self): 92 | # Try to send local heartbeats. 93 | while True: 94 | d = self.__beforeSend(heartbeat=True) 95 | e = await self.encryptor(d) 96 | await self.websocket.send(e) 97 | await asyncio.sleep(5) 98 | 99 | async def __checkRemoteHeartbeat(self): 100 | # See if remote to us is still alive. If not, raise Exception and 101 | # terminate the connections. 102 | while True: 103 | await asyncio.sleep(5) 104 | if time.time() - self.lastHeartbeat > CONNECTION_TIMEOUT: 105 | raise Exception("Connection %d timed out." % self.wsid) 106 | 107 | 108 | # ---- Data transfer 109 | 110 | async def __receiveToQueue(self): 111 | while True: 112 | e = await self.websocket.recv() # data received 113 | raw = await self.decryptor(e) 114 | if not raw: continue # decryption must success 115 | d = self.__afterReceive(raw) 116 | if not d: continue # if any data writable to TUN 117 | if self.peerAuthenticated: # if peer authenticated 118 | await self.fromWSQueue.put(d) 119 | debug(" --|%3d|%s Local %5d bytes" % ( 120 | self.wsid, 121 | "--> " if self.peerAuthenticated else "-//-", 122 | len(e) 123 | )) 124 | 125 | async def __sendFromQueue(self): 126 | while True: 127 | d = await self.toWSQueue.get() # data to be sent ready 128 | s = self.__beforeSend(data=d) # pack the data 129 | if not s: continue # if packer refuses, drop it 130 | e = await self.encryptor(s) # encrypt packed data 131 | await self.websocket.send(e) # send it 132 | debug(" Internet <--|%3d|-- %5d bytes" % ( 133 | self.wsid, 134 | len(s) 135 | )) 136 | 137 | def __await__(self): 138 | yield from asyncio.gather( 139 | self.__receiveToQueue(), 140 | self.__sendFromQueue(), 141 | self.__sendLocalHeartbeat(), 142 | self.__checkRemoteHeartbeat(), 143 | self.padder 144 | ) 145 | -------------------------------------------------------------------------------- /sizzler/transport/router.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | class PacketRouter: 4 | 5 | def __init__(self, tun): 6 | pass 7 | -------------------------------------------------------------------------------- /sizzler/transport/wsclient.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import asyncio 4 | import websockets 5 | import os 6 | import sys 7 | import time 8 | from logging import info, debug, critical, exception 9 | 10 | import yaml 11 | 12 | from ._wssession import WebsocketSession 13 | from ._transport import SizzlerTransport 14 | 15 | 16 | class WebsocketClient(SizzlerTransport): 17 | 18 | def __init__(self, uris=None, key=None): 19 | SizzlerTransport.__init__(self) 20 | self.uris = uris 21 | self.key = key 22 | 23 | async def __connect(self, baseURI): 24 | while True: 25 | try: 26 | uri = baseURI 27 | if not uri.endswith("/"): uri += "/" 28 | uri += "?_=%s" % os.urandom(32).hex() 29 | async with websockets.connect(uri) as websocket: 30 | self.increaseConnectionsCount() 31 | await WebsocketSession( 32 | websocket=websocket, 33 | path=uri, 34 | key=self.key, 35 | fromWSQueue=self.fromWSQueue, 36 | toWSQueue=self.toWSQueue 37 | ) 38 | except Exception as e: 39 | debug("Client connection break, reason: %s" % e) 40 | finally: 41 | self.decreaseConnectionsCount() 42 | info("Connection failed or broken. Try again in 5 seconds.") 43 | await asyncio.sleep(5) 44 | 45 | def __await__(self): 46 | assert self.toWSQueue != None and self.fromWSQueue != None 47 | services = [self.__connect(uri) for uri in self.uris] 48 | yield from asyncio.gather(*services) 49 | -------------------------------------------------------------------------------- /sizzler/transport/wsserver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import asyncio 4 | import websockets 5 | import time 6 | import sys 7 | from logging import info, debug, critical, exception 8 | 9 | from ._wssession import WebsocketSession 10 | from ._transport import SizzlerTransport 11 | 12 | 13 | class WebsocketServer(SizzlerTransport): 14 | 15 | def __init__(self, host=None, port=None, key=None): 16 | SizzlerTransport.__init__(self) 17 | self.host = host 18 | self.port = port 19 | self.key = key 20 | 21 | async def __wsHandler(self, websocket, path): 22 | info("New connection: %s" % path) 23 | try: 24 | self.increaseConnectionsCount() 25 | await WebsocketSession( 26 | websocket=websocket, 27 | path=path, 28 | key=self.key, 29 | fromWSQueue=self.fromWSQueue, 30 | toWSQueue=self.toWSQueue 31 | ) 32 | except Exception as e: 33 | debug("Server connection break, reason: %s" % e) 34 | finally: 35 | self.decreaseConnectionsCount() 36 | info("Current alive connections: %d" % self.connections) 37 | 38 | def __await__(self): 39 | assert self.toWSQueue != None and self.fromWSQueue != None 40 | yield from websockets.serve(self.__wsHandler, self.host, self.port) 41 | -------------------------------------------------------------------------------- /sizzler/tun.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import fcntl 5 | import struct 6 | import asyncio 7 | from logging import info, debug, critical, exception 8 | 9 | from .transport._transport import SizzlerTransport 10 | 11 | TUNSETIFF = 0x400454ca 12 | IFF_TUN = 0x0001 # Set up TUN device 13 | IFF_TAP = 0x0002 # Set up TAP device 14 | IFF_NO_PI = 0x1000 # Without this flag, received frame will have 4 bytes 15 | # for flags and protocol(each 2 bytes) 16 | 17 | def _getTUNDeviceLocation(): 18 | if os.path.exists("/dev/net/tun"): return "/dev/net/tun" 19 | if os.path.exists("/dev/tun"): return "/dev/tun" 20 | critical("TUN/TAP device not found on this OS!") 21 | raise Exception("No TUN/TAP device available.") 22 | 23 | def _getReader(tun): 24 | loop = asyncio.get_event_loop() 25 | async def read(): 26 | future = loop.run_in_executor(None, os.read, tun, 65536) 27 | return await future 28 | return read 29 | 30 | def _getWriter(tun): 31 | loop = asyncio.get_event_loop() 32 | async def write(data): 33 | future = loop.run_in_executor( 34 | None, 35 | os.write, 36 | tun, 37 | data 38 | ) 39 | await future 40 | return write 41 | 42 | 43 | class SizzlerVirtualNetworkInterface: 44 | 45 | def __init__(self, ip, dstip, mtu=1500, netmask="255.255.255.0"): 46 | self.ip = ip 47 | self.dstip = dstip 48 | self.mtu = mtu 49 | self.netmask = netmask 50 | self.__tunR, self.__tunW = self.__setup() 51 | self.toWSQueue = asyncio.Queue() 52 | self.fromWSQueue = asyncio.Queue() 53 | self.transports = [] 54 | 55 | def __setup(self): 56 | try: 57 | self.tun = os.open(_getTUNDeviceLocation(), os.O_RDWR) 58 | ret = fcntl.ioctl(\ 59 | self.tun, 60 | TUNSETIFF, 61 | struct.pack("16sH", b"sizzler-%d", IFF_TUN) 62 | ) 63 | tunName = ret[:16].decode("ascii").strip("\x00") 64 | info("Virtual network interface [%s] created." % tunName) 65 | 66 | os.system("ifconfig %s inet %s netmask %s pointopoint %s" % 67 | (tunName, self.ip, self.netmask, self.dstip) 68 | ) 69 | os.system("ifconfig %s mtu %d up" % (tunName, self.mtu)) 70 | info( 71 | """%s: mtu %d addr %s netmask %s dstaddr %s""" % 72 | (tunName, self.mtu, self.ip, self.netmask, self.dstip) 73 | ) 74 | 75 | return _getReader(self.tun), _getWriter(self.tun) 76 | except Exception as e: 77 | exception(e) 78 | raise Exception("Cannot set TUN/TAP device.") 79 | 80 | def connect(self, transport): 81 | assert isinstance(transport, SizzlerTransport) 82 | self.transports.append(transport) 83 | transport.fromWSQueue = self.fromWSQueue 84 | transport.toWSQueue = self.toWSQueue 85 | 86 | def __countAvailableTransports(self): 87 | count = sum([each.connections for each in self.transports]) 88 | return count 89 | 90 | def __await__(self): 91 | async def proxyQueueToTUN(): 92 | while True: 93 | s = await self.fromWSQueue.get() 94 | await self.__tunW(s) 95 | async def proxyTUNToQueue(): 96 | while True: 97 | s = await self.__tunR() 98 | if self.__countAvailableTransports() < 1: continue 99 | await self.toWSQueue.put(s) 100 | yield from asyncio.gather(proxyQueueToTUN(), proxyTUNToQueue()) 101 | -------------------------------------------------------------------------------- /sizzler/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scmagi/sizzler/6f1b0afb0786e56488012c7e8e16b97194f275cd/sizzler/util/__init__.py -------------------------------------------------------------------------------- /sizzler/util/cmdline.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | 5 | LICENSE = """ 6 | Copyright (c) 2018 Sizzler 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | this software and associated documentation files (the "Software"), to deal in 10 | the Software without restriction, including without limitation the rights to 11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 12 | of the Software, and to permit persons to whom the Software is furnished to do 13 | so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | """ 26 | 27 | #----------------------------------------------------------------------------# 28 | 29 | EXAMPLE_CONFIG = """ 30 | # An example config file for Sizzler 31 | # ---------------------------------- 32 | # Please edit this file according to instructions. Lines beginning with # are 33 | # comments and will be ignored. 34 | # 35 | # Save this file as something like `config.yaml`, and tell Sizzler to use it 36 | # upon starting: 37 | # sizzler -c config.yaml # for starting Sizzler as a client 38 | # sizzler -s config.yaml # for starting Sizzler as a server 39 | 40 | 41 | # This is the key for authorized access to your virtual network. 42 | # Must be kept secret. 43 | 44 | key: example-key 45 | 46 | # These are IP addresses allocated in virtual network for both server and 47 | # client. 48 | 49 | ip: 50 | server: 10.1.0.1 51 | client: 10.1.0.2 52 | 53 | # The server will listen on the address and port as follow. 54 | 55 | server: 56 | host: localhost 57 | port: 8765 58 | 59 | # The client will attempt accessing the server via following URI. This may 60 | # differ from above server settings, especially when you desire to use e.g. 61 | # reverse proxies. 62 | # 63 | # Listing multiple URIs will make client also use multiple connections. 64 | 65 | client: 66 | - ws://123.1.1.1:8765 # suppose this is the server's Internet IP 67 | - ws://example.com/foo # if you can redirect this to 123.1.1.1:8765 68 | - wss://example.org/bar # you may also use wss:// protocol 69 | """ 70 | 71 | #----------------------------------------------------------------------------# 72 | 73 | def parseCommandLineArguments(args): 74 | global EXAMPLE_CONFIG 75 | 76 | parser = argparse.ArgumentParser( 77 | prog="sizzler", 78 | description="""Sizzler is a Linux tool for setting up virtually 79 | connected network interfaces on 2 different computers. The network 80 | traffic between both interfaces will be encrypted and transmitted via 81 | WebSocket. To enable this over Internet, one computer must behave like 82 | a normal HTTP/HTTPS server, which listens for incoming WebSocket 83 | connections, while the other works like a normal web client.""", 84 | epilog="""For documentation, bug and discussions, visit 85 | .""" 86 | ) 87 | 88 | parser.add_argument( 89 | "-l", 90 | "--loglevel", 91 | choices=["debug", "warning", "error", "critical", "info"], 92 | default="info" 93 | ) 94 | 95 | job = parser.add_mutually_exclusive_group(required=True) 96 | 97 | job.add_argument( 98 | "-s", 99 | "--server", 100 | metavar="CONFIG_FILE", 101 | type=str, 102 | help="""Run as a server using given config file.""" 103 | ) 104 | 105 | job.add_argument( 106 | "-c", 107 | "--client", 108 | metavar="CONFIG_FILE", 109 | type=str, 110 | help="""Run as a client using given config file.""" 111 | ) 112 | 113 | job.add_argument( 114 | "-e", 115 | "--example", 116 | action="store_true", 117 | help="""Print an example config file and exit.""" 118 | ) 119 | 120 | results = parser.parse_args(args) 121 | 122 | if results.example: 123 | print(EXAMPLE_CONFIG) 124 | exit() 125 | 126 | return results 127 | -------------------------------------------------------------------------------- /sizzler/util/root.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import pwd 5 | import grp 6 | 7 | 8 | class RootPriviledgeManager: 9 | 10 | def isRoot(self): 11 | return os.geteuid() == 0 12 | 13 | def dropRoot(self): 14 | if not self.isRoot(): return True 15 | 16 | try: 17 | user, group = ("nobody", "nogroup") 18 | 19 | uid = pwd.getpwnam(user).pw_uid 20 | gid = grp.getgrnam(group).gr_gid 21 | 22 | os.setgroups([]) # Remove group privileges 23 | 24 | os.setgid(gid) 25 | os.setuid(uid) 26 | 27 | old_umask = os.umask(0o077) 28 | except: 29 | if self.isRoot(): 30 | raise Exception("Failed dropping root to nobody:nogroup.") 31 | 32 | return not self.isRoot() 33 | -------------------------------------------------------------------------------- /supervisor.conf.example: -------------------------------------------------------------------------------- 1 | # This is an example for using Sizzler with Supervisor 2 | # 3 | # Uncomment one of the following lines starting with "#" to set up Sizzler 4 | # as a client or a server. Do not forget writing your own config file! 5 | # (call `sizzler -e` to see an example config file) 6 | 7 | 8 | [program:sizzler] 9 | #command=sizzler --server 10 | #command=sizzler --client 11 | user=root 12 | autorestart=true 13 | --------------------------------------------------------------------------------