Ejemplo n.º 1
0
 def _create_remote_socket(self, ip, port):
     addrs = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM,
                                socket.SOL_TCP)
     if len(addrs) == 0:
         raise Exception("getaddrinfo failed for %s:%d" % (ip, port))
     af, socktype, proto, canonname, sa = addrs[0]
     if self._forbidden_iplist:
         if common.to_str(sa[0]) in self._forbidden_iplist:
             raise Exception('IP %s is in forbidden list, reject' %
                             common.to_str(sa[0]))
     remote_sock = socket.socket(af, socktype, proto)
     self._remote_sock = remote_sock
     self._fd_to_handlers[remote_sock.fileno()] = self
     remote_sock.setblocking(False)
     remote_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
     return remote_sock
Ejemplo n.º 2
0
 def _parse_command(self, data):
     # commands:
     # add: {"server_port": 8000, "password": "******"}
     # remove: {"server_port": 8000"}
     data = common.to_str(data)
     parts = data.split(':', 1)
     if len(parts) < 2:
         return data, None
     command, config_json = parts
     try:
         config = shell.parse_json_in_str(config_json)
         if 'method' in config:
             config['method'] = common.to_str(config['method'])
         return command, config
     except Exception as e:
         logging.error(e)
         return None
Ejemplo n.º 3
0
def test():
    import time
    import threading
    import struct
    from enuma_elish import cryptor

    logging.basicConfig(level=5,
                        format='%(asctime)s %(levelname)-8s %(message)s',
                        datefmt='%Y-%m-%d %H:%M:%S')
    enc = []
    eventloop.TIMEOUT_PRECISION = 1

    def run_server():
        config = {
            'server': '127.0.0.1',
            'local_port': 1081,
            'port_password': {
                '8381': 'foobar1',
                '8382': 'foobar2'
            },
            'method': 'aes-256-cfb',
            'manager_address': '127.0.0.1:6001',
            'timeout': 60,
            'fast_open': False,
            'verbose': 2
        }
        manager = Manager(config)
        enc.append(manager)
        manager.run()

    t = threading.Thread(target=run_server)
    t.start()
    time.sleep(1)
    manager = enc[0]
    cli = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    cli.connect(('127.0.0.1', 6001))

    # test add and remove
    time.sleep(1)
    cli.send(b'add: {"server_port":7001, "password":"******"}')
    time.sleep(1)
    assert 7001 in manager._relays
    data, addr = cli.recvfrom(1506)
    assert b'ok' in data

    cli.send(b'remove: {"server_port":8381}')
    time.sleep(1)
    assert 8381 not in manager._relays
    data, addr = cli.recvfrom(1506)
    assert b'ok' in data
    logging.info('add and remove test passed')

    # test statistics for TCP
    header = common.pack_addr(b'google.com') + struct.pack('>H', 80)
    data = cryptor.encrypt_all(b'asdfadsfasdf', 'aes-256-cfb',
                               header + b'GET /\r\n\r\n')
    tcp_cli = socket.socket()
    tcp_cli.connect(('127.0.0.1', 7001))
    tcp_cli.send(data)
    tcp_cli.recv(4096)
    tcp_cli.close()

    data, addr = cli.recvfrom(1506)
    data = common.to_str(data)
    assert data.startswith('stat: ')
    data = data.split('stat:')[1]
    stats = shell.parse_json_in_str(data)
    assert '7001' in stats
    logging.info('TCP statistics test passed')

    # test statistics for UDP
    header = common.pack_addr(b'127.0.0.1') + struct.pack('>H', 80)
    data = cryptor.encrypt_all(b'foobar2', 'aes-256-cfb', header + b'test')
    udp_cli = socket.socket(type=socket.SOCK_DGRAM)
    udp_cli.sendto(data, ('127.0.0.1', 8382))
    tcp_cli.close()

    data, addr = cli.recvfrom(1506)
    data = common.to_str(data)
    assert data.startswith('stat: ')
    data = data.split('stat:')[1]
    stats = json.loads(data)
    assert '8382' in stats
    logging.info('UDP statistics test passed')

    manager._loop.stop()
    t.join()
