Initial implementation of MTProxy support (#1107)

This commit is contained in:
Dmitry D. Chernov 2019-02-11 09:16:46 +10:00 committed by Lonami
parent 45fdd098cc
commit 45d0ba9e2f
8 changed files with 77 additions and 24 deletions

View File

@ -9,7 +9,7 @@ from datetime import datetime, timezone
from .. import version, __name__ as __base_name__
from ..crypto import rsa
from ..extensions import markdown
from ..network import MTProtoSender, ConnectionTcpFull
from ..network import MTProtoSender, ConnectionTcpFull, ConnectionTcpMTProxy
from ..sessions import Session, SQLiteSession, MemorySession
from ..tl import TLObject, functions, types
from ..tl.alltlobjects import LAYER
@ -245,6 +245,8 @@ class TelegramBaseClient(abc.ABC):
assert isinstance(connection, type)
self._connection = connection
init_proxy = None if connection is not ConnectionTcpMTProxy else \
types.InputClientProxy(*ConnectionTcpMTProxy.address_info(proxy))
# Used on connection. Capture the variables in a lambda since
# exporting clients need to create this InvokeWithLayerRequest.
@ -258,7 +260,8 @@ class TelegramBaseClient(abc.ABC):
lang_code=lang_code,
system_lang_code=system_lang_code,
lang_pack='', # "langPacks are for official apps only"
query=x
query=x,
proxy=init_proxy
)
)
@ -345,6 +348,7 @@ class TelegramBaseClient(abc.ABC):
await self._sender.connect(self._connection(
self.session.server_address,
self.session.port,
self.session.dc_id,
loop=self._loop,
loggers=self._log,
proxy=self._proxy
@ -474,6 +478,7 @@ class TelegramBaseClient(abc.ABC):
await sender.connect(self._connection(
dc.ip_address,
dc.port,
dc.id,
loop=self._loop,
loggers=self._log,
proxy=self._proxy
@ -505,6 +510,7 @@ class TelegramBaseClient(abc.ABC):
await sender.connect(self._connection(
dc.ip_address,
dc.port,
dc.id,
loop=self._loop,
loggers=self._log,
proxy=self._proxy

View File

@ -6,6 +6,6 @@ from .mtprotoplainsender import MTProtoPlainSender
from .authenticator import do_authentication
from .mtprotosender import MTProtoSender
from .connection import (
ConnectionTcpFull, ConnectionTcpAbridged, ConnectionTcpObfuscated,
ConnectionTcpIntermediate, ConnectionHttp
ConnectionTcpFull, ConnectionTcpIntermediate, ConnectionTcpAbridged,
ConnectionTcpObfuscated, ConnectionTcpMTProxy, ConnectionHttp
)

View File

@ -10,7 +10,7 @@ from ..tl.types import (
ResPQ, PQInnerData, ServerDHParamsFail, ServerDHParamsOk,
ServerDHInnerData, ClientDHInnerData, DhGenOk, DhGenRetry, DhGenFail
)
from .. import helpers as utils
from .. import helpers
from ..crypto import AES, AuthKey, Factorization, rsa
from ..errors import SecurityError
from ..extensions import BinaryReader
@ -94,7 +94,7 @@ async def do_authentication(sender):
'Step 2.2 answer was %s' % server_dh_params
# Step 3 sending: Complete DH Exchange
key, iv = utils.generate_key_data_from_nonce(
key, iv = helpers.generate_key_data_from_nonce(
res_pq.server_nonce, new_nonce
)
if len(server_dh_params.encrypted_answer) % 16 != 0:

View File

@ -1,5 +1,6 @@
from .tcpfull import ConnectionTcpFull
from .tcpintermediate import ConnectionTcpIntermediate
from .tcpabridged import ConnectionTcpAbridged
from .tcpobfuscated import ConnectionTcpObfuscated
from .tcpintermediate import ConnectionTcpIntermediate
from .tcpmtproxy import ConnectionTcpMTProxy
from .http import ConnectionHttp

View File

@ -18,9 +18,10 @@ class Connection(abc.ABC):
``ConnectionError``, which will raise when attempting to send if
the client is disconnected (includes remote disconnections).
"""
def __init__(self, ip, port, *, loop, loggers, proxy=None):
def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
self._ip = ip
self._port = port
self._dc_id = dc_id # only for MTProxy, it's an abstraction leak
self._loop = loop
self._log = loggers[__name__]
self._proxy = proxy
@ -94,12 +95,6 @@ class Connection(abc.ABC):
if self._writer:
self._writer.close()
def clone(self):
"""
Creates a clone of the connection.
"""
return self.__class__(self._ip, self._port, loop=self._loop)
def send(self, data):
"""
Sends a packet of data through this connection mode.

View File

@ -10,8 +10,9 @@ class ConnectionTcpFull(Connection):
Default Telegram mode. Sends 12 additional bytes and
needs to calculate the CRC value of the packet itself.
"""
def __init__(self, ip, port, *, loop, loggers, proxy=None):
super().__init__(ip, port, loop=loop, loggers=loggers, proxy=proxy)
def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
super().__init__(
ip, port, dc_id, loop=loop, loggers=loggers, proxy=proxy)
self._send_counter = 0
async def connect(self, timeout=None, ssl=None):

View File

@ -0,0 +1,40 @@
import hashlib
from .tcpobfuscated import ConnectionTcpObfuscated
from ...crypto import AESModeCTR
class ConnectionTcpMTProxy(ConnectionTcpObfuscated):
"""
Wrapper around the "obfuscated2" mode that modifies it a little and allows
user to connect to the Telegram proxy servers commonly known as MTProxy.
Implemented very ugly due to the leaky abstractions in Telethon networking
classes that should be refactored later (TODO).
"""
@staticmethod
def address_info(proxy_info):
if proxy_info is None:
raise ValueError("No proxy info specified for MTProxy connection")
return proxy_info[:2]
def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
proxy_host, proxy_port = self.address_info(proxy)
super().__init__(
proxy_host, proxy_port, dc_id, loop=loop, loggers=loggers)
# TODO: Implement the dd-secret secure mode (adds noise to fool DPI)
self._secret = bytes.fromhex(proxy[2])
if len(self._secret) != 16:
raise ValueError(
"MTProxy secure mode is not implemented for now"
if len(self._secret) == 17 and self._secret[0] == 0xDD else
"MTProxy secret must be a hex-string representing 16 bytes"
)
def _compose_key(self, data):
return hashlib.sha256(data + self._secret).digest()
def _compose_tail(self, data):
dc_id_bytes = self._dc_id.to_bytes(2, "little", signed=True)
tail_bytes = super()._compose_tail(data)
return tail_bytes[:4] + dc_id_bytes + tail_bytes[6:]

View File

@ -7,12 +7,14 @@ from ...crypto import AESModeCTR
class ConnectionTcpObfuscated(ConnectionTcpAbridged):
"""
Encodes the packet just like `ConnectionTcpAbridged`, but encrypts
every message with a randomly generated key using the
AES-CTR mode so the packets are harder to discern.
Mode that Telegram defines as "obfuscated2". Encodes the packet
just like `ConnectionTcpAbridged`, but encrypts every message with
a randomly generated key using the AES-CTR mode so the packets are
harder to discern.
"""
def __init__(self, ip, port, *, loop, loggers, proxy=None):
super().__init__(ip, port, loop=loop, loggers=loggers, proxy=proxy)
def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
super().__init__(
ip, port, dc_id, loop=loop, loggers=loggers, proxy=proxy)
self._aes_encrypt = None
self._aes_decrypt = None
@ -40,14 +42,22 @@ class ConnectionTcpObfuscated(ConnectionTcpAbridged):
random_reversed = random[55:7:-1] # Reversed (8, len=48)
# Encryption has "continuous buffer" enabled
encrypt_key = bytes(random[8:40])
encrypt_key = self._compose_key(bytes(random[8:40]))
encrypt_iv = bytes(random[40:56])
decrypt_key = bytes(random_reversed[:32])
decrypt_key = self._compose_key(bytes(random_reversed[:32]))
decrypt_iv = bytes(random_reversed[32:48])
self._aes_encrypt = AESModeCTR(encrypt_key, encrypt_iv)
self._aes_decrypt = AESModeCTR(decrypt_key, decrypt_iv)
random[56:64] = self._aes_encrypt.encrypt(bytes(random))[56:64]
random[56:64] = self._compose_tail(bytes(random))
self._writer.write(random)
await self._writer.drain()
# Next functions provide the variable parts of the connection handshake.
# This is necessary to modify obfuscated2 the way that MTProxy requires.
def _compose_key(self, data):
return data
def _compose_tail(self, data):
return self._aes_encrypt.encrypt(data)[56:64]