Support for TLS MTProxy

This commit is contained in:
Wizou 2022-01-11 04:14:23 +01:00
parent 16258cb5ba
commit d6d656c8fe
6 changed files with 237 additions and 43 deletions

View file

@ -54,7 +54,7 @@ namespace WTelegram
private readonly Session _session;
private Session.DCSession _dcSession;
private TcpClient _tcpClient;
private NetworkStream _networkStream;
private Stream _networkStream;
private IObject _lastSentMsg;
private long _lastRecvMsgId;
private readonly List<long> _msgsToAck = new();
@ -223,6 +223,7 @@ namespace WTelegram
private async Task DoConnectAsync()
{
_cts = new();
IPEndPoint endpoint = null;
byte[] preamble, secret = null;
int dcId = _dcSession?.DcID ?? 0;
@ -235,9 +236,10 @@ namespace WTelegram
var server = parms["server"];
int port = int.Parse(parms["port"]);
var str = parms["secret"]; // can be hex or base64
secret = str.All("0123456789ABCDEFabcdef".Contains) ? Convert.FromHexString(str) :
var secretBytes = secret = str.All("0123456789ABCDEFabcdef".Contains) ? Convert.FromHexString(str) :
System.Convert.FromBase64String(str.Replace('_', '/').Replace('-', '+') + new string('=', (2147483644 - str.Length) % 4));
if ((secret.Length == 17 && secret[0] == 0xDD) || (secret.Length >= 21 && secret[0] == 0xEE))
var tlsMode = secret.Length >= 21 && secret[0] == 0xEE;
if (tlsMode || (secret.Length == 17 && secret[0] == 0xDD))
{
_paddedMode = true;
secret = secret[1..17];
@ -245,6 +247,9 @@ namespace WTelegram
else if (secret.Length != 16) throw new ArgumentException("Invalid/unsupported secret", nameof(secret));
Helpers.Log(2, $"Connecting to DC {dcId} via MTProxy {server}:{port}...");
_tcpClient = await TcpHandler(server, port);
_networkStream = _tcpClient.GetStream();
if (tlsMode)
_networkStream = await TlsStream.HandshakeAsync(_networkStream, secret, secretBytes[17..], _cts.Token);
#else
throw new Exception("Library was not compiled with OBFUSCATION symbol");
#endif
@ -305,8 +310,8 @@ namespace WTelegram
throw;
}
_tcpClient = tcpClient;
_networkStream = _tcpClient.GetStream();
}
_networkStream = _tcpClient.GetStream();
byte protocolId = (byte)(_paddedMode ? 0xDD : 0xEE);
#if OBFUSCATION
@ -314,9 +319,8 @@ namespace WTelegram
#else
preamble = new byte[] { protocolId, protocolId, protocolId, protocolId };
#endif
await _networkStream.WriteAsync(preamble, 0, preamble.Length);
await _networkStream.WriteAsync(preamble, 0, preamble.Length, _cts.Token);
_cts = new();
_saltChangeCounter = 0;
_reactorTask = Reactor(_networkStream, _cts);
_sendSemaphore.Release();
@ -445,7 +449,7 @@ namespace WTelegram
}
}
private async Task Reactor(NetworkStream stream, CancellationTokenSource cts)
private async Task Reactor(Stream stream, CancellationTokenSource cts)
{
const int MinBufferSize = 1024;
var data = new byte[MinBufferSize];
@ -454,17 +458,19 @@ namespace WTelegram
IObject obj = null;
try
{
if (await FullReadAsync(stream, data, 4, cts.Token) != 4)
if (await stream.FullReadAsync(data, 4, cts.Token) != 4)
throw new ApplicationException(ConnectionShutDown);
#if OBFUSCATION
_recvCtr.EncryptDecrypt(data, 4);
#endif
int payloadLen = BinaryPrimitives.ReadInt32LittleEndian(data);
if (payloadLen > data.Length)
if (payloadLen <= 0)
throw new ApplicationException("Could not read frame data : Invalid payload length");
else if (payloadLen > data.Length)
data = new byte[payloadLen];
else if (Math.Max(payloadLen, MinBufferSize) < data.Length / 4)
data = new byte[Math.Max(payloadLen, MinBufferSize)];
if (await FullReadAsync(stream, data, payloadLen, cts.Token) != payloadLen)
if (await stream.FullReadAsync(data, payloadLen, cts.Token) != payloadLen)
throw new ApplicationException("Could not read frame data : Connection shut down");
#if OBFUSCATION
_recvCtr.EncryptDecrypt(data, payloadLen);
@ -629,17 +635,6 @@ namespace WTelegram
};
}
private static async Task<int> FullReadAsync(Stream stream, byte[] buffer, int length, CancellationToken ct = default)
{
for (int offset = 0; offset < length;)
{
var read = await stream.ReadAsync(buffer, offset, length - offset, ct);
if (read == 0) return offset;
offset += read;
}
return length;
}
private async Task<long> SendAsync(IObject msg, bool isContent)
{
if (_dcSession.AuthKeyID != 0 && isContent && CheckMsgsToAck() is MsgsAck msgsAck)
@ -743,7 +738,7 @@ namespace WTelegram
seqno = reader.ReadInt32(),
bytes = reader.ReadInt32(),
};
if ((msg.seqno & 1) != 0) lock(_msgsToAck) _msgsToAck.Add(msg.msg_id);
if ((msg.seqno & 1) != 0) lock (_msgsToAck) _msgsToAck.Add(msg.msg_id);
var pos = reader.BaseStream.Position;
try
{
@ -801,11 +796,30 @@ namespace WTelegram
}
else
{
result = reader.ReadTLObject();
if (MsgIdToStamp(msgId) >= _session.SessionStart)
Helpers.Log(4, $" → {result?.GetType().Name,-37} for unknown msgId #{(short)msgId.GetHashCode():X4}");
string typeName;
var ctorNb = reader.ReadUInt32();
if (ctorNb == Layer.VectorCtor)
{
reader.BaseStream.Position -= 4;
var array = reader.ReadTLVector(typeof(IObject[]));
if (array.Length > 0)
{
for (type = array.GetValue(0).GetType(); type.BaseType != typeof(object);) type = type.BaseType;
typeName = type.Name + "[]";
}
else
typeName = "object[]";
result = array;
}
else
Helpers.Log(1, $" → {result?.GetType().Name,-37} for past msgId #{(short)msgId.GetHashCode():X4}");
{
result = reader.ReadTLObject(ctorNb);
typeName = result?.GetType().Name;
}
if (MsgIdToStamp(msgId) >= _session.SessionStart)
Helpers.Log(4, $" → {typeName,-37} for unknown msgId #{(short)msgId.GetHashCode():X4}");
else
Helpers.Log(1, $" → {typeName,-37} for past msgId #{(short)msgId.GetHashCode():X4}");
}
return new RpcResult { req_msg_id = msgId, result = result };
}
@ -1219,7 +1233,7 @@ namespace WTelegram
for (long bytesLeft = length; !abort && bytesLeft != 0; file_part++)
{
var bytes = new byte[Math.Min(FilePartSize, bytesLeft)];
read = await FullReadAsync(stream, bytes, bytes.Length);
read = await stream.FullReadAsync(bytes, bytes.Length, default);
await _parallelTransfers.WaitAsync();
bytesLeft -= read;
var task = SavePart(file_part, bytes);
@ -1369,7 +1383,7 @@ namespace WTelegram
/// <returns>MIME type of the document/thumbnail</returns>
public async Task<string> DownloadFileAsync(Document document, Stream outputStream, PhotoSizeBase thumbSize = null, ProgressCallback progress = null)
{
if (thumbSize is PhotoStrippedSize psp)
if (thumbSize is PhotoStrippedSize psp)
return InflateStrippedThumb(outputStream, psp.bytes) ? "image/jpeg" : null;
var fileLocation = document.ToFileLocation(thumbSize);
var fileType = await DownloadFileAsync(fileLocation, outputStream, document.dc_id, thumbSize?.FileSize ?? document.size, progress);
@ -1617,7 +1631,7 @@ namespace WTelegram
return new Updates { date = DateTime.UtcNow, users = new(), updates = Array.Empty<Update>(),
chats = (await this.Messages_GetChats(new[] { chat.chat_id })).chats };
case InputPeerChannel channel:
return await this.Channels_EditAdmin(channel, user,
return await this.Channels_EditAdmin(channel, user,
new ChatAdminRights { flags = is_admin ? (ChatAdminRights.Flags)0x8BF : 0 }, null);
default:
throw new ArgumentException("This method works on Chat & Channel only");

View file

@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
namespace WTelegram
{
@ -40,6 +42,19 @@ namespace WTelegram
#endif
}
public static async Task<int> FullReadAsync(this Stream stream, byte[] buffer, int length, CancellationToken ct)
{
for (int offset = 0; offset < length;)
{
#pragma warning disable CA1835
var read = await stream.ReadAsync(buffer, offset, length - offset, ct);
#pragma warning restore CA1835
if (read == 0) return offset;
offset += read;
}
return length;
}
internal static byte[] ToBigEndian(ulong value) // variable-size buffer
{
int i = 1;

View file

@ -358,8 +358,10 @@ namespace TL
partial class Contacts_ResolvedPeer
{
public static implicit operator InputPeer(Contacts_ResolvedPeer resolved) => resolved.UserOrChat.ToInputPeer();
/// <returns>A <see cref="TL.User"/>, or <see langword="null"/> if the username was for a channel</returns>
public User User => peer is PeerUser pu ? users[pu.user_id] : null;
public ChatBase Chat => peer is PeerChat or PeerChannel ? chats[peer.ID] : null;
/// <returns>A <see cref="Channel"/> or <see cref="ChannelForbidden"/>, or <see langword="null"/> if the username was for a user</returns>
public ChatBase Chat => peer is PeerChannel or PeerChat ? chats[peer.ID] : null;
}
partial class Updates_ChannelDifferenceBase

View file

@ -1,6 +1,7 @@
// This file is generated automatically using the Generator class
// This file is generated automatically
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace TL
{
@ -14,7 +15,8 @@ namespace TL
internal const uint MsgContainerCtor = 0x73F1F8DC;
internal const uint BadMsgCtor = 0xA7EFF811;
internal readonly static Dictionary<uint, Type> Table = new()
[EditorBrowsable(EditorBrowsableState.Never)]
public readonly static Dictionary<uint, Type> Table = new()
{
[0xF35C6D01] = typeof(RpcResult),
[0x5BB8E511] = typeof(_Message),

161
src/TlsStream.cs Normal file
View file

@ -0,0 +1,161 @@
using System;
using System.Buffers.Binary;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
// necessary for .NET Standard 2.0 compilation:
#pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
namespace WTelegram
{
class TlsStream : Stream
{
public TlsStream(Stream innerStream) => _innerStream = innerStream;
private readonly Stream _innerStream;
private int _tlsFrameleft;
private readonly byte[] _tlsSendHeader = new byte[] { 0x17, 0x03, 0x03, 0, 0 };
private readonly byte[] _tlsReadHeader = new byte[5];
static readonly byte[] TlsServerHelloPart3 = new byte[] { 0x14, 0x03, 0x03, 0x00, 0x01, 0x01, 0x17, 0x03, 0x03 };
static readonly byte[] TlsClientPrefix = new byte[] { 0x14, 0x03, 0x03, 0x00, 0x01, 0x01 };
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
public override void Flush() => _innerStream.Flush();
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct)
{
if (_tlsFrameleft == 0)
{
if (await _innerStream.FullReadAsync(_tlsReadHeader, 5, ct) != 5)
return 0;
if (_tlsReadHeader[0] != 0x17 || _tlsReadHeader[1] != 0x03 || _tlsReadHeader[2] != 0x03)
throw new ApplicationException("Could not read frame data : Invalid TLS header");
_tlsFrameleft = (_tlsReadHeader[3] << 8) + _tlsReadHeader[4];
}
var read = await _innerStream.ReadAsync(buffer, offset, Math.Min(count, _tlsFrameleft), ct);
_tlsFrameleft -= read;
return read;
}
public override async Task WriteAsync(byte[] buffer, int start, int count, CancellationToken ct)
{
for (int offset = 0; offset < count;)
{
int len = Math.Min(count - offset, 2878);
_tlsSendHeader[3] = (byte)(len >> 8);
_tlsSendHeader[4] = (byte)len;
await _innerStream.WriteAsync(_tlsSendHeader, 0, _tlsSendHeader.Length, ct);
await _innerStream.WriteAsync(buffer, start + offset, len, ct);
offset += len;
}
}
public static async Task<TlsStream> HandshakeAsync(Stream stream, byte[] key, byte[] domain, CancellationToken ct)
{
var clientHello = TlsClientHello(key, domain);
await stream.WriteAsync(clientHello, 0, clientHello.Length, ct);
var part1 = new byte[5];
if (await stream.FullReadAsync(part1, 5, ct) == 5)
if (part1[0] == 0x16 && part1[1] == 0x03 && part1[2] == 0x03)
{
var part2size = BinaryPrimitives.ReadUInt16BigEndian(part1.AsSpan(3));
var part23 = new byte[part2size + TlsServerHelloPart3.Length + 2];
if (await stream.FullReadAsync(part23, part23.Length, ct) == part23.Length)
if (TlsServerHelloPart3.SequenceEqual(part23.Skip(part2size).Take(TlsServerHelloPart3.Length)))
{
var part4size = BinaryPrimitives.ReadUInt16BigEndian(part23.AsSpan(part23.Length - 2));
var part4 = new byte[part4size];
if (await stream.FullReadAsync(part4, part4size, ct) == part4size)
{
var serverDigest = part23[6..38];
Array.Clear(part23, 6, 32); // clear server digest from received parts
var hmc = new HMACSHA256(key); // hash the client digest + all received parts
hmc.TransformBlock(clientHello, 11, 32, null, 0);
hmc.TransformBlock(part1, 0, part1.Length, null, 0);
hmc.TransformBlock(part23, 0, part23.Length, null, 0);
hmc.TransformFinalBlock(part4, 0, part4.Length);
if (serverDigest.SequenceEqual(hmc.Hash))
{
Helpers.Log(2, "TLS Handshake succeeded");
await stream.WriteAsync(TlsClientPrefix, 0, TlsClientPrefix.Length, ct);
return new TlsStream(stream);
}
}
}
}
throw new ApplicationException("TLS Handshake failed");
}
static readonly byte[] TlsClientHello1 = new byte[] {
0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03 };
static readonly byte[] TlsClientHello2 = new byte[] {
0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9,
0xcc, 0xa8, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0x01, 0x00, 0x01, 0x93 };
static readonly byte[] TlsClientHello3 = new byte[] {
0x00, 0x17, 0x00, 0x00, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08,
0, 0, // grease 4
0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x23, 0x00, 0x00,
0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31,
0x2e, 0x31, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x12, 0x00,
0x10, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 0x06,
0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x33, 0x00, 0x2b, 0x00, 0x29,
0, 0, // grease 4
0x00, 0x01, 0x00, 0x00, 0x1d, 0x00, 0x20 };
static readonly byte[] TlsClientHello4 = new byte[] {
0x00, 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, 0x2b, 0x00, 0x0b, 0x0a,
0, 0, // grease 6
0x03, 0x04, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02,
0, 0, // grease 3
0x00, 0x01, 0x00, 0x00, 0x15 };
static byte[] TlsClientHello(byte[] key, byte[] domain)
{
int dlen = domain.Length;
var greases = new byte[7];
Encryption.RNG.GetBytes(greases);
for (int i = 0; i < 7; i++) greases[i] = (byte)((greases[i] & 0xF0) + 0x0A);
if (greases[3] == greases[2]) greases[3] = (byte)(0x10 ^ greases[3]);
var buffer = new byte[517];
TlsClientHello1.CopyTo(buffer, 0);
TlsClientHello2.CopyTo(buffer, 80);
buffer[43] = buffer[77] = 0x20;
Encryption.RNG.GetBytes(buffer, 44, 32);
buffer[78] = buffer[79] = greases[0];
buffer[114] = buffer[115] = greases[2];
buffer[121] = (byte)(dlen + 5);
buffer[123] = (byte)(dlen + 3);
buffer[126] = (byte)dlen;
domain.CopyTo(buffer, 127);
TlsClientHello3.CopyTo(buffer, 127 + dlen);
buffer[142 + dlen] = buffer[143 + dlen] = greases[4];
buffer[219 + dlen] = buffer[220 + dlen] = greases[4];
Encryption.RNG.GetBytes(buffer, 228 + dlen, 32); // public key
TlsClientHello4.CopyTo(buffer, 260 + dlen);
buffer[271 + dlen] = buffer[272 + dlen] = greases[6];
buffer[288 + dlen] = buffer[289 + dlen] = greases[3];
buffer[296 + dlen] = (byte)(220 - dlen);
// patch-in digest with timestamp
using var hmac = new HMACSHA256(key);
var digest = hmac.ComputeHash(buffer);
var stamp = BinaryPrimitives.ReadInt32LittleEndian(digest.AsSpan(28));
stamp ^= (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
BinaryPrimitives.WriteInt32LittleEndian(digest.AsSpan(28), stamp);
digest.CopyTo(buffer, 11);
return buffer;
}
}
}