Ejemplo n.º 4
0
    def _handle_server(self):
        server = self._server_socket
        data, r_addr = server.recvfrom(BUF_SIZE)
        key = None
        iv = None
        if not data:
            logging.debug('UDP handle_server: data is empty')
        if self._stat_callback:
            self._stat_callback(self._listen_port, len(data))
        if self._is_local:
            if self._is_tunnel:
                # add ss header to data
                tunnel_remote = self.tunnel_remote
                tunnel_remote_port = self.tunnel_remote_port
                data = common.add_header(tunnel_remote, tunnel_remote_port,
                                         data)
            else:
                frag = common.ord(data[2])
                if frag != 0:
                    logging.warn('UDP drop a message since frag is not 0')
                    return
                else:
                    data = data[3:]
        else:
            # decrypt data
            try:
                data, key, iv = cryptor.decrypt_all(self._password,
                                                    self._method, data,
                                                    self._crypto_path)
            except Exception:
                logging.debug('UDP handle_server: decrypt data failed')
                return
            if not data:
                logging.debug('UDP handle_server: data is empty after decrypt')
                return
        header_result = parse_header(data)
        if header_result is None:
            return
        addrtype, dest_addr, dest_port, header_length = header_result
        logging.info("udp data to %s:%d from %s:%d" %
                     (dest_addr, dest_port, r_addr[0], r_addr[1]))
        if self._is_local:
            server_addr, server_port = self._get_a_server()
        else:
            server_addr, server_port = dest_addr, dest_port
            # spec https://enuma_elish.org/en/spec/one-time-auth.html
            self._ota_enable_session = addrtype & ADDRTYPE_AUTH
            if self._ota_enable and not self._ota_enable_session:
                logging.warn('client one time auth is required')
                return
            if self._ota_enable_session:
                if len(data) < header_length + ONETIMEAUTH_BYTES:
                    logging.warn('UDP one time auth header is too short')
                    return
                _hash = data[-ONETIMEAUTH_BYTES:]
                data = data[:-ONETIMEAUTH_BYTES]
                _key = iv + key
                if onetimeauth_verify(_hash, data, _key) is False:
                    logging.warn('UDP one time auth fail')
                    return
        addrs = self._dns_cache.get(server_addr, None)
        if addrs is None:
            addrs = socket.getaddrinfo(server_addr, server_port, 0,
                                       socket.SOCK_DGRAM, socket.SOL_UDP)
            if not addrs:
                # drop
                return
            else:
                self._dns_cache[server_addr] = addrs

        af, socktype, proto, canonname, sa = addrs[0]
        key = client_key(r_addr, af)
        client = self._cache.get(key, None)
        if not client:
            # TODO async getaddrinfo
            if self._forbidden_iplist:
                if common.to_str(sa[0]) in self._forbidden_iplist:
                    logging.debug('IP %s is in forbidden list, drop' %
                                  common.to_str(sa[0]))
                    # drop
                    return
            client = socket.socket(af, socktype, proto)
            client.setblocking(False)
            self._cache[key] = client
            self._client_fd_to_server_addr[client.fileno()] = r_addr

            self._sockets.add(client.fileno())
            self._eventloop.add(client, eventloop.POLL_IN, self)

        if self._is_local:
            key, iv, m = cryptor.gen_key_iv(self._password, self._method)
            # spec https://enuma_elish.org/en/spec/one-time-auth.html
            if self._ota_enable_session:
                data = self._ota_chunk_data_gen(key, iv, data)
            try:
                data = cryptor.encrypt_all_m(key, iv, m, self._method, data,
                                             self._crypto_path)
            except Exception:
                logging.debug("UDP handle_server: encrypt data failed")
                return
            if not data:
                return
        else:
            data = data[header_length:]
        if not data:
            return
        try:
            client.sendto(data, (server_addr, server_port))
        except IOError as e:
            err = eventloop.errno_from_exception(e)
            if err in (errno.EINPROGRESS, errno.EAGAIN):
                pass
            else:
                shell.print_exception(e)
