├── .gitignore ├── Dockerfile ├── LICENSE ├── Melkweg.jpg ├── README.md ├── build.sh ├── client.sh ├── requirements.txt ├── server.sh ├── src ├── cipher.py ├── client.py ├── config.py ├── kcp_local.py ├── kcp_server.py ├── localdns.py ├── packet.proto ├── packet_factory.py ├── packet_pb2.py ├── protocol.py ├── server.py ├── socks5.py ├── web_client.py └── web_client │ └── client.html └── test ├── __init__.py └── test_protocol.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | nohup.out 6 | 7 | # Vim temp file 8 | *.sw[ponlm] 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | ext/ 16 | env/ 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # dotenv 88 | .env 89 | 90 | # virtualenv 91 | .venv 92 | venv/ 93 | ENV/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Melkweg runs on Python 2 2 | FROM python:2 3 | 4 | # Create app directory 5 | RUN mkdir /usr/src/app 6 | WORKDIR /usr/src/app 7 | 8 | # Bundle app source 9 | COPY . /usr/src/app 10 | 11 | # Install dependencies 12 | RUN build.sh 13 | 14 | # Expose port 15 | # Should be enough for Toni 16 | EXPOSE 19985-20025 17 | 18 | # Start tcp server by default 19 | CMD server.sh 20 | # Alternatively, start kcp server 21 | # CMD kcp_server.sh 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Wizmann 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 | -------------------------------------------------------------------------------- /Melkweg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wizmann/Melkweg/cea46192c3eab383a3c837e71e0676d9cc7e744a/Melkweg.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Melkweg 2 | 3 | ![](https://github.com/Wizmann/Melkweg/blob/master/Melkweg.jpg?raw=true) 4 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | BASEDIR=$(dirname "$0") 4 | 5 | virtualenv --clear $BASEDIR/env 6 | 7 | echo $BASEDIR/env/bin/activate 8 | source $BASEDIR/env/bin/activate 9 | 10 | pip install -r $BASEDIR/requirements.txt 11 | 12 | mkdir $BASEDIR/ext 13 | pushd $BASEDIR/ext 14 | git clone --depth 1 https://github.com/Wizmann/txkcp.git 15 | git clone --depth 1 https://github.com/Wizmann/python-ikcp.git 16 | 17 | pushd python-ikcp 18 | python setup.py build 19 | python setup.py install 20 | 21 | popd 22 | popd 23 | 24 | -------------------------------------------------------------------------------- /client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd "$( dirname "$0" )" && pwd )" 4 | 5 | cd $DIR 6 | 7 | source env/bin/activate 8 | 9 | export PYTHONPATH="$PWD/ext/txkcp/src:$PYTHONPATH" 10 | 11 | python src/client.py 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attrs>=17.4.0 2 | Automat==0.6.0 3 | constantly==15.1.0 4 | enum34==1.1.6 5 | hyperlink==17.3.1 6 | incremental==17.5.0 7 | protobuf==3.15.0 8 | py==1.10.0 9 | pycryptodome>=3.6.6 10 | pytest==3.2.3 11 | six==1.11.0 12 | twisted>=19.7.0 13 | zope.interface==4.4.3 14 | web.py>=0.38 15 | txsocksx==1.15.0.2 16 | -------------------------------------------------------------------------------- /server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source env/bin/activate 4 | 5 | export PYTHONPATH="$PWD/ext/txkcp/src:$PYTHONPATH" 6 | 7 | ulimit -n 10240 8 | 9 | python src/server.py 10 | -------------------------------------------------------------------------------- /src/cipher.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | 3 | from __future__ import absolute_import 4 | 5 | import abc 6 | import binascii 7 | import random 8 | from Crypto.Cipher import AES 9 | from Crypto.Util import Counter 10 | import Crypto.Hash.SHA256 11 | from Crypto.Util.number import bytes_to_long 12 | 13 | def digest(plain_text, length=16): 14 | h = Crypto.Hash.SHA256.new() 15 | h.update(plain_text) 16 | return h.digest()[:length] 17 | 18 | def nonce(length): 19 | return ''.join([chr(random.randint(0, 255)) for i in xrange(length)]) 20 | 21 | def hexlify(binary): 22 | return binascii.hexlify(binary) 23 | 24 | class ICipher(object): 25 | __metaclass__ = abc.ABCMeta 26 | 27 | def encrypt(self, plain_data): 28 | pass 29 | 30 | def decrypt(self, encrypted_data): 31 | pass 32 | 33 | def factory(cipher, *args): 34 | if cipher == "AES_CTR": 35 | return AES_CTR(*args) 36 | elif cipher == "AES_CTR_HMAC": 37 | return AES_CTR_HMAC(*args) 38 | else: 39 | raise Exception("No cipher: %s is found" % cipher) 40 | 41 | 42 | class AES_CTR(ICipher): 43 | def __init__(self, key, iv): 44 | ctr = Counter.new(128, initial_value=bytes_to_long(iv)) 45 | self.aes = AES.new(digest(key), AES.MODE_CTR, counter=ctr) 46 | 47 | def encrypt(self, plain_data): 48 | return self.aes.encrypt(plain_data) 49 | 50 | def decrypt(self, encrypted_data): 51 | return self.aes.decrypt(encrypted_data) 52 | 53 | class AES_CTR_HMAC(ICipher): 54 | HMAC_LEN = 16 55 | def __init__(self, key, iv): 56 | self.hmac_key = key 57 | ctr = Counter.new(128, initial_value=bytes_to_long(iv)) 58 | self.aes = AES.new(digest(key), AES.MODE_CTR, counter=ctr) 59 | 60 | def encrypt(self, plain_data): 61 | encrypted = self.aes.encrypt(plain_data) 62 | tag = digest(self.hmac_key + plain_data + self.hmac_key, self.HMAC_LEN) 63 | return encrypted + tag 64 | 65 | def decrypt(self, encrypted_data): 66 | tag = encrypted_data[-self.HMAC_LEN:] 67 | encrypted = encrypted_data[:-self.HMAC_LEN] 68 | decrypted = self.aes.decrypt(encrypted) 69 | 70 | tag2 = digest(self.hmac_key + decrypted + self.hmac_key, self.HMAC_LEN) 71 | if tag != tag2: 72 | raise Exception("hmac check error") 73 | return decrypted 74 | -------------------------------------------------------------------------------- /src/client.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | 3 | import random 4 | import logging 5 | from twisted.internet import defer, protocol, reactor, error 6 | 7 | import config 8 | from protocol import MelkwegClientProtocol 9 | from packet_factory import PacketFactory 10 | 11 | if config.USE_KCP: 12 | from kcp_local import LocalProxyProtocolFactory as KcpLocalProxyProtocolFactory 13 | 14 | class MelkwegLocalProxyProtocol(protocol.Protocol): 15 | def __init__(self, addr, port, outgoing): 16 | self.addr = addr 17 | self.port = port 18 | self.outgoing = outgoing 19 | 20 | self.outgoing.d[self.port] = self 21 | 22 | def connectionMade(self): 23 | logging.debug("proxy connection made") 24 | 25 | def dataReceived(self, buf): 26 | logging.debug("outgoing buf size: %s" % len(buf)) 27 | if self.outgoing.transport: 28 | self.outgoing.write(PacketFactory.create_data_packet(self.port, buf)) 29 | else: 30 | self.transport.loseConnection() 31 | 32 | def connectionLost(self, reason): 33 | if reason.check(error.ConnectionDone): 34 | self.outgoing.write(PacketFactory.create_fin_packet(self.port)) 35 | else: 36 | self.outgoing.write(PacketFactory.create_rst_packet(self.port)) 37 | if self.port in self.outgoing.d: 38 | del self.outgoing.d[self.port] 39 | 40 | class MelkwegClientProtocolFactory(protocol.ReconnectingClientFactory): 41 | initialDelay = 3 42 | maxDelay = 10 43 | 44 | def buildProtocol(self, addr): 45 | self.outgoing = MelkwegClientProtocol() 46 | return self.outgoing 47 | 48 | def clientConnectionFailed(self, connector, reason): 49 | logging.error("connection failed: %s" % reason) 50 | MelkwegClientProtocolFactory.outgoing = MelkwegClientProtocol() 51 | protocol.ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) 52 | 53 | def clientConnectionLost(self, connector, reason): 54 | logging.error("connection lost: %s" % reason) 55 | MelkwegClientProtocolFactory.outgoing = MelkwegClientProtocol() 56 | protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason) 57 | 58 | class MelkwegLocalProxyFactory(protocol.Factory): 59 | def __init__(self, host, port): 60 | self.outgoing = [ 61 | MelkwegClientProtocolFactory() 62 | for i in xrange(config.CLIENT_OUTGOING_CONN_NUM) 63 | ] 64 | 65 | for outgoing in self.outgoing: 66 | reactor.connectTCP(host, port, outgoing) 67 | 68 | def buildProtocol(self, addr): 69 | logging.debug("build protocol for %s" % addr) 70 | outgoingProtocol = random.choice(self.outgoing) 71 | protocol = MelkwegLocalProxyProtocol( 72 | addr.host, addr.port, outgoingProtocol.outgoing) 73 | return protocol 74 | 75 | if __name__ == '__main__': 76 | if config.USE_KCP: 77 | reactor.listenTCP(config.CLIENT_KCP_PORT, KcpLocalProxyProtocolFactory()) 78 | reactor.listenTCP( 79 | config.CLIENT_PORT, 80 | MelkwegLocalProxyFactory(config.CLIENT_KCP_ADDR, config.CLIENT_KCP_PORT)) 81 | else: 82 | reactor.listenTCP( 83 | config.CLIENT_PORT, 84 | MelkwegLocalProxyFactory(config.SERVER, config.SERVER_PORT)) 85 | 86 | if config.USE_LOCAL_DNS: 87 | import localdns 88 | localdns.build_dns() 89 | 90 | reactor.run() 91 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | 3 | import logging 4 | from twisted.python import log 5 | 6 | observer = log.PythonLoggingObserver() 7 | observer.start() 8 | 9 | fmt = "%(levelname)-8s %(asctime)-15s [%(filename)s:%(lineno)d] %(message)s" 10 | logging.basicConfig(format=fmt, level=logging.INFO) 11 | 12 | TIMEOUT = 60 13 | HEARTBEAT = 10 14 | CLIENT_OUTGOING_CONN_NUM = 5 15 | SOCKS5_OUTGOING_PROTOCOL_TIMEOUT = 30 16 | 17 | CIPHER = "AES_CTR_HMAC" 18 | 19 | SERVER = "127.0.0.1" 20 | SERVER_PORT = 20000 21 | CLIENT_PORT = 20001 22 | KEY = "if you stand, if you are true" 23 | 24 | SERVER_SOCKS5_ADDR = "127.0.0.1" 25 | SERVER_SOCKS5_PORT = 20002 26 | 27 | USE_KCP = False 28 | KCP_WINDOW_SIZE = 1024 29 | 30 | CLIENT_KCP_ADDR = "127.0.0.1" 31 | CLIENT_KCP_PORT = 20010 32 | 33 | SERVER_KCP_ADDR = "127.0.0.1" 34 | SERVER_KCP_PORT = 20011 35 | 36 | USE_LOCAL_DNS = True 37 | REMOTE_DNS_SERVICE_ADDRS = [('8.8.4.4', 53)] 38 | LOCAL_DNS_SERVICE_UDP_PORT = 20053 39 | LOCAL_DNS_SERVICE_TCP_PORT = 20053 40 | -------------------------------------------------------------------------------- /src/kcp_local.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | 3 | import random 4 | import config 5 | import logging 6 | 7 | if config.USE_KCP: 8 | import txkcp 9 | import ikcp 10 | 11 | from twisted.internet import protocol, reactor, defer 12 | 13 | 14 | class ClientOutgoing(txkcp.Protocol): 15 | mode=ikcp.FAST_MODE 16 | wndsize = 1024 * 4 17 | 18 | def __init__(self, addr, peer, conv): 19 | self.addr = addr 20 | self.peer = peer 21 | self.peer.outgoing = self 22 | 23 | txkcp.Protocol.__init__( 24 | self, self.addr, conv, wndsize=config.KCP_WINDOW_SIZE, mode=ikcp.FAST_MODE) 25 | 26 | def dataReceived(self, data): 27 | logging.debug("data received: %d" % len(data)) 28 | self.peer.transport.write(data) 29 | 30 | class LocalProxyProtocol(protocol.Protocol): 31 | def connectionMade(self): 32 | self.srv_addr = (config.SERVER_KCP_ADDR, config.SERVER_KCP_PORT) 33 | conv = random.randint(10000, 30000) 34 | reactor.listenUDP(0, ClientOutgoing(self.srv_addr, self, conv)) 35 | 36 | def dataReceived(self, data): 37 | self.outgoing.send(data) 38 | 39 | def connctionLost(self, reason): 40 | logging.error("connection lost for reason: %s" % reason) 41 | 42 | class LocalProxyProtocolFactory(protocol.ServerFactory): 43 | protocol = LocalProxyProtocol 44 | -------------------------------------------------------------------------------- /src/kcp_server.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | 3 | import txkcp 4 | import logging 5 | from twisted.internet import protocol, reactor, defer 6 | 7 | import config 8 | import txkcp 9 | import ikcp 10 | 11 | class KcpOutgoing(protocol.Protocol): 12 | def __init__(self, peersock, defer): 13 | assert peersock 14 | logging.debug("kcp outgoing init") 15 | self.peersock = peersock 16 | self.peersock.peersock = self 17 | self.defer = defer 18 | 19 | def connectionMade(self): 20 | self.defer.callback(None) 21 | 22 | def connectionLost(self, reason): 23 | logging.debug("connection lost: %s" % reason) 24 | self.peersock.loseConnection() 25 | self.peersock.peersock = None 26 | self.peersock = None 27 | 28 | def dataReceived(self, buf): 29 | logging.debug("data received: %d" % len(buf)) 30 | self.peersock.send(buf) 31 | 32 | class ServerProtocol(txkcp.Protocol): 33 | mode=ikcp.FAST_MODE 34 | wndsize = 1024 * 4 35 | 36 | def startProtocol(self, defer): 37 | logging.debug("kcp server protocol is starting") 38 | protocol.ClientCreator(reactor, KcpOutgoing, self, defer)\ 39 | .connectTCP(config.SERVER, config.SERVER_PORT) 40 | 41 | def dataReceived(self, data): 42 | self.peersock.transport.write(data) 43 | 44 | class ServerProtocolFactory(txkcp.ProtocolFactory): 45 | protocol = ServerProtocol 46 | -------------------------------------------------------------------------------- /src/localdns.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | from twisted.internet import error, defer, protocol, reactor 3 | from twisted.names import common, dns, hosts, resolve, server 4 | from twisted.python import failure 5 | from twisted.internet.endpoints import TCP4ClientEndpoint 6 | from txsocksx.client import SOCKS5ClientEndpoint 7 | import logging 8 | import config 9 | import socket 10 | 11 | class ResolverBase(common.ResolverBase): 12 | def __init__(self, proxy, servers): 13 | self.proxy = proxy 14 | self.servers = servers 15 | common.ResolverBase.__init__(self) 16 | 17 | def getHostByName(self, name, timeout=None, effort=10): 18 | deferred = self.lookupAllRecords(name, timeout) 19 | deferred.addCallback(self._cbRecords, name, timeout, effort) 20 | return deferred 21 | 22 | def _cbRecords(self, records, name, timeout, effort): 23 | (answers, authority, additional) = records 24 | result = self._extractRecord(answers + authority + additional, name, timeout, effort) 25 | if not result: 26 | raise error.DNSLookupError(name) 27 | return result 28 | 29 | def _extractRecord(self, records, name, timeout, effort): 30 | dnsName = dns.Name(name) 31 | 32 | if not effort: 33 | return None 34 | for r in records: 35 | if r.name == dnsName and r.type == dns.A: 36 | return socket.inet_ntop(socket.AF_INET, r.payload.address) 37 | for r in records: 38 | if r.name == dnsName and r.type == dns.A6: 39 | return socket.inet_ntop(socket.AF_INET6, r.payload.address) 40 | for r in records: 41 | if r.name == dnsName and r.type == dns.AAAA: 42 | return socket.inet_ntop(socket.AF_INET6, r.payload.address) 43 | for r in records: 44 | if r.name == dnsName and r.type == dns.CNAME: 45 | result = self._extractRecord(records, str(r.payload.name), timeout, effort - 1) 46 | if not result: 47 | return self.getHostByName(str(r.payload.name), timeout, effort - 1) 48 | return result 49 | for r in records: 50 | if r.type == dns.NS: 51 | resolver = ServerResolver(self.proxy, str(r.payload.name), dns.PORT) 52 | return resolver.getHostByName(name, timeout, effort - 1) 53 | 54 | class DNSProtocolClientFactory(protocol.ReconnectingClientFactory): 55 | def __init__(self, controller): 56 | self.controller = controller 57 | 58 | def clientConnectionLost(self, connector, reason): 59 | logging.error("DNSProtocolClientFactory.clientConnectionLost, reason: %s", reason) 60 | protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason) 61 | 62 | def clientConnectionFailed(self, connector, reason): 63 | logging.error("DNSProtocolClientFactory.clientConnectionFailed, reason: %s", reason) 64 | 65 | deferreds = self.controller.deferreds[:] 66 | del self.controller.deferreds[:] 67 | for deferred, name, type, cls, timeout in deferreds: 68 | deferred.errback(reason) 69 | protocol.ReconnectingClientFactory.clientConnectionFailed(self, connector, reason) 70 | 71 | def buildProtocol(self, addr): 72 | logging.debug("DNSProtocolClientFactory.buildProtocol") 73 | p = dns.DNSProtocol(self.controller) 74 | p.factory = self 75 | return p 76 | 77 | class DnsOnTcpResolver(ResolverBase): 78 | def __init__(self, proxy, servers): 79 | ResolverBase.__init__(self, proxy, servers) 80 | 81 | self.i = 0 82 | self.proxy = proxy 83 | self.connections = [] 84 | self.deferreds = [] 85 | self.factory = DNSProtocolClientFactory(self) 86 | 87 | def connectionMade(self, connection): 88 | self.connections.append(connection) 89 | 90 | deferreds = self.deferreds[:] 91 | del self.deferreds[:] 92 | for (deferred, name, type, cls, timeout) in deferreds: 93 | self._lookup(name, cls, type, timeout).chainDeferred(deferred) 94 | 95 | def connectionLost(self, connection): 96 | self.connections.remove(connection) 97 | 98 | def messageReceived(self, message, protocol, address=None): 99 | pass 100 | 101 | def _lookup(self, name, cls, type, timeout=None): 102 | if not len(self.connections): 103 | self.i = (self.i + 1) % len(self.servers) 104 | 105 | server = self.servers[self.i] 106 | proxy_addr, proxy_port = self.proxy 107 | server_addr, server_port = server 108 | proxyEndpoint = TCP4ClientEndpoint(reactor, proxy_addr, proxy_port) 109 | dnsEndpoint = SOCKS5ClientEndpoint(server_addr, server_port, proxyEndpoint) 110 | 111 | dnsEndpoint.connect(self.factory) 112 | 113 | deferred = defer.Deferred() 114 | self.deferreds.append((deferred, name, type, cls, timeout)) 115 | return deferred 116 | else: 117 | deferred = self.connections[0].query([dns.Query(name, type, cls)]) 118 | deferred.addCallback(self._cbMessage) 119 | return deferred 120 | 121 | def _cbMessage(self, message): 122 | if message.rCode != dns.OK: 123 | return failure.Failure(self.exceptionForCode(message.rCode)(message)) 124 | 125 | return (message.answers, message.authority, message.additional) 126 | 127 | def build_dns(): 128 | resolver = DnsOnTcpResolver( 129 | proxy=('127.0.0.1', config.CLIENT_PORT), 130 | servers=config.REMOTE_DNS_SERVICE_ADDRS) 131 | factory = server.DNSServerFactory(clients=[resolver], verbose=0) 132 | reactor.listenUDP(config.LOCAL_DNS_SERVICE_UDP_PORT, dns.DNSDatagramProtocol(factory)) 133 | reactor.listenTCP(config.LOCAL_DNS_SERVICE_TCP_PORT, factory) 134 | 135 | if __name__ == '__main__': 136 | build_dns() 137 | reactor.run() 138 | -------------------------------------------------------------------------------- /src/packet.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package Melkweg; 4 | 5 | message MPacket { 6 | bytes iv = 1; 7 | 8 | uint32 port = 10; 9 | uint32 flags = 11; 10 | bytes data = 12; 11 | bytes padding = 13; 12 | 13 | string user = 20; 14 | string secret = 21; 15 | 16 | uint64 client_time = 30; 17 | uint64 server_time = 31; 18 | } 19 | -------------------------------------------------------------------------------- /src/packet_factory.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | 3 | from packet_pb2 import MPacket 4 | from cipher import nonce 5 | 6 | class PacketFlag(object): 7 | DATA = 1 8 | LIV = 2 9 | RST = 3 10 | FIN = 4 11 | KILL = 5 12 | 13 | class PacketFactory(object): 14 | @classmethod 15 | def create_syn_packet(self, iv): 16 | packet = MPacket() 17 | packet.iv = iv 18 | return packet 19 | 20 | @classmethod 21 | def create_rst_packet(self, port): 22 | packet = MPacket() 23 | packet.port = port 24 | packet.flags = PacketFlag.RST 25 | return packet 26 | 27 | @classmethod 28 | def create_kill_packet(self): 29 | packet = MPacket() 30 | packet.flags = PacketFlag.KILL 31 | return packet 32 | 33 | @classmethod 34 | def create_data_packet(self, port, data): 35 | packet = MPacket() 36 | packet.flags = PacketFlag.DATA 37 | packet.port = port 38 | packet.data = data 39 | return packet 40 | 41 | @classmethod 42 | def create_fin_packet(self, port): 43 | packet = MPacket() 44 | packet.flags = PacketFlag.FIN 45 | packet.port = port 46 | return packet 47 | 48 | @classmethod 49 | def create_liv_packet(self): 50 | packet = MPacket() 51 | packet.flags = PacketFlag.LIV 52 | return packet 53 | 54 | -------------------------------------------------------------------------------- /src/packet_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # source: packet.proto 3 | 4 | import sys 5 | _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import message as _message 8 | from google.protobuf import reflection as _reflection 9 | from google.protobuf import symbol_database as _symbol_database 10 | from google.protobuf import descriptor_pb2 11 | # @@protoc_insertion_point(imports) 12 | 13 | _sym_db = _symbol_database.Default() 14 | 15 | 16 | 17 | 18 | DESCRIPTOR = _descriptor.FileDescriptor( 19 | name='packet.proto', 20 | package='Melkweg', 21 | syntax='proto3', 22 | serialized_pb=_b('\n\x0cpacket.proto\x12\x07Melkweg\"\x99\x01\n\x07MPacket\x12\n\n\x02iv\x18\x01 \x01(\x0c\x12\x0c\n\x04port\x18\n \x01(\r\x12\r\n\x05\x66lags\x18\x0b \x01(\r\x12\x0c\n\x04\x64\x61ta\x18\x0c \x01(\x0c\x12\x0f\n\x07padding\x18\r \x01(\x0c\x12\x0c\n\x04user\x18\x14 \x01(\t\x12\x0e\n\x06secret\x18\x15 \x01(\t\x12\x13\n\x0b\x63lient_time\x18\x1e \x01(\x04\x12\x13\n\x0bserver_time\x18\x1f \x01(\x04\x62\x06proto3') 23 | ) 24 | 25 | 26 | 27 | 28 | _MPACKET = _descriptor.Descriptor( 29 | name='MPacket', 30 | full_name='Melkweg.MPacket', 31 | filename=None, 32 | file=DESCRIPTOR, 33 | containing_type=None, 34 | fields=[ 35 | _descriptor.FieldDescriptor( 36 | name='iv', full_name='Melkweg.MPacket.iv', index=0, 37 | number=1, type=12, cpp_type=9, label=1, 38 | has_default_value=False, default_value=_b(""), 39 | message_type=None, enum_type=None, containing_type=None, 40 | is_extension=False, extension_scope=None, 41 | options=None), 42 | _descriptor.FieldDescriptor( 43 | name='port', full_name='Melkweg.MPacket.port', index=1, 44 | number=10, type=13, cpp_type=3, label=1, 45 | has_default_value=False, default_value=0, 46 | message_type=None, enum_type=None, containing_type=None, 47 | is_extension=False, extension_scope=None, 48 | options=None), 49 | _descriptor.FieldDescriptor( 50 | name='flags', full_name='Melkweg.MPacket.flags', index=2, 51 | number=11, type=13, cpp_type=3, label=1, 52 | has_default_value=False, default_value=0, 53 | message_type=None, enum_type=None, containing_type=None, 54 | is_extension=False, extension_scope=None, 55 | options=None), 56 | _descriptor.FieldDescriptor( 57 | name='data', full_name='Melkweg.MPacket.data', index=3, 58 | number=12, type=12, cpp_type=9, label=1, 59 | has_default_value=False, default_value=_b(""), 60 | message_type=None, enum_type=None, containing_type=None, 61 | is_extension=False, extension_scope=None, 62 | options=None), 63 | _descriptor.FieldDescriptor( 64 | name='padding', full_name='Melkweg.MPacket.padding', index=4, 65 | number=13, type=12, cpp_type=9, label=1, 66 | has_default_value=False, default_value=_b(""), 67 | message_type=None, enum_type=None, containing_type=None, 68 | is_extension=False, extension_scope=None, 69 | options=None), 70 | _descriptor.FieldDescriptor( 71 | name='user', full_name='Melkweg.MPacket.user', index=5, 72 | number=20, type=9, cpp_type=9, label=1, 73 | has_default_value=False, default_value=_b("").decode('utf-8'), 74 | message_type=None, enum_type=None, containing_type=None, 75 | is_extension=False, extension_scope=None, 76 | options=None), 77 | _descriptor.FieldDescriptor( 78 | name='secret', full_name='Melkweg.MPacket.secret', index=6, 79 | number=21, type=9, cpp_type=9, label=1, 80 | has_default_value=False, default_value=_b("").decode('utf-8'), 81 | message_type=None, enum_type=None, containing_type=None, 82 | is_extension=False, extension_scope=None, 83 | options=None), 84 | _descriptor.FieldDescriptor( 85 | name='client_time', full_name='Melkweg.MPacket.client_time', index=7, 86 | number=30, type=4, cpp_type=4, label=1, 87 | has_default_value=False, default_value=0, 88 | message_type=None, enum_type=None, containing_type=None, 89 | is_extension=False, extension_scope=None, 90 | options=None), 91 | _descriptor.FieldDescriptor( 92 | name='server_time', full_name='Melkweg.MPacket.server_time', index=8, 93 | number=31, type=4, cpp_type=4, label=1, 94 | has_default_value=False, default_value=0, 95 | message_type=None, enum_type=None, containing_type=None, 96 | is_extension=False, extension_scope=None, 97 | options=None), 98 | ], 99 | extensions=[ 100 | ], 101 | nested_types=[], 102 | enum_types=[ 103 | ], 104 | options=None, 105 | is_extendable=False, 106 | syntax='proto3', 107 | extension_ranges=[], 108 | oneofs=[ 109 | ], 110 | serialized_start=26, 111 | serialized_end=179, 112 | ) 113 | 114 | DESCRIPTOR.message_types_by_name['MPacket'] = _MPACKET 115 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 116 | 117 | MPacket = _reflection.GeneratedProtocolMessageType('MPacket', (_message.Message,), dict( 118 | DESCRIPTOR = _MPACKET, 119 | __module__ = 'packet_pb2' 120 | # @@protoc_insertion_point(class_scope:Melkweg.MPacket) 121 | )) 122 | _sym_db.RegisterMessage(MPacket) 123 | 124 | 125 | # @@protoc_insertion_point(module_scope) 126 | -------------------------------------------------------------------------------- /src/protocol.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | from enum import Enum 3 | import time 4 | import logging 5 | 6 | from twisted.internet import defer, protocol, reactor, error 7 | from twisted.protocols.basic import Int32StringReceiver 8 | from twisted.protocols.policies import TimeoutMixin 9 | 10 | from packet_pb2 import MPacket 11 | 12 | import config 13 | from cipher import AES_CTR, nonce, hexlify 14 | from cipher import factory as cipher_factory 15 | from socks5 import SOCKSv5 16 | from packet_factory import PacketFactory, PacketFlag 17 | 18 | class ProtocolState(Enum): 19 | READY = 1 20 | RUNNING = 2 21 | DONE = 3 22 | ERROR = 4 23 | 24 | def to_millisec(sec): 25 | return sec / 1000. 26 | 27 | def timestamp(): 28 | return int(time.time() * 1000) 29 | 30 | class MelkwegProtocolBase(Int32StringReceiver, TimeoutMixin): 31 | def __init__(self): 32 | self.key = config.KEY 33 | self.iv = nonce(16) 34 | self.aes = cipher_factory(config.CIPHER, self.key, self.iv) 35 | self.state = ProtocolState.READY 36 | self.d = {} 37 | self.lock = defer.DeferredLock() 38 | self.setTimeout(config.TIMEOUT) 39 | logging.info("[%d] self iv: %s" % (id(self), hexlify(self.iv))) 40 | 41 | def heartbeat(self): 42 | assert self.is_client() 43 | try: 44 | packet = PacketFactory.create_liv_packet() 45 | packet.client_time = timestamp() 46 | 47 | self.write(packet) 48 | reactor.callLater(config.HEARTBEAT, self.heartbeat) 49 | except Exception, e: 50 | logging.error("error on heartbeat: %s" % e) 51 | reactor.callLater(config.HEARTBEAT, self.heartbeat) 52 | 53 | def is_server(self): 54 | return 'SERVER' in self.__class__.__dict__ 55 | 56 | def is_client(self): 57 | return 'CLIENT' in self.__class__.__dict__ 58 | 59 | def write(self, packet): 60 | data = packet.SerializeToString() 61 | logging.debug("[%d] write data [packet:%d|state:%s]" % (id(self), len(data), self.state)) 62 | if self.state == ProtocolState.READY: 63 | self.sendString(data) 64 | else: 65 | self.sendString(self.aes.encrypt(data)) 66 | 67 | def stringReceived(self, string): 68 | logging.debug("[%d] string received: %d, state: %s" % (id(self), len(string), self.state)) 69 | 70 | mpacket = self.parse(string) 71 | 72 | if mpacket == None: 73 | self.handle_error() 74 | return 75 | 76 | if self.state == ProtocolState.READY: 77 | if mpacket.iv != None: 78 | self.peer_aes = cipher_factory(config.CIPHER, self.key, mpacket.iv) 79 | logging.info("[%d] get iv: %s from %s" % (id(self), hexlify(mpacket.iv), self.transport.getPeer())) 80 | 81 | if self.is_server(): 82 | self.write(PacketFactory.create_syn_packet(self.iv)) 83 | 84 | self.state = ProtocolState.RUNNING 85 | 86 | if self.is_client(): 87 | self.heartbeat() 88 | else: 89 | self.handle_error() 90 | 91 | elif self.state == ProtocolState.RUNNING: 92 | if mpacket.flags == PacketFlag.DATA: 93 | self.handleDataPacket(mpacket) 94 | elif mpacket.flags in [PacketFlag.RST, PacketFlag.FIN]: 95 | logging.debug("connection on port %d will be terminated" % mpacket.port) 96 | if mpacket.port in self.d and self.d[mpacket.port] and self.d[mpacket.port].transport: 97 | self.d[mpacket.port].transport.loseConnection() 98 | if self.is_server() and mpacket.port in self.d: 99 | del self.d[mpacket.port] 100 | elif mpacket.flags == PacketFlag.LIV: 101 | if self.is_server(): 102 | packet = PacketFactory.create_liv_packet() 103 | packet.client_time = mpacket.client_time 104 | packet.server_time = timestamp() 105 | self.write(packet) 106 | 107 | elif self.is_client(): 108 | client_time = mpacket.client_time 109 | logging.warn("[%d][HEARTBEAT] ping = %d ms" % (id(self), timestamp() - client_time)) 110 | 111 | self.resetTimeout() 112 | else: 113 | self.handle_error() 114 | else: 115 | self.handle_error() 116 | 117 | def handle_error(self): 118 | logging.error("handle error") 119 | self.state = ProtocolState.ERROR 120 | if self.transport: 121 | self.transport.loseConnection() 122 | for outgoing in self.d.values(): 123 | outgoing.transport.loseConnection() 124 | 125 | def timeoutConnection(self): 126 | logging.error("protocol timeout") 127 | self.handle_error() 128 | 129 | def parse(self, string): 130 | mpacket = MPacket() 131 | try: 132 | if self.state == ProtocolState.READY: 133 | mpacket.ParseFromString(string) 134 | else: 135 | plain_data = self.peer_aes.decrypt(string) 136 | mpacket.ParseFromString(plain_data) 137 | return mpacket 138 | except Exception, e: 139 | logging.error('[%d]: %s' %(id(self), e)) 140 | self.handle_error() 141 | 142 | class MelkwegServerOutgoingProtocol(protocol.Protocol): 143 | def __init__(self, peersock, port): 144 | self.peersock = peersock 145 | self.port = port 146 | self.peersock.d[self.port] = self 147 | 148 | def resumeProducing(self): 149 | self.transport.startReading() 150 | 151 | def pauseProducing(self): 152 | self.transport.stopReading() 153 | 154 | def stopProducing(self): 155 | self.transport.abortConnection() 156 | 157 | def dataReceived(self, data): 158 | self.peersock.write(PacketFactory.create_data_packet(self.port, data)) 159 | 160 | def connectionLost(self, reason): 161 | logging.debug("outgoing protocol on port %d is lost: %s" % (self.port, reason)) 162 | if reason.check(error.ConnectionDone): 163 | self.peersock.write(PacketFactory.create_fin_packet(self.port)) 164 | else: 165 | self.peersock.write(PacketFactory.create_rst_packet(self.port)) 166 | 167 | class MelkwegServerProtocol(MelkwegProtocolBase): 168 | SERVER = True 169 | def connectionMade(self): 170 | logging.debug("[%d] connection is made" % id(self)) 171 | if self.is_server(): 172 | self.transport.registerProducer(self, streaming=True) 173 | 174 | def connectionLost(self, reason): 175 | for (port, outgoing) in self.d.items(): 176 | if outgoing.transport: 177 | outgoing.transport.loseConnection() 178 | 179 | logging.error("connection to client %s is lost, %s" % (self.transport.getPeer(), reason)) 180 | 181 | def handleDataPacket(self, mpacket): 182 | port = mpacket.port 183 | 184 | if port not in self.d: 185 | self.transport.stopReading() 186 | protocol.ClientCreator(reactor, MelkwegServerOutgoingProtocol, self, port)\ 187 | .connectTCP(config.SERVER_SOCKS5_ADDR, config.SERVER_SOCKS5_PORT)\ 188 | .addCallback(lambda _: self.d[port].transport.write(mpacket.data))\ 189 | .addBoth(lambda _: self.transport.startReading()) 190 | else: 191 | self.d[port].transport.write(mpacket.data) 192 | 193 | def pauseProducing(self): 194 | for outgoing in self.d.values(): 195 | if outgoing.transport and outgoing.transport.fileno() > 0: 196 | outgoing.transport.stopReading() 197 | 198 | def resumeProducing(self): 199 | for outgoing in self.d.values(): 200 | if outgoing.transport and outgoing.transport.fileno() > 0: 201 | outgoing.transport.startReading() 202 | 203 | def stopProducing(self): 204 | self.handle_error() 205 | 206 | class MelkwegClientProtocol(MelkwegProtocolBase): 207 | CLIENT = True 208 | def connectionMade(self): 209 | logging.debug("connection is made") 210 | self.write(PacketFactory.create_syn_packet(self.iv)) 211 | 212 | def connectionLost(self, reason): 213 | logging.error("connection to server is lost") 214 | for (port, proxy) in self.d.items(): 215 | proxy.transport.loseConnection() 216 | 217 | def handleDataPacket(self, mpacket): 218 | port = mpacket.port 219 | if port in self.d: 220 | self.d[port].transport.write(mpacket.data) 221 | else: 222 | self.write(PacketFactory.create_rst_packet(port)) 223 | -------------------------------------------------------------------------------- /src/server.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | 3 | import logging 4 | from twisted.internet import protocol, reactor, defer 5 | 6 | import config 7 | from protocol import MelkwegServerProtocol 8 | from socks5 import SOCKSv5 9 | 10 | class MelkwegServerFactory(protocol.Factory): 11 | protocol = MelkwegServerProtocol 12 | 13 | def buildProtocol(self, addr): 14 | logging.debug("build protocol for %s" % addr) 15 | return self.protocol() 16 | 17 | class MelkwegSocks5ProtocolFactory(protocol.Factory): 18 | protocol = SOCKSv5 19 | 20 | if __name__ == '__main__': 21 | if config.USE_KCP: 22 | from kcp_server import ServerProtocolFactory as KcpServerProtocolFactory 23 | 24 | reactor.listenTCP(config.SERVER_PORT, MelkwegServerFactory()) 25 | reactor.listenTCP(config.SERVER_SOCKS5_PORT, MelkwegSocks5ProtocolFactory()) 26 | reactor.listenUDP(config.SERVER_KCP_PORT, KcpServerProtocolFactory()) 27 | else: 28 | reactor.listenTCP(config.SERVER_PORT, MelkwegServerFactory()) 29 | reactor.listenTCP(config.SERVER_SOCKS5_PORT, MelkwegSocks5ProtocolFactory()) 30 | 31 | reactor.run() 32 | -------------------------------------------------------------------------------- /src/socks5.py: -------------------------------------------------------------------------------- 1 | """ 2 | License 3 | 4 | Copyright (C) 5 | 2002-2003 Dave Smith (dizzyd@jabber.org) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | $Id: socks5.py 31 2007-12-21 00:08:34Z fabio.forno $ 26 | """ 27 | 28 | import config 29 | from twisted import internet 30 | from twisted.internet import protocol, reactor 31 | from twisted.protocols.policies import TimeoutMixin 32 | import struct 33 | import logging 34 | 35 | STATE_INITIAL = 0 36 | STATE_AUTH = 1 37 | STATE_REQUEST = 2 38 | STATE_READY = 3 39 | STATE_AUTH_USERPASS = 4 40 | STATE_LAST = 5 41 | 42 | STATE_CONNECT_PENDING = STATE_LAST + 1 43 | 44 | SOCKS5_VER = 0x05 45 | 46 | ADDR_IPV4 = 0x01 47 | ADDR_DOMAINNAME = 0x03 48 | ADDR_IPV6 = 0x04 49 | 50 | CMD_CONNECT = 0x01 51 | CMD_BIND = 0x02 52 | CMD_UDPASSOC = 0x03 53 | 54 | AUTHMECH_ANON = 0x00 55 | AUTHMECH_USERPASS = 0x02 56 | AUTHMECH_INVALID = 0xFF 57 | 58 | REPLY_SUCCESS = 0x00 59 | REPLY_GENERAL_FAILUR = 0x01 60 | REPLY_CONN_NOT_ALLOWED = 0x02 61 | REPLY_NETWORK_UNREACHABLE = 0x03 62 | REPLY_HOST_UNREACHABLE = 0x04 63 | REPLY_CONN_REFUSED = 0x05 64 | REPLY_TTL_EXPIRED = 0x06 65 | REPLY_CMD_NOT_SUPPORTED = 0x07 66 | REPLY_ADDR_NOT_SUPPORTED = 0x08 67 | 68 | def IP2Int(ip): 69 | o = map(int, ip.split('.')) 70 | res = (16777216 * o[0]) + (65536 * o[1]) + (256 * o[2]) + o[3] 71 | return res 72 | 73 | def Int2IP(ipnum): 74 | o1 = int(ipnum / 16777216) % 256 75 | o2 = int(ipnum / 65536) % 256 76 | o3 = int(ipnum / 256) % 256 77 | o4 = int(ipnum) % 256 78 | return '%(o1)s.%(o2)s.%(o3)s.%(o4)s' % locals() 79 | 80 | class SOCKSv5Outgoing(protocol.Protocol, TimeoutMixin): 81 | paused = False 82 | 83 | def __init__(self, peersock): 84 | assert peersock 85 | logging.debug("socks5 outgoing init") 86 | self.peersock = peersock 87 | self.peersock.peersock = self 88 | self.setTimeout(config.SOCKS5_OUTGOING_PROTOCOL_TIMEOUT) 89 | 90 | def connectionMade(self): 91 | ipaddr = self.transport.getPeer() 92 | _invalid_, hostname, port = ipaddr.type, ipaddr.host, ipaddr.port 93 | logging.debug("connection made on %s:%d" % (hostname, port)) 94 | self.peersock.transport.registerProducer(self, streaming=True) 95 | self.peersock.connectCompleted(hostname, port) 96 | self.resetTimeout() 97 | 98 | def connectionLost(self, reason): 99 | logging.debug("connection lost: %s" % reason) 100 | self.peersock.transport.unregisterProducer() 101 | self.peersock.transport.loseConnection() 102 | self.peersock.peersock = None 103 | self.peersock = None 104 | 105 | def dataReceived(self, buf): 106 | logging.debug("data received: %d" % len(buf)) 107 | self.peersock.transport.write(buf) 108 | self.resetTimeout() 109 | 110 | def pauseProducing(self): 111 | self.paused = True 112 | self.transport.pauseProducing() 113 | 114 | def resumeProducing(self): 115 | self.paused = False 116 | self.transport.resumeProducing() 117 | self.dataReceived('') 118 | 119 | def stopProducing(self): 120 | self.paused = True 121 | self.transport.stopProducing() 122 | 123 | class SOCKSv5(protocol.Protocol): 124 | def __init__(self): 125 | self.state = STATE_INITIAL 126 | self.buf = "" 127 | self.supportedAuthMechs = [ AUTHMECH_ANON, AUTHMECH_USERPASS ] 128 | self.supportedAddrs = [ ADDR_IPV4, ADDR_DOMAINNAME ] 129 | self.enabledCommands = [ CMD_CONNECT, CMD_BIND ] 130 | self.peersock = None 131 | self.addressType = 0 132 | self.requestType = 0 133 | 134 | def _parseNegotiation(self): 135 | try: 136 | # Parse out data 137 | ver, nmethod = struct.unpack('!BB', self.buf[:2]) 138 | methods = struct.unpack('%dB' % nmethod, self.buf[2:nmethod+2]) 139 | 140 | # Ensure version is correct 141 | if ver != 5: 142 | self.transport.write(struct.pack('!BB', SOCKS5_VER, AUTHMECH_INVALID)) 143 | logging.error("the version[%d] is incorrect for this socks proxy" % ver) 144 | self.transport.loseConnection() 145 | return 146 | 147 | # Trim off front of the buffer 148 | self.buf = self.buf[nmethod+2:] 149 | 150 | # Check for supported auth mechs 151 | for m in self.supportedAuthMechs: 152 | if m in methods: 153 | # Update internal state, according to selected method 154 | if m == AUTHMECH_ANON: 155 | self.state = STATE_REQUEST 156 | elif m == AUTHMECH_USERPASS: 157 | self.state = STATE_AUTH_USERPASS 158 | # Complete negotiation w/ this method 159 | logging.debug("negotiation completed") 160 | self.transport.write(struct.pack('!BB', SOCKS5_VER, m)) 161 | return 162 | 163 | # No supported mechs found, notify client and close the connection 164 | logging.debug("negotiation with error") 165 | self.transport.write(struct.pack('!BB', SOCKS5_VER, AUTHMECH_INVALID)) 166 | self.transport.loseConnection() 167 | except struct.error: 168 | pass 169 | 170 | def _parseUserPass(self): 171 | try: 172 | # Parse out data 173 | ver, ulen = struct.unpack('BB', self.buf[:2]) 174 | uname, = struct.unpack('%ds' % ulen, self.buf[2:ulen + 2]) 175 | plen, = struct.unpack('B', self.buf[ulen + 2]) 176 | password, = struct.unpack('%ds' % plen, self.buf[ulen + 3:ulen + 3 + plen]) 177 | # Trim off fron of the buffer 178 | self.buf = self.buf[3 + ulen + plen:] 179 | # Fire event to authenticate user 180 | if self.authenticateUserPass(uname, password): 181 | # Signal success 182 | self.state = STATE_REQUEST 183 | self.transport.write(struct.pack('!BB', SOCKS5_VER, 0x00)) 184 | else: 185 | # Signal failure 186 | self.transport.write(struct.pack('!BB', SOCKS5_VER, 0x01)) 187 | self.transport.loseConnection() 188 | except struct.error: 189 | pass 190 | 191 | def sendErrorReply(self, errorcode): 192 | # Any other address types are not supported 193 | logging.debug("send error reply: %d" % errorcode) 194 | result = struct.pack('!BBBBIH', SOCKS5_VER, errorcode, 0, 1, 0, 0) 195 | self.transport.write(result) 196 | self.transport.loseConnection() 197 | 198 | def _parseRequest(self): 199 | logging.debug("start parse request") 200 | try: 201 | # Parse out data and trim buffer accordingly 202 | ver, cmd, rsvd, self.addressType = struct.unpack('!BBBB', self.buf[:4]) 203 | 204 | # Ensure we actually support the requested address type 205 | if self.addressType not in self.supportedAddrs: 206 | self.sendErrorReply(REPLY_ADDR_NOT_SUPPORTED) 207 | return 208 | 209 | # Deal with addresses 210 | if self.addressType == ADDR_IPV4: 211 | addr, port = struct.unpack('!IH', self.buf[4:10]) 212 | self.buf = self.buf[10:] 213 | elif self.addressType == ADDR_DOMAINNAME: 214 | nlen = ord(self.buf[4]) 215 | addr, port = struct.unpack('!%dsH' % nlen, self.buf[5:]) 216 | self.buf = self.buf[7 + len(addr):] 217 | else: 218 | # Any other address types are not supported 219 | self.sendErrorReply(REPLY_ADDR_NOT_SUPPORTED) 220 | return 221 | 222 | # Ensure command is supported 223 | if cmd not in self.enabledCommands: 224 | # Send a not supported error 225 | self.sendErrorReply(REPLY_CMD_NOT_SUPPORTED) 226 | return 227 | 228 | # Process the command 229 | logging.debug("command: %d [%s:%s]" % (cmd, addr, port)) 230 | if cmd == CMD_CONNECT: 231 | self.connectRequested(addr, port) 232 | elif cmd == CMD_BIND: 233 | self.bindRequested(addr, port) 234 | else: 235 | # Any other command is not supported 236 | self.sendErrorReply(REPLY_CMD_NOT_SUPPORTED) 237 | 238 | except struct.error, why: 239 | logging.debug("parse request error: %s" % why) 240 | return None 241 | 242 | 243 | def connectRequested(self, addr, port): 244 | self.transport.stopReading() 245 | self.state = STATE_CONNECT_PENDING 246 | if isinstance(addr, int) or isinstance(addr, long): 247 | addr = Int2IP(addr) 248 | 249 | return protocol.ClientCreator(reactor, SOCKSv5Outgoing, self).connectTCP(addr, port) 250 | 251 | def connectCompleted(self, remotehost, remoteport): 252 | if self.addressType == ADDR_IPV4: 253 | remotehost = IP2Int(remotehost) 254 | result = struct.pack( 255 | '!BBBBIH', 256 | SOCKS5_VER, 257 | REPLY_SUCCESS, 258 | 0, 259 | 1, 260 | remotehost, 261 | remoteport) 262 | elif self.addressType == ADDR_DOMAINNAME: 263 | result = struct.pack( 264 | '!BBBBB%dsH' % len(remotehost), 265 | SOCKS5_VER, 266 | REPLY_SUCCESS, 267 | 0, 268 | ADDR_DOMAINNAME, 269 | len(remotehost), 270 | remotehost, 271 | remoteport) 272 | self.transport.write(result) 273 | self.state = STATE_READY 274 | self.transport.startReading() 275 | 276 | def bindRequested(self, addr, port): 277 | pass 278 | 279 | def authenticateUserPass(self, user, passwd): 280 | print "User/pass: ", user, passwd 281 | return True 282 | 283 | def dataReceived(self, buf): 284 | logging.debug("state[%s], buf[%d]" % (self.state, len(buf))) 285 | if self.state == STATE_READY: 286 | self.peersock.transport.write(buf) 287 | return 288 | 289 | self.buf = self.buf + buf 290 | if self.state == STATE_INITIAL: 291 | self._parseNegotiation() 292 | if self.state == STATE_AUTH_USERPASS: 293 | self._parseUserPass() 294 | if self.state == STATE_REQUEST: 295 | self._parseRequest() 296 | 297 | def connectionLost(self, reason): 298 | if self.peersock and self.peersock.transport: 299 | self.peersock.transport.loseConnection() 300 | 301 | if __name__ == "__main__": 302 | factory = protocol.Factory() 303 | factory.protocol = SOCKSv5 304 | reactor.listenTCP(8888, factory) 305 | reactor.run() 306 | -------------------------------------------------------------------------------- /src/web_client.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import web 3 | from web import form 4 | 5 | import config 6 | import client 7 | 8 | render = web.template.render('web_client/') 9 | urls = ( 10 | '/', 'index' 11 | ) 12 | app = web.application(urls, globals()) 13 | 14 | clientconf = form.Form( 15 | form.Textbox( 16 | "Port", 17 | form.notnull, 18 | form.Validator( 19 | "Must be between 1000 and 65535", 20 | lambda x: 1000 <= int(x) <= 65535 21 | ), 22 | value = config.CLIENT_PORT), 23 | form.Button('Start'), 24 | # form.Button('Kill') 25 | ) 26 | 27 | class index: 28 | def GET(self): 29 | form = clientconf() 30 | return render.client(form) 31 | 32 | def POST(self): 33 | form = clientconf() 34 | if not form.validates(): 35 | return render.client(form) 36 | else: 37 | client.reactor.listenTCP(int(form["Port"].value), client.MelkwegLocalProxyFactory()) 38 | client.reactor.run() 39 | return "Success." 40 | 41 | if __name__=="__main__": 42 | web.internalerror = web.debugerror 43 | app.run() 44 | -------------------------------------------------------------------------------- /src/web_client/client.html: -------------------------------------------------------------------------------- 1 | $def with (form) 2 | 3 |
4 | $if not form.valid:

