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

View File

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

View File

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

View File

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

View File

@ -18,9 +18,10 @@ class Connection(abc.ABC):
``ConnectionError``, which will raise when attempting to send if ``ConnectionError``, which will raise when attempting to send if
the client is disconnected (includes remote disconnections). 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._ip = ip
self._port = port self._port = port
self._dc_id = dc_id # only for MTProxy, it's an abstraction leak
self._loop = loop self._loop = loop
self._log = loggers[__name__] self._log = loggers[__name__]
self._proxy = proxy self._proxy = proxy
@ -94,12 +95,6 @@ class Connection(abc.ABC):
if self._writer: if self._writer:
self._writer.close() 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): def send(self, data):
""" """
Sends a packet of data through this connection mode. 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 Default Telegram mode. Sends 12 additional bytes and
needs to calculate the CRC value of the packet itself. needs to calculate the CRC value of the packet itself.
""" """
def __init__(self, ip, port, *, loop, loggers, proxy=None): def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
super().__init__(ip, port, loop=loop, loggers=loggers, proxy=proxy) super().__init__(
ip, port, dc_id, loop=loop, loggers=loggers, proxy=proxy)
self._send_counter = 0 self._send_counter = 0
async def connect(self, timeout=None, ssl=None): 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): class ConnectionTcpObfuscated(ConnectionTcpAbridged):
""" """
Encodes the packet just like `ConnectionTcpAbridged`, but encrypts Mode that Telegram defines as "obfuscated2". Encodes the packet
every message with a randomly generated key using the just like `ConnectionTcpAbridged`, but encrypts every message with
AES-CTR mode so the packets are harder to discern. 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): def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None):
super().__init__(ip, port, loop=loop, loggers=loggers, proxy=proxy) super().__init__(
ip, port, dc_id, loop=loop, loggers=loggers, proxy=proxy)
self._aes_encrypt = None self._aes_encrypt = None
self._aes_decrypt = None self._aes_decrypt = None
@ -40,14 +42,22 @@ class ConnectionTcpObfuscated(ConnectionTcpAbridged):
random_reversed = random[55:7:-1] # Reversed (8, len=48) random_reversed = random[55:7:-1] # Reversed (8, len=48)
# Encryption has "continuous buffer" enabled # 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]) 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]) decrypt_iv = bytes(random_reversed[32:48])
self._aes_encrypt = AESModeCTR(encrypt_key, encrypt_iv) self._aes_encrypt = AESModeCTR(encrypt_key, encrypt_iv)
self._aes_decrypt = AESModeCTR(decrypt_key, decrypt_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) self._writer.write(random)
await self._writer.drain() 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]