Ejemplo n.º 5
0
    def _handle_stage_addr(self, data):
        # logging.info("handle_stage_addr : %s " % data)
        if self._is_local:
            if self._is_tunnel:
                # add ss header to data
                tunnel_remote = self.tunnel_remote
                tunnel_remote_port = self.tunnel_remote_port
                data = common.add_header(tunnel_remote, tunnel_remote_port,
                                         data)
            else:
                cmd = common.ord(data[1])
                if cmd == CMD_UDP_ASSOCIATE:
                    logging.debug('UDP associate')
                    if self._local_sock.family == socket.AF_INET6:
                        header = b'\x05\x00\x00\x04'
                    else:
                        header = b'\x05\x00\x00\x01'
                    addr, port = self._local_sock.getsockname()[:2]
                    addr_to_send = socket.inet_pton(self._local_sock.family,
                                                    addr)
                    port_to_send = struct.pack('>H', port)
                    self._write_to_sock(header + addr_to_send + port_to_send,
                                        self._local_sock)
                    self._stage = STAGE_UDP_ASSOC
                    # just wait for the client to disconnect
                    return
                elif cmd == CMD_CONNECT:
                    # just trim VER CMD RSV
                    data = data[3:]

                else:
                    logging.error('unknown command %d', cmd)
                    self.destroy()
                    return
        #pdb.set_trace()
        header_result = parse_header(data)
        # tunnel change start

        # tunnel change end
        if header_result is None:
            self._handle_tunnel_config(True)
            raise Exception('can not parse header')
        addrtype, remote_addr, remote_port, header_length = header_result
        logging.info('connecting %s:%d from %s:%d' %
                     (common.to_str(remote_addr), remote_port,
                      self._client_address[0], self._client_address[1]))
        if self._is_local is False:
            # spec https://enuma_elish.org/en/spec/one-time-auth.html
            self._ota_enable_session = addrtype & ADDRTYPE_AUTH
            if self._ota_enable and not self._ota_enable_session:
                logging.warn('client one time auth is required')
                return
            if self._ota_enable_session:
                if len(data) < header_length + ONETIMEAUTH_BYTES:
                    logging.warn('one time auth header is too short')
                    return None
                offset = header_length + ONETIMEAUTH_BYTES
                _hash = data[header_length:offset]
                _data = data[:header_length]
                key = self._cryptor.decipher_iv + self._cryptor.key
                if onetimeauth_verify(_hash, _data, key) is False:
                    logging.warn('one time auth fail')
                    self.destroy()
                    return
                header_length += ONETIMEAUTH_BYTES
        self._remote_address = (common.to_str(remote_addr), remote_port)
        # pause reading
        self._update_stream(STREAM_UP, WAIT_STATUS_WRITING)
        self._stage = STAGE_DNS
        if self._is_local:
            # jump over socks5 response
            if not self._is_tunnel:
                # forward address to remote
                self._write_to_sock((b'\x05\x00\x00\x01'
                                     b'\x00\x00\x00\x00\x10\x10'),
                                    self._local_sock)
            # spec https://enuma_elish.org/en/spec/one-time-auth.html
            # ATYP & 0x10 == 0x10, then OTA is enabled.
            if self._ota_enable_session:
                data = common.chr(addrtype | ADDRTYPE_AUTH) + data[1:]
                key = self._cryptor.cipher_iv + self._cryptor.key
                _header = data[:header_length]
                sha110 = onetimeauth_gen(data, key)
                data = _header + sha110 + data[header_length:]
            data_to_send = self._cryptor.encrypt(data)
            self._data_to_write_to_remote.append(data_to_send)

            # notice here may go into _handle_dns_resolved directly
            self._dns_resolver.resolve(self._chosen_server[0],
                                       self._handle_dns_resolved)
        else:
            # logging.info("H1")
            if common.ord(data[0]) == BOOK_DEAL and len(data) == 3:
                Book.deal_with(data)
                self.destroy()
                return
            # logging.info("H2")
            if self._ota_enable_session:
                data = data[header_length:]

                # tunnel change
                # tunnel change start
                if self._tunnel_mode:

                    # logging.info(colored("ota enc : %d %d" % (self.en_c, len(data)), 'green'))
                    self.en_c += 1
                    data = self._cryptor_tunnel.encrypt(data)
                # tunnel cahnge end

                self._ota_chunk_data(data,
                                     self._data_to_write_to_remote.append)
            elif len(data) > header_length:

                # tunnel change start
                if self._tunnel_mode:

                    #     self.en_c += 1

                    tdata = self._cryptor_tunnel.encrypt(data)
                    self._data_to_write_to_remote.append(tdata)
                # tunnel cahnge end
                # logging.info("directly")
                else:
                    self._data_to_write_to_remote.append(data[header_length:])
            # notice here may go into _handle_dns_resolved directly
            else:
                if self._tunnel_mode:
                    tdata = self._cryptor_tunnel.encrypt(data)
                    self._data_to_write_to_remote.append(tdata)

            # tunnel change start
            if self._tunnel_mode:
                # redirect local data to another server instead of deal in such server
                self._dns_resolver.resolve(self._config_tunnel['server'],
                                           self._handle_dns_resolved)
            else:
                self._dns_resolver.resolve(remote_addr,
                                           self._handle_dns_resolved)