Try again, AmeriCAN:

5 | $:form.render() 6 |
-------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | 3 | import os 4 | import sys 5 | 6 | sys.path.insert(0, os.path.abspath('src')) 7 | -------------------------------------------------------------------------------- /test/test_protocol.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | 3 | import os 4 | import sys 5 | import pytest 6 | import logging 7 | 8 | from twisted.trial import unittest 9 | from twisted.test import proto_helpers 10 | from twisted.internet import defer 11 | from twisted.python import log 12 | 13 | from protocol import MelkwegClientProtocol, MelkwegServerProtocol, ProtocolStatus 14 | from packet_factory import PacketFactory 15 | 16 | class FakeTransport(object): 17 | def __init__(self): 18 | self.buffer = [] 19 | 20 | def write(self, data): 21 | self.buffer.append(data) 22 | 23 | class TestProtocol(unittest.TestCase): 24 | def test_ping_pong(self): 25 | server = MelkwegServerProtocol() 26 | client = MelkwegClientProtocol() 27 | 28 | server_fake_transport = FakeTransport() 29 | client_fake_transport = FakeTransport() 30 | 31 | server.transport = server_fake_transport 32 | client.transport = client_fake_transport 33 | 34 | client.connectionMade() 35 | self.assertEqual(len(client_fake_transport.buffer), 1) 36 | 37 | server.stringReceived(client_fake_transport.buffer[-1]) 38 | self.assertEqual(server.status, ProtocolStatus.RUNNING) 39 | 40 | client.stringReceived(server_fake_transport.buffer[-1]) 41 | self.assertEqual(client.status, ProtocolStatus.RUNNING) 42 | 43 | packet1 = PacketFactory.create_data_packet(1234, "hello world") 44 | client.write(packet1) 45 | 46 | 47 | --------------------------------------------------------------------------------