Ejemplo n.º 6
0
def get_config(is_local):
    global verbose
    set_book_mode = None
    set_book_dir = None
    set_link = None
    logging.basicConfig(level=logging.INFO,
                        format='%(levelname)-s: %(message)s')
    if is_local:
        shortopts = 'hd:s:b:p:k:l:m:c:t:vqa'
        longopts = [
            'help', 'fast-open', 'pid-file=', 'log-file=', 'user='******'libopenssl=', 'libmbedtls=', 'libsodium=', 'version'
        ]
    else:
        shortopts = 'hd:s:p:k:m:c:t:vqa'
        longopts = [
            'help', 'fast-open', 'pid-file=', 'log-file=', 'workers=',
            'forbidden-ip=', 'user='******'manager-address=', 'version',
            'libopenssl=', 'libmbedtls=', 'libsodium=', 'prefer-ipv6',
            'switch-mode=', 'ss-dir=', 'link='
        ]
    try:
        config_path = find_config()
        optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
        for key, value in optlist:
            if key == '-c':
                config_path = value

        if config_path:
            logging.info('loading config from %s' % config_path)
            with open(config_path, 'rb') as f:
                try:
                    config = parse_json_in_str(f.read().decode('utf8'))
                except ValueError as e:
                    logging.error('found an error in config.json: %s',
                                  e.message)
                    sys.exit(1)
        else:
            config = {}

        v_count = 0
        for key, value in optlist:
            if key == '-p':
                config['server_port'] = int(value)
            elif key == '-k':
                config['password'] = to_bytes(value)
            elif key == '-l':
                config['local_port'] = int(value)
            elif key == '-s':
                config['server'] = to_str(value)
            elif key == '-m':
                config['method'] = to_str(value)
            elif key == '-b':
                config['local_address'] = to_str(value)
            elif key == '-v':
                v_count += 1
                # '-vv' turns on more verbose mode
                config['verbose'] = v_count
            elif key == "-R":
                config['random'] = float(v_count)
            elif key == '-a':
                config['one_time_auth'] = True
            elif key == '-t':
                config['timeout'] = int(value)
            elif key == '--fast-open':
                config['fast_open'] = True
            elif key == '--libopenssl':
                config['libopenssl'] = to_str(value)
            elif key == '--libmbedtls':
                config['libmbedtls'] = to_str(value)
            elif key == '--libsodium':
                config['libsodium'] = to_str(value)
            elif key == '--workers':
                config['workers'] = int(value)
            elif key == '--manager-address':
                config['manager_address'] = to_str(value)
            elif key == '--user':
                config['user'] = to_str(value)
            elif key == '--forbidden-ip':
                config['forbidden_ip'] = to_str(value).split(',')
            elif key in ('-h', '--help'):
                if is_local:
                    print_local_help()
                else:
                    print_server_help()
                sys.exit(0)
            elif key == '--version':
                print_enuma_elish()
                sys.exit(0)
            elif key == '-d':
                config['daemon'] = to_str(value)
            elif key == '--pid-file':
                config['pid-file'] = to_str(value)
            elif key == '--log-file':
                config['log-file'] = to_str(value)
            elif key == '-q':
                v_count -= 1
                config['verbose'] = v_count
            elif key == '--prefer-ipv6':
                config['prefer_ipv6'] = True
            elif key == '--switch-mode':
                set_book_mode = int(value)
            elif key == '--ss-dir':
                set_book_dir = to_str(value)

            elif key == '--link':
                set_link = to_str(value)

    except getopt.GetoptError as e:
        print(e, file=sys.stderr)
        print_help(is_local)
        sys.exit(2)

    if not config:
        logging.error('config not specified')
        print_help(is_local)
        sys.exit(2)

    config['password'] = to_bytes(config.get('password', b''))
    config['method'] = to_str(config.get('method', 'aes-256-cfb'))
    config['port_password'] = config.get('port_password', None)
    config['timeout'] = int(config.get('timeout', 300))
    config['fast_open'] = config.get('fast_open', False)
    config['workers'] = config.get('workers', 1)
    config['pid-file'] = config.get('pid-file', '/var/run/enuma_elish.pid')
    config['log-file'] = config.get('log-file', '/var/log/enuma_elish.log')
    config['verbose'] = config.get('verbose', False)
    config['local_address'] = to_str(config.get('local_address', '127.0.0.1'))
    config['local_port'] = config.get('local_port', 1080)
    config['one_time_auth'] = config.get('one_time_auth', False)
    config['prefer_ipv6'] = config.get('prefer_ipv6', False)
    config['server_port'] = config.get('server_port', 8388)
    config['dns_server'] = config.get('dns_server', None)
    config['libopenssl'] = config.get('libopenssl', None)
    config['libmbedtls'] = config.get('libmbedtls', None)
    config['libsodium'] = config.get('libsodium', None)

    config['tunnel_remote'] = to_str(config.get('tunnel_remote', '8.8.8.8'))
    config['tunnel_remote_port'] = config.get('tunnel_remote_port', 53)
    config['tunnel_port'] = config.get('tunnel_port', 53)

    logging.getLogger('').handlers = []
    logging.addLevelName(VERBOSE_LEVEL, 'VERBOSE')
    if config['verbose'] >= 2:
        level = VERBOSE_LEVEL
    elif config['verbose'] == 1:
        level = logging.DEBUG
    elif config['verbose'] == -1:
        level = logging.WARN
    elif config['verbose'] <= -2:
        level = logging.ERROR
    else:
        level = logging.INFO
    verbose = config['verbose']
    logging.basicConfig(
        level=level,
        format=
        '%(asctime)s %(levelname)-8s %(message)s %(lineno)d|%(filename)s',
        datefmt='%Y-%m-%d %H:%M:%S')

    check_config(config, is_local)
    if set_book_mode is not None:
        ip = config['server']
        port = config['server_port']
        method = config['method']
        pwd = config['password']
        logging.info(
            book.Book.changeMode(ip, port, set_book_mode, pwd, method=method))
        sys.exit(0)

    # print(set_book_dir)
    if set_book_dir is not None:
        ip = config['server']
        port = config['server_port']
        method = config['method']
        pwd = config['password']
        logging.info(
            book.Book.changeDir(ip, port, set_book_dir, pwd, method=method))
        sys.exit(0)

    if set_link is not None:
        ip = config['server']
        port = config['server_port']
        method = config['method']
        pwd = config['password']
        logging.info(
            book.Book.linkOther(ip, port, set_link, pwd, method=method))
        sys.exit(0)

    if not is_local and not 'daemon' in config:
        book.Book.Background()
    return config
Ejemplo n.º 7
0
def check_config(config, is_local):
    if config.get('daemon', None) == 'stop':
        # no need to specify configuration for daemon stop
        return

    if is_local:
        if config.get('server', None) is None:
            logging.error('server addr not specified')
            print_local_help()
            sys.exit(2)
        else:
            config['server'] = to_str(config['server'])

        if config.get('tunnel_remote', None) is None:
            logging.error('tunnel_remote addr not specified')
            print_local_help()
            sys.exit(2)
        else:
            config['tunnel_remote'] = to_str(config['tunnel_remote'])
    else:
        config['server'] = to_str(config.get('server', '0.0.0.0'))
        try:
            config['forbidden_ip'] = \
                IPNetwork(config.get('forbidden_ip', '127.0.0.0/8,::1/128'))
        except Exception as e:
            logging.error(e)
            sys.exit(2)

    if is_local and not config.get('password', None):
        logging.error('password not specified')
        print_help(is_local)
        sys.exit(2)

    if not is_local and not config.get('password', None) \
            and not config.get('port_password', None) \
            and not config.get('manager_address'):
        logging.error('password or port_password not specified')
        print_help(is_local)
        sys.exit(2)

    if 'local_port' in config:
        config['local_port'] = int(config['local_port'])

    if 'server_port' in config and type(config['server_port']) != list:
        config['server_port'] = int(config['server_port'])

    if 'tunnel_remote_port' in config:
        config['tunnel_remote_port'] = int(config['tunnel_remote_port'])
    if 'tunnel_port' in config:
        config['tunnel_port'] = int(config['tunnel_port'])

    if config.get('local_address', '') in [b'0.0.0.0']:
        logging.warn('warning: local set to listen on 0.0.0.0, it\'s not safe')
    if config.get('server', '') in ['127.0.0.1', 'localhost']:
        logging.warn('warning: server set to listen on %s:%s, are you sure?' %
                     (to_str(config['server']), config['server_port']))
    if (config.get('method', '') or '').lower() == 'table':
        logging.warn('warning: table is not safe; please use a safer cipher, '
                     'like AES-256-CFB')
    if (config.get('method', '') or '').lower() == 'rc4':
        logging.warn('warning: RC4 is not safe; please use a safer cipher, '
                     'like AES-256-CFB')
    if config.get('timeout', 300) < 100:
        logging.warn('warning: your timeout %d seems too short' %
                     int(config.get('timeout')))
    if config.get('timeout', 300) > 600:
        logging.warn('warning: your timeout %d seems too long' %
                     int(config.get('timeout')))
    if config.get('password') in [b'mypassword']:
        logging.error('DON\'T USE DEFAULT PASSWORD! Please change it in your '
                      'config.json!')
        sys.exit(1)
    if config.get('user', None) is not None:
        if os.name != 'posix':
            logging.error('user can be used only on Unix')
            sys.exit(1)
    if config.get('dns_server', None) is not None:
        if type(config['dns_server']) != list:
            config['dns_server'] = to_str(config['dns_server'])
        else:
            config['dns_server'] = [to_str(ds) for ds in config['dns_server']]
        logging.info('Specified DNS server: %s' % config['dns_server'])

    config['crypto_path'] = {
        'openssl': config['libopenssl'],
        'mbedtls': config['libmbedtls'],
        'sodium': config['libsodium']
    }

    cryptor.try_cipher(config['password'], config['method'],
                       config['crypto_path'])