Крытыка пратакола і аргпадыходаў Telegram. Частка 1, тэхнічная: вопыт напісання кліента з нуля - TL, MT

У апошні час на Хабре сталі гушчару з'яўляцца пасты аб тым, як добры Telegram, як геніяльныя і дасведчаныя браты Дуровы ў пабудове сеткавых сістэм, і да т.п. У той жа час, вельмі мала хто сапраўды апускаўся ў тэхнічную прыладу - як максімум, выкарыстоўваюць досыць просты (і вельмі адрозны ад MTProto) Bot API на базе JSON, а звычайна проста прымаюць на веру усе тыя дыфірамбы і піяр, што круцяцца вакол месэнджэра. Амаль паўтара гады таму мой калега па НВА "Эшалон" Васіль (нажаль, яго ўлік на Хабры сцерлі разам з чарнавіком) пачаў пісаць свой уласны кліент Telegram з нуля на Perl, пазней далучыўся і аўтар гэтых радкоў. Чаму на Perl неадкладна спытаюць некаторыя? Таму што на іншых мовах такія праекты ўжо ёсць. Насамрэч, сутнасць не ў гэтым, магла быць любая іншая мова, дзе яшчэ няма гатовай бібліятэкі, і адпаведна аўтар павінен прайсці ўвесь шлях з нуля. Тым больш, крыптаграфія справа такая - давярай, але правярай. З прадуктам, накіраваным на бяспеку, вы не можаце проста ўзяць і пакласціся на гатовую бібліятэку ад вытворцы, слепа яму паверыўшы (зрэшты, гэта тэма больш для другой часткі). На дадзены момант бібліятэка цалкам працуе на "сярэднім" узроўні (дазваляе рабіць любыя API-запыты).

Тым не менш, у дадзенай серыі пастоў будзе не так шмат крыптаграфіі і матэматыкі. Затое будзе шмат іншых тэхнічных падрабязнасцей і архітэктурных мыліц (спатрэбіцца і тым, хто не будзе пісаць з нуля, а будзе карыстацца бібліятэкай на любой мове). Такім чынам, галоўнай мэтай было - паспрабаваць рэалізаваць кліент з нуля па афіцыйнай дакументацыі. Гэта значыць, выкажам здагадку, што зыходны код афіцыйных кліентаў зачынены (ізноў жа ў другой частцы падрабязней раскрыем тэму таго, што гэта і праўда бывае так), але, як у старыя часы, напрыклад, ёсць стандарт па тыпе RFC – ці магчыма напісаць кліент па адной толькі спецыфікацыі, "не падглядваючы" у зыходнікі, хоць афіцыйных (Telegram Desktop, мабільных), хоць неафіцыйных Telethon?

змест:

Дакументацыя… яна ж ёсць? Праўда?..

Фрагменты нататак для гэтага артыкула пачалі збірацца яшчэ мінулым летам. Увесь гэты час на афіцыйным сайце https://core.telegram.org дакументацыя была па стане на Layer 23, г.зн. затрымаўшыся недзе ў 2014 годзе (памятаеце, тады нават каналаў яшчэ не было?). Вядома, па ідэі, гэта павінна было дазваляць рэалізаваць кліент з функцыянальнасцю на той момант 2014 года. Але і ў такім стане дакументацыя была, па-першае, няпоўная, па-другое, месцамі супярэчыла сама сабе. Крыху больш за месяц таму, у верасні 2019, было выпадкова выяўлена, што на сайце вялікае абнаўленне дакументацыі, на суцэль свежы Layer 105, з пазнакай, што зараз усю трэба чытаць нанова. Сапраўды, многія артыкулы былі перапрацаваны, але многія — так і засталіся без змен. Таму, чытаючы крытыку ніжэй наконт дакументацыі, варта мець на ўвазе, што некаторыя з гэтых рэчаў ужо неактуальныя, але некаторыя — усё яшчэ цалкам. У рэшце рэшт, 5 гадоў у сучасным свеце - гэта не проста шмат, а вельмі шмат. З тых часоў (асабліва калі не ўлічваць выкінутыя і зноўку адроджаныя з тых часоў геочаты) лік API-метадаў у схеме вырасла з сотні да больш за двухсот пяцідзесяці!

З чаго пачаць маладому аўтару?

Усё роўна, ці пішаце Вы з нуля ці выкарыстоўваеце напрыклад гатовыя бібліятэкі тыпу Telethon для Python або Madeline для PHP, у любым выпадку Вам спатрэбіцца спачатку зарэгістраваць сваё прыкладанне - атрымаць параметры api_id и api_hash (якія працавалі з API Укантакце адразу разумеюць), па якіх сервер будзе ідэнтыфікаваць прыкладанне. Гэта давядзецца зрабіць і па юрыдычных меркаваннях, але падрабязней аб тым, чаму аўтары бібліятэк не могуць яго публікаваць, пагаворым у другой частцы. Магчыма, вас задаволяць тэставыя значэнні, хаця яны моцна абмежаваныя — справа ў тым, што зараз на свой нумар можна зарэгістраваць. толькі адно дадатак, так што не кідайцеся адразу акрэсліўшы галаву.

Цяпер жа нас, з тэхнічнага пункту гледжання, павінна было цікавіць тое, што пасля рэгістрацыі нам павінны прыходзіць ад Telegram апавяшчэння аб абнаўленнях дакументацыі, пратаколу, і г.д. Гэта значыць можна было б выказаць здагадку, што на сайт з докамі проста "забілі" і працягнулі працаваць канкрэтна з тымі, хто стаў рабіць кліенты, т.я. так прасцей. Але не, нічога такога не назіралася, ніякай інфармацыі не прыходзіла.

І калі пісаць з нуля, то да выкарыстання атрыманых параметраў насамрэч яшчэ далёка. Хаця https://core.telegram.org/ і кажа ў Getting Started пра іх перш за ўсё, на самай справе спачатку давядзецца рэалізаваць пратакол MTProto - але калі Вы паверылі раскладцы па мадэлі OSI у канцы старонкі агульнага апісання пратакола, то зусім дарма.

Насамрэч, і да MTProto, і пасля, на некалькіх узроўнях адразу (як кажуць замежныя якія працуюць у ядры АС сецявікі, layer violation) на шляхі ўстане вялікая, хворая і жудасная тэма…

Бінарная серыялізацыя: TL (Type Language) і яго схема, і пласты, і шмат іншых страшных слоў

Гэтая тэма, уласна, у праблемах Telegram – ключавая. І страшных слоў, калі Вы паспрабуеце ў яе ўглыбіцца, будзе шмат.

Такім чынам, схема. Калі на гэтае слова Вам успомнілася, скажам, Схема JSON, Вы падумалі правільна. Мэта тая ж: некаторая мова для апісання магчымага набору перадаваных дадзеных. На гэтым, уласна, падабенства і заканчваецца. Калі са старонкі пратакола MTProto, або з дрэва зыходных тэкстаў афіцыйнага кліента, мы паспрабуем адкрыць якую-небудзь схему, то ўбачым нешта накшталт:

int ? = Int;
long ? = Long;
double ? = Double;
string ? = String;

vector#1cb5c415 {t:Type} # [ t ] = Vector t;

rpc_error#2144ca19 error_code:int error_message:string = RpcError;

rpc_answer_unknown#5e2ad36e = RpcDropAnswer;
rpc_answer_dropped_running#cd78e586 = RpcDropAnswer;
rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer;

msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;

---functions---

set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer;

ping#7abe77ec ping_id:long = Pong;
ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;

invokeAfterMsg#cb9f372d msg_id:long query:!X = X;
invokeAfterMsgs#3dc4b4f0 msg_ids:Vector<long> query:!X = X;

account.updateProfile#78515775 flags:# first_name:flags.0?string last_name:flags.1?string about:flags.2?string = User;
account.sendChangePhoneCode#8e57deb flags:# allow_flashcall:flags.0?true phone_number:string current_number:flags.0?Bool = auth.SentCode;

Чалавек, які бачыць гэта ўпершыню, інтуітыўна зможа распазнаць толькі частку напісанага - ну, гэта мабыць структуры (хоць дзе імя, злева ці справа?), вось ёсць палі ў іх, пасля якіх праз двукроп'е ідзе тып… мусіць. Вось у кутніх дужках напэўна шаблоны як у Сі++ (насамрэч, не зусім). А што значаць усе астатнія знакі, знакі пытання, клічнікі, працэнты, рашоткі (прычым відавочна ж у розных месцах значаць рознае), дзесьці прысутныя, а дзесьці не, шаснаццацірычныя цыферкі - і самае галоўнае, як з гэтага атрымаць правільны (які не будзе адпрэчаны серверам) паток байт? Прыйдзецца чытаць дакументацыю (так, тамака побач бываюць спасылкі на схему ў JSON-версіі — але зразумелей ад гэтага не становіцца).

Адкрываем старонку Binary Data Serialization і апускаемся ў чароўны свет грыбоў і дыскрэтнай матэматыкі нешта падобнае на матан на 4 курсе. Алфавіт, тып, значэнне, камбінатар, функцыянальны камбінатар, нармальная форма, кампазітны тып, паліморфны тып… і ўсё гэта толькі першая старонка! Далей Вас чакае TL Language, які хоць ужо і змяшчае прыклад трывіяльнага запыту і адказу, зусім не дае адказу на больш тыповыя выпадкі, а значыць, давядзецца прадзірацца праз пераказ матэматыкі ў перакладзе з рускай на ангельскую яшчэ на васьмі ўкладзеных старонках!

Чытачы, знаёмыя з функцыянальнымі мовамі і аўтаматычнай высновай тыпаў, зразумела, убачылі ў гэтай мове апісання, нават з прыкладу, значна больш знаёмага, і могуць сказаць, што гэта наогул у прынцыпе нядрэнна. Пярэчанні на гэта такія:

  • ды, мэта гучыць добра, але нажаль, яна не дасягаецца
  • адукацыя ў ВНУ Расіі вар'іруе нават сярод IT-шных спецыяльнасцяў - адпаведны курс чыталі не ўсім
  • нарэшце, як мы ўбачым, на практыцы гэта не патрабуецца, паколькі выкарыстоўваецца толькі абмежаванае падмноства нават таго TL, што быў апісаны

як сказаў LeoNerd на канале #perl у IRC-сетцы FreeNode, які спрабаваў рэалізаваць гейт з Telegram у Matrix (пераклад цытаты недакладны па памяці):

Такое пачуццё, што хтосьці ўпершыню пазнаёміўся з тэорыяй тыпаў, прыйшоў у захапленне і пачаў спрабаваць гуляць з гэтым, не асабліва клапоцячыся, ці трэба гэта на практыцы.

Глядзіце самі, калі неабходнасць bare-тыпаў (int, long і г.д.) як чагосьці элементарнага пытанняў не выклікаюць - у канчатковым рахунку іх трэба рэалізаваць уручную - для прыкладу возьмем спробу вывесці з іх вектар. Гэта значыць, насамрэч, масіў, калі называць атрыманыя рэчы сваімі імёнамі.

Але перш

Кароткае апісанне падмноства сінтаксісу TL для тых, хто не… е чытаць афіцыйную дакументацыю

constructor = Type;
myVec ids:Vector<long> = Type;

fixed#abcdef34 id:int = Type2;

fixedVec set:Vector<Type2> = FixedVec;

constructorOne#crc32 field1:int = PolymorType;
constructorTwo#2crc32 field_a:long field_b:Type3 field_c:int = PolymorType;
constructorThree#deadcrc bit_flags_of_what_really_present:# optional_field4:bit_flags_of_what_really_present.1?Type = PolymorType;

an_id#12abcd34 id:int = Type3;
a_null#6789cdef = Type3;

Пачынае вызначэнне заўсёды канструктар, пасля якога апцыянальна (на практыцы - заўсёды) праз сімвал # варта CRC32 ад нармалізаванага радка апісання дадзенага тыпу. Далей ідзе апісанне палёў, калі яны ёсць - тып можа быць і пустым. Сканчае гэта ўсё праз знак роўнасці імя тыпу, якому дадзены канструктар - гэта значыць, фактычна, падтып - належыць. Той тып, што справа ад знака роўнасці, ён паліморфны - гэта значыць яму можа адпавядаць некалькі пэўных тыпаў.

Калі ж вызначэнне сустрэлася пасля радка ---functions---, то сінтаксіс застанецца такім жа, але сэнс будзе ўжо іншы: канструктар стане імем RPC-функцыі, палі - параметрамі (ну гэта значыць ён застанецца сапраўды такой жа структурай дадзенай, як апісана ніжэй, проста такі будзе надзяляемы сэнс), а «паліморфны тып » - Тыпам вяртаецца выніку. Ён, праўда, усё роўна застанецца паліморфным - проста вызначаным у секцыі. ---types---, а гэты вось канструктар будзе "не лічыцца". Перагрузкі тыпаў выкліканых функцый па іх аргументах, г.зн. некалькі функцый з адным і тым жа імем, але рознай сігнатурай, як у C++, у TL чамусьці не прадугледжана.

Чаму "канструктар" і "паліморфны", калі гэта не ААП? Ну, насамрэч, камусьці будзе прасцей будзе думаць пра гэта менавіта ў тэрмінах ААП - паліморфны тып як абстрактны клас, а канструктары - гэта яго прамыя класы-спадчыннікі, прычым final у тэрміналогіі шэрагу моў. Насамрэч, вядома, тут толькі падобнасць з рэальнымі перагружанымі метадамі канструктараў у ГА-мовах праграмавання. Паколькі тут - усяго толькі структуры дадзеных, ніякіх метадаў няма (хоць апісанне функцый і метадаў далей цалкам здольна стварыць блытаніну ў галаве, што яны ёсць, але тое гаворка пра іншае) - то можна думаць аб канструктару як аб значэнні, з якога канструюецца тып пры чытанні патоку байт.

Як гэта адбываецца? Дэсерыялізатар, які заўсёды чытае па 4 байта, бачыць значэнне 0xcrc32 - і разумее, што далей будзе field1 з тыпам int, г.зн. чытае роўна 4 байта, на гэтым вышэйлеглае поле з тыпам PolymorType прачытана. Бачыць 0x2crc32 і разумее, што далей два палі, спачатку long, значыць чытаем 8 байт. А далей зноў складаны тып, які дэсерыялізуецца аналагічна. Напрыклад, Type3 мог быць абвешчаны ў схеме як толькі два канструктары, адпаведна, далей павінны сустрэцца альбо 0x12abcd34, пасля якога трэба прачытаць яшчэ 4 байта int, Альбо 0x6789cdef, пасля якога не будзе нічога. Што заўгодна іншае - трэба выкінуць выключэнне. У любым выпадку пасля гэтага мы вяртаемся да чытання 4 байт int поля field_c в constructorTwo і на тым заканчваем чытаць наш PolymorType.

Урэшце, калі трапіўся 0xdeadcrc для constructorThree, то ўсё становіцца складаней. Першым у нас поле bit_flags_of_what_really_present з тыпам # - на самай справе, гэта ўсяго толькі аліяс для тыпу nat, Які азначае «натуральны лік». Гэта значыць, па сутнасці, unsigned int – адзіны, дарэчы, выпадак, калі ў рэальных схемах сустракаюцца беззнакавыя лікі. Такім чынам, далей канструкцыя са знакам пытання, якая азначае, што вось гэтае поле – яно будзе прысутнічаць on the wire, толькі калі ўсталяваны які адпавядае біт у поле, на якое спаслаліся (прыкладна як тэрнарны аператар). Такім чынам, выкажам здагадку, што гэты біт стаяў, значыць, далей трэба чытаць поле тыпу Type, у якога ў нашым прыкладзе 2 канструктара. Адзін пусты (складаецца толькі з ідэнтыфікатара), у іншым ёсць поле ids з тыпам ids:Vector<long>.

Вы можаце падумаць, што як шаблоны і generic'і ​​ў плюсах ці Java. Але не. Ну, бадай. Гэта адзіны выпадак прымянення кутніх дужак у рэальных схемах, і ён выкарыстоўваецца ТОЛЬКІ для Vector. У струмені байт гэта будуць 4 байт CRC32 для самога тыпу Vector, заўсёды аднолькавыя, потым 4 байта - лік элементаў масіва, і далей самі гэтыя элементы.

Дадайце да гэтага тое, што серыялізацыя заўсёды адбываецца словамі па 4 байта, усе тыпы ёй кратныя - да ўбудаваных тыпаў апісаны яшчэ bytes и string з ручной серыялізацыяй даўжыні і гэтага выраўноўвання па 4 - ну, накшталт бы гучыць звычайна і нават параўнальна эфектыўна? Хоць TL заяўляецца як эфектыўная бінарная серыялізацыя, але хрэн ужо з імі, з пашырэннем чаго патрапіла, нават булева значэнняў і аднасімвольных радкоў да 4 байт, усё роўна JSON будзе куды тоўшчы? Вунь, нават непатрэбныя палі могуць быць прапушчаны бітавымі сцягамі, усё зусім добра, і нават якое пашыраецца на будучыню, узяў ды і дасыпаў новых апцыянальных палёў у канструктар потым?..

А вось не, калі чытаць не маё кароткае апісанне, а поўную дакументацыю, і падумаць над рэалізацыяй. Па-першае, CRC32 канструктара лічыцца па нармалізаваным радку тэкставага апісання схемы (прыбраць лішнія whitespace і г.д.) так што калі дадаецца новае поле, зменіцца радок апісання тыпу, а значыць і яе CRC32 і, такім чынам, серыялізацыя. Ды і што стары кліент рабіў бы, калі б яму прыйшло поле з новымі ўсталяванымі сцягамі, а ён не ведае, што з імі рабіць далей?

Па-другое, успомнім аб CRC32, якая прымяняецца тут па сутнасці ў якасці хэш-функцыі для ўнікальнага вызначэння, што за тып (дэ)серыялізуецца. Тут мы сутыкаемся з праблемай калізій - і не, верагоднасць не адзінка на 232, а значна больш. Хто ўспомніў аб тым, што CRC32 заменчаная на выяўленне (і выпраўленне) памылак у канале сувязі, і адпаведна паляпшае гэтыя ўласцівасці ў шкоду іншым? Напрыклад, ёй пляваць на перастаноўку байт: калі Вы палічыце CRC32 ад двух радкоў, у другой першыя 4 байта памяняеце месцамі з наступнымі 4 байтамі - яна будзе аднолькавая. Калі ў нас на ўваходзе тэкставыя радкі з лацінскага алфавіту (і крыху пунктуацыі), і імёны гэтыя не асоба выпадковыя, верагоднасць такой перастаноўкі выдатна падвышаецца.

Дарэчы, а хто правяраў, што тамака сапраўды CRC32? У адным з ранніх зыходнікаў (яшчэ да Вальтмана) была хэш-функцыя, якая памнажала кожны знак на так каханае гэтымі людзьмі лік 239, ха-ха!

Нарэшце, добра, мы зразумелі, што канструктары з тыпам поля Vector<int> и Vector<PolymorType> будуць мець розны CRC32. А што наконт уяўлення на лініі? І з пункту гледжання тэорыі, ці становіцца гэта часткай тыпу? Дапушчальны, мы перадаем масіў з дзесяці тысяч лікаў, ну з Vector<int> усё зразумела, даўжыня і яшчэ 40000 байт. А калі гэта Vector<Type2>, які складаецца толькі з аднаго поля int і ён адзін у тыпе - ці трэба нам 10000 разоў паўтараць 0xabcdef34 і затым 4 байта int, ці ж мова ў стане вывесці гэта за нас з канструктара fixedVec і замест 80000 байт перадаць зноў толькі 40000?

Гэта зусім не бяздзейнае тэарэтычнае пытанне - прадстаўце, Вы атрымліваеце спіс карыстачоў групы, кожны з якіх мае id, імя, прозвішча - розніца ў аб'ёме перадаваных дадзеных па мабільным злучэнні можа быць значнай. Менавіта эфектыўнасць серыялізацыі Telegram нам і рэкламуюць.

Такім чынам ...

Vector, які так і не змаглі вывесці

Калі Вы паспрабуеце прадзерціся праз старонкі апісання камбінатараў і каля, Вы ўбачыце, што вектар (і нават матрыцу) фармальна спрабуюць вывесці праз tuples некалькі лістоў. Але ў канчатковым выніку забіваюць, канчатковы крок прапускаецца, і проста даецца азначэнне вектара, які яшчэ і не прывязаны да тыпу. У чым тут справа? У мовах праграмавання, асабліва функцыянальных, цалкам тыпова апісаць структуру рэкурсіўна - кампілятар з яго lazy evaluation сам усё зразумее і зробіць. У мове серыялізацыі дадзеных ж неабходна ЭФЕКТЫЎНАСЦЬ: досыць проста апісаць спіс, г.зн. структуру з двух элементаў - першым элемент дадзеных, другім - саму гэтую ж структуру або пустое месца для хваста (пачка (cons) у Lisp). Але гэта, відавочна, запатрабуе для кожнага элемента дадаткова марнаваць 4 байта (CRC32 у выпадку ў TL) на апісанне яго тыпу. Лёгка можна апісаць і масіў фіксаванага памеру, але вось у выпадку масіва загадзя невядомай даўжыні - абломваемся.

Таму, паколькі TL не дазваляе вывесці вектар, яго прыйшлося дадаць узбоч. У канчатковым выніку дакументацыя паведамляе:

Serialization always uses the same constructor “vector” (const 0x1cb5c415 = crc32(«vector t:Type # [ t ] = Vector t”), што не dependent on specific value of variable type t.

Value of optional parameter is not involved in the serialization since is derived from the result type (always known prior to deserialization).

Прыгледзьцеся: vector {t:Type} # [ t ] = Vector t - але нідзе у самім гэтым азначэнні не сказанае, што першы лік павінна быць роўным даўжыні вектара! І ніадкуль гэта не варта. Гэта дадзенасць, якую трэба памятаць і рэалізоўваць рукамі. У іншых месцах дакументацыя нават сапраўды згадвае, што тып несапраўдны:

У Vector t polymorphic pseudotype з'яўляецца “type”.

… але не акцэнтуе на гэтым увагу. Калі Вы, статут прадзірацца праз нацягванне матэматыкі (можа быць нават вядомай Вам з універсітэцкага курсу), вырашаеце забіць і глядзець ужо ўласна як з гэтым працаваць на практыку, у галаве засталося ўражанне: тут Сур'ёзная Матэматыка ў аснове, прыдумлялі відавочна Крутыя Людзі (два матэматыка -прызёра ACM), а не хто патрапіла. Мэта - пусціць пыл у вочы - дасягнута.

Дарэчы, аб ліку. Нагадаем, # гэта сінонім nat, натуральнага ліку:

There are type expressions (type-expr) and numeric expressions (nat-expr). However, яны існуюць той жа спосаб.

type-expr ::= expr
nat-expr ::= expr

але ў граматыцы яны апісаны аднолькава, г.зн. гэтую розніцу зноў трэба памятаць і закладваць у рэалізацыю рукамі.

Ну і так, шаблонныя тыпы (vector<int>, vector<User>) маюць агульны ідэнтыфікатар (#1cb5c415), г.зн. калі ведаеш, што выклік аб'яўлены як

users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;

то чакаеш ужо не проста вектар, а вектар юзэраў. Дакладней, павінен чакаць - у рэальным кодзе кожны элемент, калі не bare-тып, будзе мець канструктар, і па-добраму ў імплементацыі трэба было б правяраць - а нам сапраўды ў кожным элеменце гэтага вектара даслалі таго тыпу? А калі гэта быў які-небудзь PHP, у якога ў масіве могуць ляжаць розныя тыпы ў розных элементах?

На гэтым месцы пачынаеш задумвацца – а ці патрэбен такі TL? Можа, для воза можна было б і чалавечы серыялізатар выкарыстоўваць той жа protobuf, які ўжо тады існаваў? Гэта была тэорыя, давайце паглядзім на практыку.

Існуючыя рэалізацыі TL у кодзе

TL нарадзіўся ў нетрах ВКонтакте яшчэ да вядомых падзей з продажам долі Дурава і (напэўна), яшчэ да пачатку распрацоўкі Telegram. І ў выкладзеных у апенсорс зыходніках першай рэалізацыі можна знайсці шмат вясёлых мыліц. Ды і сама мова там была рэалізавана больш поўна, чым зараз у Telegram. Напрыклад, хэшы ў схеме не выкарыстоўваюцца зусім (маецца на ўвазе ўбудаваны псеўдатып (як вектар) з дэвіянтнымі паводзінамі). Або

Templates are not used now. Instead, the same universal constructors (for example, vector {t:Type} [t] = Vector t) are used w

але разгледзім для паўнаты карціны, каб прасачыць, так бы мовіць, эвалюцыю Гіганта Думкі.

#define ZHUKOV_BYTES_HACK

#ifdef ZHUKOV_BYTES_HACK

/* dirty hack for Zhukov request */

Або вось, цудоўнае:

    static const char *reserved_words_polymorhic[] = {

      "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", NULL

      };

Гэты фрагмент - пра шаблоны, выгляду:

intHash {alpha:Type} vector<coupleInt<alpha>> = IntHash<alpha>;

Гэта вызначэнне шаблоннага тыпу хэшмэп, як вектара пар int - Type. У C++ гэта выглядала б прыкладна так:

    template <T> class IntHash {
      vector<pair<int,T>> _map;
    }

дык вось, alpha - ключавое слова! Але толькі ў C++ ты можаш пісаць T, а павінен пісаць alpha, beta… Але не Больш за 8 параметраў, на тэце фантазія скончылася. Так і ўяўляецца, што некалі ў Піцеры здарыліся прыкладна такія дыялогі:

-- Надо сделать в TL шаблоны
-- Бл... Ну пусть параметры зовут альфа, бета,... Какие там ещё буквы есть... О, тэта!
-- Грамматика? Ну потом напишем

-- Смотрите, какой я синтаксис придумал для шаблонов и вектора!
-- Ты долбанулся, как мы это парсить будем?
-- Да не ссыте, он там один в схеме, захаркодить -- и ок

Але гэта было пра першую выкладзеную рэалізацыю TL наогул . Пяройдзем да разгляду рэалізацый ва ўласна Telegram-кліентах.

Слова Васілю:

Vasily, [09.10.18 17:07] Больш за ўсё дупа распаляецца ад таго, што яны накруцілі кучу абстракцый, а потым забілі на іх ніт, і абклалі кодагегератор мыліцамі
У выніку, спачатку ад докі лётчык.jpg
Потым ад кода джэкічан.webp

Вядома, ад знаёмых з алгарытмамі і матэматыкай людзей мы можам чакаць, што яны чыталі Ахо, Ульмана, і знаёмыя са сталымі за дзесяцігоддзі стандартам дэ-факта ў галіны прыладамі для напісання кампілятараў сваіх DSL, праўда?..

Па аўтару telegram-cli з'яўляецца Віталь Вальтман, як можна зразумець па встречаемості фармату TLO за яго (cli) межамі, член каманды – зараз бібліятэка для парсінгу TL выдзелена асобна, якое складваецца ўражанне пра яе парсер TL? ..

16.12 04:18 Vasily: па-мойму, хтосьці не здужаў lex+yacc
16.12 04:18 Vasily: інакш я не магу растлумачыць гэта
16.12 04:18 Vasily: ну ці ім за колькасць радкоў у вк плацілі
16.12 04:19 Vasily: 3к+ радкоў інш<censored> замест парсера

Можа, выключэнне? Давайце паглядзім, як робіць гэта АФІЦЫЙНЫ кліент — Telegram Desktop:

    nametype = re.match(r'([a-zA-Z.0-9_]+)(#[0-9a-f]+)?([^=]*)=s*([a-zA-Z.<>0-9_]+);', line);
    if (not nametype):
      if (not re.match(r'vector#1cb5c415 {t:Type} # [ t ] = Vector t;', line)):
         print('Bad line found: ' + line);

1100+ радкоў на Пітоне, пара рэгулярак + адмысловыя выпадкі тыпу вектара, які, вядома, абвешчаны ў схеме як належыць па сінтаксісе TL, але клалі яны на гэты сінтаксіс, парсіць яго яшчэ… Пытаецца, навошта было гарадзіць усё гэтае цудияшчэ пластовае, калі ўсё роўна ніхто не збіраецца гэта парсіць па дакументацыі?!

Дарэчы… Памятаеце, мы казалі аб праверцы CRC32? Дык вось, у кодагенератары Telegram Desktop ёсць спіс выключэнняў для тых тыпаў, у якіх разлічаны CRC32 не супадае з указаным у схеме!

Vasily, [18.12 22:49] і тут бы задумацца, а ці патрэбен такі TL
калі б я хацеў падгадзіць альтэрнатыўным рэалізацыям, я б пачаў пераносы радкоў устаўляць, палова парсераў зламаецца на шматрадковых вызначэннях
tdesktop, зрэшты, таксама

Запомніце момант аб аднарадковасці, мы да яго вернемся крыху пазней.

Добра, telegram-cli – неафіцыйны, Telegram Desktop – афіцыйны, але што наконт іншых? А хто ведае?.. У кодзе Android-кліента наогул не знайшлося парсера схемы (што выклікае пытанні да апенсорснасці, але гэта для другой часткі), затое знайшлося некалькі іншых вясёлых кавалкаў кода, але пра іх у падраздзеле ніжэй.

Якія яшчэ пытанні на практыцы ўзнімае серыялізацыя? Напрыклад, навярнулі яны, вядома, з бітавымі палямі і ўмоўнымі палямі:

Vasily: flags.0? true
азначае, што поле прысутнічае і роўна true, калі сцяг выстаўлены

Vasily: flags.1? int
азначае, што поле прысутнічае, і яго трэба дэсерыялізаваць

Vasily: Дупа, не гары, што ты робіш!
Vasily: Там недзе ў доку ёсць згадка, што true - гэта голы тып нулявой даўжыні, але з іх докі нешта сабраць нерэальна
Vasily: У сорцах адкрытых рэалізацый гэтага таксама няма, затое ёсць куча мыліц і падпорак

А, дапусцім, Telethon? Забягаючы наперад па тэме MTProto, прыклад – у дакументацыі ёсць вось такія кавалкі, але знак % у ёй апісаны толькі як "адпаведны дадзенаму bare-тып", г.зн. у прыкладах ніжэй ці памылка, ці нешта недакументаванае:

Vasily, [22.06.18 18:38] У адным месцы:

msg_container#73f1f8dc messages:vector message = MessageContainer;

У іншым:

msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;

І гэта дзве вялікія розніцы, у рэале прыходзіць нейкі голы вектар

Я не бачыў bare вызначэння вектара і не сустракаў яго

У telethon рукамі напісаны разбор

У яго схеме закаментавана вызначэнне msg_container

Ізноў жа, застаецца пытанне пра %. Яно не апісана.

Vadim Goncharov, [22.06.18 19:22] а ў tdesktop?

Vasily, [22.06.18 19:23] Але іх парсер TL на регуляиках гэта таксама хутчэй за ўсё не з'есць

// parsed manually

TL прыгожая абстракцыя, ніхто яго не рэалізуе цалкам

А% у іх версіі схемы няма

Але тут дакументацыя супярэчыць сама сабе, так што хз

Яно сустракалася ў граматыцы, яны маглі проста забыцца апісаць семантыку.

Ты ж бачыў доку на TL, там без паўлітры не разбярэшся

"Ну дапусцім", скажа іншы чытач, "нешта вы ўсё крытыкуеце, так пакажыце, як трэба".

Васіль адказвае: «а што тычыцца парсера, мне штукі віду

    args: /* empty */ { $$ = NULL; }
        | args arg { $$ = g_list_append( $1, $2 ); }
        ;

    arg: LC_ID ':' type-term { $$ = tl_arg_new( $1, $3 ); }
            | LC_ID ':' condition '?' type-term { $$ = tl_arg_new_cond( $1, $5, $3 ); free($3); }
            | UC_ID ':' type-term { $$ = tl_arg_new( $1, $3 ); }
            | type-term { $$ = tl_arg_new( "", $1 ); }
            | '[' LC_ID ']' { $$ = tl_arg_new_mult( "", tl_type_new( $2, TYPE_MOD_NONE ) ); }
            ;

неяк больш падабаюцца, чым

struct tree *parse_args4 (void) {
  PARSE_INIT (type_args4);
  struct parse so = save_parse ();
  PARSE_TRY (parse_optional_arg_def);
  if (S) {
    tree_add_child (T, S);
  } else {
    load_parse (so);
  }
  if (LEX_CHAR ('!')) {
    PARSE_ADD (type_exclam);
    EXPECT ("!");
  }
  PARSE_TRY_PES (parse_type_term);
  PARSE_OK;
}

або

        # Regex to match the whole line
        match = re.match(r'''
            ^                  # We want to match from the beginning to the end
            ([w.]+)           # The .tl object can contain alpha_name or namespace.alpha_name
            (?:
                #             # After the name, comes the ID of the object
                ([0-9a-f]+)    # The constructor ID is in hexadecimal form
            )?                 # If no constructor ID was given, CRC32 the 'tl' to determine it

            (?:s              # After that, we want to match its arguments (name:type)
                {?             # For handling the start of the '{X:Type}' case
                w+            # The argument name will always be an alpha-only name
                :              # Then comes the separator between name:type
                [wd<>#.?!]+  # The type is slightly more complex, since it's alphanumeric and it can
                               # also have Vector<type>, flags:# and flags.0?default, plus :!X as type
                }?             # For handling the end of the '{X:Type}' case
            )*                 # Match 0 or more arguments
            s                 # Leave a space between the arguments and the equal
            =
            s                 # Leave another space between the equal and the result
            ([wd<>#.?]+)     # The result can again be as complex as any argument type
            ;$                 # Finally, the line should always end with ;
            ''', tl, re.IGNORECASE | re.VERBOSE)

гэта ЎВЕСЬ лексер:

    ---functions---         return FUNCTIONS;
    ---types---             return TYPES;
    [a-z][a-zA-Z0-9_]*      yylval.string = strdup(yytext); return LC_ID;
    [A-Z][a-zA-Z0-9_]*      yylval.string = strdup(yytext); return UC_ID;
    [0-9]+                  yylval.number = atoi(yytext); return NUM;
    #[0-9a-fA-F]{1,8}       yylval.number = strtol(yytext+1, NULL, 16); return ID_HASH;

    n                      /* skip new line */
    [ t]+                  /* skip spaces */
    //.*$                 /* skip comments */
    /*.**/              /* skip comments */
    .                       return (int)yytext[0];

г.зн. прасцей - гэта мякка сказана ».

Увогуле, у выніку парсер і кодагенератар для рэальна выкарыстоўванага падмноства TL уклаўся ў прыкладна 100 радкоў граматыкі і ~300 радкоў генератара (лічачы і ўсё print'ы генераванага кода), уключаючы плюшкі тыпу інфармацыю аб тыпах для інтраспекцыі ў кожным класе. Кожны паліморфны тып ператвараецца ў пусты абстрактны базавы клас, а канструктары - ўспадкоўваюцца ад яго і маюць метады для серыялізацыі і дэсерыялізацыі.

Недахоп тыпаў у мове тыпаў

Строгая тыпізацыя - гэта ж добра, праўда? Не, гэта не халівар (хоць я аддаю перавагу дынамічныя мовы), а пастулат у рамках TL. Зыходзячы з яго, мова павінна забяспечваць усялякія праверкі за нас. Ну окей, хай не ён сам, а рэалізацыя, але ён павінен іх хаця б апісваць. І якія ж магчымасці мы хочам?

Першым чынам, constraints. Вось мы бачым у дакументацыі па запампоўцы файлаў:

File's binary content is the split into parts. All parts must have the same size ( part_size ) and the following conditions павінны быць:

  • part_size % 1024 = 0 (divisible by 1KB)
  • 524288 % part_size = 0 (512KB must be evenly divisible by part_size)

Апошнія часы не маюць ніякага значэння ў гэтых умовах, ажыццёўлены яе памеры, і не так, як part_size.

Кожная частка павінна быць пасля лічбы, file_part, with a value ranging з 0 to 2,999.

Після файла будзе партызан, які вы хочаце, каб знайсці спосаб для ажыццяўлення яго на сервере. Use upload.saveBigFilePart у выпадку ўсёй колькасці файлаў больш за 10 MB and upload.saveFilePart for smaller files.
[…] Адзін з тых, што адпавядаюць data ўводу falls могуць быць адабраны:

  • FILE_PARTS_INVALID - Invalid number of parts. The value is not between 1..3000

Што-небудзь з гэтага прысутнічае ў схеме? Гэта неяк выразна сродкамі TL? Не. Але дазвольце, бо нават дзедаўскі Turbo Pascal умеў апісваць тыпы, якія задаюцца. дыяпазонамі. І яшчэ адну рэч умеў, цяпер больш вядомую як enum - Тып, які складаецца з пераліку фіксаванай (невялікай) колькасці значэнняў. У мовах тыпу Сі - лікавых, заўважце, мы пакуль казалі толькі пра тыпы лічбы. А бо ёсць яшчэ масівы, радкі… напрыклад, нядрэнна было б апісаць, што вось гэты радок можа змяшчаць толькі нумар тэлефона, так?

Нічога з гэтага ў TL не. Затое ёсць, напрыклад, у JSON Schema. І калі пра дзялімасць 512 Кб хтосьці яшчэ можа запярэчыць, што такое ўсё роўна трэба правяраць у кодзе, тое зрабіць так, каб кліент папросту не мог паслаць нумар па-за дыяпазонам 1..3000 (і адпаведнай памылкі не магло ўзнікнуць) ужо можна было б, так?..

Дарэчы, аб памылках і якія вяртаюцца значэннях. Вока замыльваецца нават у тых, хто папрацаваў з TL – да нас не адразу дайшло, што кожная функцыя ў TL на самой справе можа вярнуць не толькі апісаны тып вяртання, але і памылку. Але гэта сродкамі самога TL не выводзіцца ніяк. Вядома, яно і так зразумела і нафіг не трэба на практыцы (хоць насамрэч, RPC можна рабіць па-рознаму, мы яшчэ вернемся да гэтага) – але як жа Чысціня канцэпцый Матэматыкі Абстрактных Тыпаў са свету горняга?.. Узяўся за гуж – так адпавядай ужо.

І ўрэшце, што наконт чытэльнасці? Ну, тамака, наогул жадалася бы апісанне мець прама ў схеме (у JSON-схеме ізноў жа ёсць), але калі ўжо з ім натуга, то як наконт практычнага боку - хоць бы банальна глядзець дыффы пры абнаўленнях? Глядзіце самі на рэальных прыкладах:

-channelFull#76af5481 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int = ChatFull;
+channelFull#1c87a71a flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_view_stats:flags.12?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int = ChatFull;

або

-message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message;
+message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message;

У каго як, але GitHub, напрыклад, змены ўнутры такіх доўгіх радкоў падсвятляць адмаўляецца. Гульня "знайдзі 10 адрозненняў", прычым што мозг адразу бачыць, гэта што пачала і канцы ў абодвух прыкладах аднолькавыя, трэба нудна ўчытвацца дзесьці ў сярэдзіне… На мой погляд, вось гэта вось не тое што ў тэорыі, а чыста візуальна выглядае брудна і неахайна.

Дарэчы, аб чысціні тэорыі. А навошта патрэбны бітавыя палі? Ці не здаецца, што яны пахнуць нядобра з пункту гледжання тэорыі тыпаў? Тлумачэнне можна ўбачыць у ранніх версіях схемы. Спачатку так, так і было, на кожных чых ствараўся новы тып. Гэтыя рудыменты і зараз ёсць вось у такім выглядзе напрыклад:

storage.fileUnknown#aa963b05 = storage.FileType;
storage.filePartial#40bc6f52 = storage.FileType;
storage.fileJpeg#7efe0e = storage.FileType;
storage.fileGif#cae1aadf = storage.FileType;
storage.filePng#a4f63c0 = storage.FileType;
storage.filePdf#ae1e508d = storage.FileType;
storage.fileMp3#528a0677 = storage.FileType;
storage.fileMov#4b09ebbc = storage.FileType;
storage.fileMp4#b3cea0e4 = storage.FileType;
storage.fileWebp#1081464c = storage.FileType;

Але зараз прадстаўце, калі ў Вас у структуры 5 апцыянальных палёў, то Вам спатрэбіцца 32 тыпу для ўсіх магчымых варыянтаў. Камбінаторны выбух. Так крыштальная чысціня тэорыі TL у чарговы раз разбілася аб чыгунную азадак суровай рэальнасці серыялізацыі.

Акрамя таго, месцамі гэтыя хлопцы самі парушаюць сваю ж тыпізацыю. Напрыклад, у MTProto (наступная частка) адказ можа быць паціснуты Gzip, усё разумна – акрамя таго, што парушэнне пластоў і схемы. Раз, і паціснулі не сам RpcResult, а яго змесціва. Ну вось навошта так рабіць?.. Прыйшлося ўпілоўваць мыліцу, каб сціск працаваў дзе заўгодна.

Або яшчэ прыклад, мы аднойчы выявілі памылку - пасылаўся InputPeerUser замест InputUser. Ці наадварот. Але гэта працавала! Гэта значыць серверу было пофіг на тып. Як такое можа быць? Адказ, магчыма, падкажуць нам фрагменты кода з telegram-cli:

  if (tgl_get_peer_type (E->id) != TGL_PEER_CHANNEL || (C && (C->flags & TGLCHF_MEGAGROUP))) {
    out_int (CODE_messages_get_history);
    out_peer_id (TLS, E->id);
  } else {    
    out_int (CODE_channels_get_important_history);

    out_int (CODE_input_channel);
    out_int (tgl_get_peer_id (E->id));
    out_long (E->id.access_hash);
  }
  out_int (E->max_id);
  out_int (E->offset);
  out_int (E->limit);
  out_int (0);
  out_int (0);

Іншымі словамі, тут серыялізацыя робіцца ВРУЧНУ, а не згенераваным кодам! Можа быць, сервер рэалізаваны аналагічна?.. У прынцыпе, такое спатрэбіцца, калі зрабіць адзін раз, але як гэта потым падтрымліваць пры абнаўленнях? Ці не за гэтым ці схема была прыдумана? І тут мы пераходзім да наступнага пытання.

Версіённасць. Пласты (layers)

Чаму версіі схемы названы пластамі, можна рабіць толькі здагадкі, зыходзячы з гісторыі апублікаваных схем. Па ўсёй бачнасці, спачатку аўтарам падалося, што базавыя рэчы можна рабіць на нязменнай схеме, і толькі там, дзе трэба, на канкрэтныя запыты ўказваць, што яны робяцца па іншай версіі. У прынцыпе, нават нядрэнная ідэя - і новае будзе як бы "падмешвацца", напластоўвацца на старое. Але паглядзім, як гэта было зроблена. Праўда, паглядзець з самага пачатку не атрымалася - пацешна, але схемы базавага пласта проста не існуе. Пласты пачаліся з 2. Дакументацыя распавядае нам пра спецыяльную фічу TL:

If a client supports Layer 2, then the following constructor мусяць быць выкарыстаны:

invokeWithLayer2#289dd1f6 {X:Type} query:!X = X;

У практыцы, гэтыя спосабы, што перад усімі API Call, не з'яўляецца з значэннем 0x289dd1f6 be added before the method number.

Гучыць нармальна. Але што было далей? Далей з'явіўся

invokeWithLayer3#b7475268 query:!X = X;

А далей? Як няцяжка здагадацца,

invokeWithLayer4#dea0d430 query:!X = X;

Смешна? Не, яшчэ рана смяяцца, падумайце над тым, што кожны запыт з іншага пласта трэба абгортваць у такі спецыяльны тып - калі яны ў Вас усё розныя, як іх інакш адрозніваць-то? І даданне ўсяго толькі 4 байт перад - даволі эфектыўны метад. So,

invokeWithLayer5#417a57ae query:!X = X;

Але відавочна, што праз некаторы час гэта стане некаторай вакханаліяй. І прыйшло рашэнне:

Update: Starting with Layer 9, helper methods invokeWithLayerN can be used only together with initConnection

Ура! Праз 9 версій мы прыйшлі, нарэшце, да таго, што ў Internet-пратаколах рабілася яшчэ ў 80-е - узгадненню версіі адзін раз у пачатку злучэння!

А далей?..

invokeWithLayer10#39620c41 query:!X = X;
...
invokeWithLayer18#1c900537 query:!X = X;

А вось зараз ткі можна смяяцца. Толькі яшчэ праз 9 пластоў быў, нарэшце, дададзены ўніверсальны канструктар з нумарам версіі, які трэба выклікаць толькі адзін раз у пачатку злучэння, і сэнс у пластах быццам бы знік, зараз гэта проста ўмоўная версія, як і ўсюды. Праблема вырашана.

Дакладна?..

Vasily, [16.07.18 14:01] Яшчэ ў пятніцу падумалася:
Падзеі тэлесервер дасылае без запыту. Запыты трэба заварочваць у InvokeWithLayer. Апдэйты сервер не заварочвае, няма структуры для абгортвання адказаў і апдэйтаў.

Г.зн. кліент не можа паказаць пласт, у якім ён хоча апдэйты

Vadim Goncharov, [16.07.18 14:02] а InvokeWithLayer хіба не мыліца ў прынцыпе?

Vasily, [16.07.18 14:02] Гэта адзіны спосаб

Vadim Goncharov, [16.07.18 14:02] які па сутнасці павінен значыць узгадненне лэйера ў пачатку сесіі

дарэчы, з гэтага вынікае, што даунгрэйд кліента не прадугледжаны

Апдэйты, г.зн. тып Updates у схеме - гэта тое, што сервер дасылае кліенту не ў адказ на API-запыт, а самастойна па ўзнікненні падзеі. Гэта складаная тэма, якая будзе разгледжана ў іншым пасце, зараз жа важна ведаць, што сервер копіць Updates і падчас афлайну кліента.

Такім чынам, пры адмове ад абгортвання кожнага пакета ва ўказанне яму версіі, адгэтуль лагічна ўзнікаюць наступныя магчымыя праблемы:

  • сервер пасылае кліенту апдэйты яшчэ да таго, як той паведаміў, якая ім падтрымліваецца версія
  • што трэба рабіць пасля апгрэйду кліента?
  • хто гарантуе, Што меркаванне сервера аб нумары пласта не памяняецца ў працэсе?

Думаеце, гэта асабліва тэарэтычныя разуменні, і на практыцы такога не можа паўстаць, бо сервер напісаны карэктна (ва ўсякім разе, тэстуецца добра)? Ха! Як бы не так!

Менавіта на гэта мы ў жніўні і напароліся. 14 жніўня мільгалі паведамленні, што на серверах Telegram нешта абнаўляюцца… а далей у логах:

2019-08-15 09:28:35.880640 MSK warn  main: ANON:87: unknown object type: 0x80d182d1 at TL/Object.pm line 213.
2019-08-15 09:28:35.751899 MSK warn  main: ANON:87: unknown object type: 0xb5223b0f at TL/Object.pm line 213.

і далей некалькі мегабайт стэктрэйсаў (ну, заадно і лагіраванне пафіксілі). Бо калі ў Вас у TL нешта не распазналася - ён жа бінарны па сігнатурах, далей у струмені УСЕ паедзе, дэкадаванне стане немагчымым. Што ўвогуле ў такой сітуацыі рабіць?

Ну, першае што любому ў галаву прыходзіць - адлучыцца і паспрабаваць нанова. Не дапамагло. Гуглі па CRC32 – гэта апынуліся аб'екты з 73 схемы, хоць мы працавалі на 82. Уважліва глядзім у логі – там ідэнтыфікатары з двух розных схем!

Можа, праблема асабліва ў нашым неафіцыйным кліенце? Не, запускаем Telegram Desktop 1.2.17 (версія, якая пастаўляецца ў шэрагу дыстрыбутываў Linux), ён піша ў лог Exception: MTP

Крытыка пратакола і аргпадыходаў Telegram. Частка 1, тэхнічная: вопыт напісання кліента з нуля - TL, MT

Гугль паказаў, што падобная праблема ў кагосьці з неафіцыйных кліентаў ужо здаралася, але тады нумары версій і адпаведна здагадкі былі іншыя…

Дык што ж рабіць-то? Мы з Васілём падзяліліся: ён паспрабаваў абнавіць схему да 91, я вырашыў пачакаць некалькі дзён і паспрабаваць на 73. Абодва спосабу спрацавалі, але паколькі яны эмпірычныя, няма ніякага разумення, ні на колькі версій уверх ці ўніз трэба скакаць, ні колькі часу трэба чакаць .

Пазней у мяне атрымалася прайграць сітуацыю: запускаем кліент, адключаем, перакампілюем схему на іншы пласт, перазапускаем, зноў ловім праблему, вяртаемся на папярэдні - опа, ужо ніякія пераключэнні схемы і перазапускі кліента на працягу некалькіх хвілін не дапамогуць. Вам будзе прыходзіць мікс са структур дадзеных з розных пластоў.

Тлумачэнне? Як можна здагадацца па розных ускосных сімптомах, сервер складаецца з шматлікіх працэсаў розных тыпаў на розных машынах. Хутчэй за ўсё, той з сервераў, што адказвае за "буферызацыю", паклаў у чаргу тое, што яму аддавалі вышэйстаячыя, а яны аддавалі ў той схеме, якая была на момант генерацыі. І пакуль гэтая чарга не "пратухла", нічога з гэтым зрабіць было нельга.

Няўжо што… але ж гэта жудасны мыліца?!.. Не, перш чым думаць аб вар'яцкіх ідэях, давайце паглядзім у код афіцыйных кліентаў. У версіі для Android мы не знаходзім ніякага TL-парсера, але знаходзім здаравенны файл (гітхаб адмаўляецца яго падфарбоўваць) з (дэ)серыялізацыяй. Вось фрагменты кода:

public static class TL_message_layer68 extends TL_message {
    public static int constructor = 0xc09be45f;
//...
//еще пачка подобных
//...
    public static class TL_message_layer47 extends TL_message {
        public static int constructor = 0xc992e15c;
        public static Message TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) {
            Message result = null;
            switch (constructor) {
                case 0x1d86f70e:
                    result = new TL_messageService_old2();
                    break;
                case 0xa7ab1991:
                    result = new TL_message_old3();
                    break;
                case 0xc3060325:
                    result = new TL_message_old4();
                    break;
                case 0x555555fa:
                    result = new TL_message_secret();
                    break;
                case 0x555555f9:
                    result = new TL_message_secret_layer72();
                    break;
                case 0x90dddc11:
                    result = new TL_message_layer72();
                    break;
                case 0xc09be45f:
                    result = new TL_message_layer68();
                    break;
                case 0xc992e15c:
                    result = new TL_message_layer47();
                    break;
                case 0x5ba66c13:
                    result = new TL_message_old7();
                    break;
                case 0xc06b9607:
                    result = new TL_messageService_layer48();
                    break;
                case 0x83e5de54:
                    result = new TL_messageEmpty();
                    break;
                case 0x2bebfa86:
                    result = new TL_message_old6();
                    break;
                case 0x44f9b43d:
                    result = new TL_message_layer104();
                    break;
                case 0x1c9b1027:
                    result = new TL_message_layer104_2();
                    break;
                case 0xa367e716:
                    result = new TL_messageForwarded_old2(); //custom
                    break;
                case 0x5f46804:
                    result = new TL_messageForwarded_old(); //custom
                    break;
                case 0x567699b3:
                    result = new TL_message_old2(); //custom
                    break;
                case 0x9f8d60bb:
                    result = new TL_messageService_old(); //custom
                    break;
                case 0x22eb6aba:
                    result = new TL_message_old(); //custom
                    break;
                case 0x555555F8:
                    result = new TL_message_secret_old(); //custom
                    break;
                case 0x9789dac4:
                    result = new TL_message_layer104_3();
                    break;

або

    boolean fixCaption = !TextUtils.isEmpty(message) &&
    (media instanceof TLRPC.TL_messageMediaPhoto_old ||
     media instanceof TLRPC.TL_messageMediaPhoto_layer68 ||
     media instanceof TLRPC.TL_messageMediaPhoto_layer74 ||
     media instanceof TLRPC.TL_messageMediaDocument_old ||
     media instanceof TLRPC.TL_messageMediaDocument_layer68 ||
     media instanceof TLRPC.TL_messageMediaDocument_layer74)
    && message.startsWith("-1");

Кхм… выглядае дзіка. Але, мусіць, гэта згенераваны код, тады добра?.. Затое сапраўды ўсе версіі падтрымлівае! Праўда, незразумела, чаму ўсё намяшана ў адну кучу, і сакрэтныя чаты, і ўсякія _old7 неяк не падобныя на машынную генерацыю… Зрэшты, больш за ўсё я афігеў ад

TL_message_layer104
TL_message_layer104_2
TL_message_layer104_3

Хлопцы, вы там што, нават усярэдзіне аднаго пласта вызначыцца не можаце?! Ну, добра, два, дапусцім, рэлізнуліся з памылкай, ну бывае, але ТРЫ?.. Адразу ж яшчэ раз на тыя ж граблі? Што гэта за парнаграфія, пардон?..

У зыходніках Telegram Desktop, дарэчы, здараецца аналагічнае – раз так, і некалькі комітаў запар у схему не мяняюць яе нумары пласта, а нешта фіксяць. Ва ўмовах, калі афіцыйнай крыніцы дадзеных па схеме няма, адкуль яе браць, акрамя зыходнікаў афіцыйнага кліента? А возьмеш адтуль, не можаш быць упэўнены, што схема цалкам правільная, пакуль не пратэстуеш усе метады.

А як такое наогул можна тэсціраваць? Спадзяюся, аматары юніт-, функцыянальных і іншых тэстаў падзеляцца ў каментарах.

Добра, разгледзім яшчэ фрагмент кода:

public static class TL_folders_deleteFolder extends TLObject {
    public static int constructor = 0x1c295881;

    public int folder_id;

    public TLObject deserializeResponse(AbstractSerializedData stream, int constructor, boolean exception) {
        return Updates.TLdeserialize(stream, constructor, exception);
    }

    public void serializeToStream(AbstractSerializedData stream) {
        stream.writeInt32(constructor);
        stream.writeInt32(folder_id);
    }
}

//manually created

//RichText start
public static abstract class RichText extends TLObject {
    public String url;
    public long webpage_id;
    public String email;
    public ArrayList<RichText> texts = new ArrayList<>();
    public RichText parentRichText;

    public static RichText TLdeserialize(AbstractSerializedData stream, int constructor, boolean exception) {
        RichText result = null;
        switch (constructor) {
            case 0x1ccb966a:
                result = new TL_textPhone();
                break;
            case 0xc7fb5e01:
                result = new TL_textSuperscript();
                break;

Вось гэты каментар "manually created" наводзіць на думку, што толькі частка гэтага файла напісана ўручную (уяўляеце ўвесь кашмар у частцы maintenance?), а астатняе ткі згенеравана машынай. Аднак, тады ўзнікае іншае пытанне - аб тым, што зыходнікі даступныя. не цалкам (а-ля блобы пад GPL у ядры Linux), аднак гэта ўжо тэма для другой часткі.

Але дастаткова. Пяройдзем да пратакола, па-над якім уся гэтая серыялізацыя ганяецца.

MT Proto

Такім чынам, адкрываем агульнае апісанне и дэталёвае апісанне пратакола і перш за ўсё спатыкаемся аб тэрміналогію. І з багаццем усяго. Наогул, гэта падобна фірмовая фішка Telegram – зваць рэчы ў розных месцах па-рознаму, альбо розныя рэчы адным словам, альбо наадварот (напрыклад, у высокаўзроўневым API калі ўбачыце sticker pack – гэта не тое, што Вы падумалі).

Напрыклад, "паведамленне" (message) і "сесія" (session) – тут значаць іншае, чым у звыклым інтэрфейсе Telegram-кліента. Ну, з паведамленнем усё зразумела, яго можна было б тлумачыць у тэрмінах ААП, ці ж проста зваць словам "пакет" - гэта нізкі, транспартны ўзровень, тут не тыя паведамленні, што ў інтэрфейсе, шмат службовых. А вось сесія… але пра ўсё па парадку.

Транспартны ўзровень

Перш за ўсё - транспарт. Нам раскажуць аж пра 5 варыянтаў:

  • TCP
  • Вэбсакет
  • Websocket over HTTPS
  • HTTP
  • HTTPS

Vasily, [15.06.18 15:04] А яшчэ ёсць UDP транспарт, але ён не дакументаваны

А TCP у трох варыянтах

Першы падобны на UDP па-над TCP, кожны пакет уключае ў сябе sequence number і crc
Чаму чытаць докі на каляску так балюча?

Ну, зараз там TCP ужо ў 4 варыянтах:

  • скарочаны
  • Прамежкавы
  • Padded intermediate
  • Поўны

Ну добра, Padded intermediate для MTProxy, гэта пазней дадалі з прычыны вядомых падзей. А вось навошта яшчэ дзве версіі (разам тры), калі можна было б абысціся адной? Усе чатыры ў сутнасці адрозніваюцца толькі тым, якім чынам задаць даўжыню і payload уласна таго асноўнага MTProto, аб якім прамова пайдзе далей:

  • у Abridged гэта 1 ці 4 байта, але не 0xef, затым цела
  • у Intermediate гэта 4 байта даўжыні і поле, прычым першы раз кліент павінен паслаць 0xeeeeeeee для ўказання, што гэта Intermediate
  • у Full самая наркаманія, з пункту гледжання сецявіка: даўжыня, sequence number, прычым НЕ ТОЙ, што ў асноўным MTProto, цела, CRC32. Так, усё гэта па-над TCP. Які дае нам надзейны транспарт у выглядзе паслядоўнага патоку байт, ніякія паслядоўнасці не патрэбныя, тым больш кантрольныя сумы. Окей, мне зараз запярэчаць, што ў TCP 16-бітная кантрольная сума, так што скажэнне дадзеных здараецца. Выдатна, толькі ў нас наогул крыптаграфічны пратакол з хэшамі даўжэй 16 байт, усе гэтыя памылкі – і нават больш – будуць адлоўленыя на несупадзенні SHA узроўнем вышэй. Ніякага сэнсу ў CRC32 па-над гэтым – НЕ.

Параўнальны Abridged, у якім магчымы адзін байт даўжыні, з Intermediate, які абгрунтоўваецца "In case 4-byte data alignment is needed", што даволі-такі глупства. Што, лічыцца, што праграмісты Telegram настолькі няўмехі, што не могуць прачытаць дадзеныя з сокета ў выраўнаваны буфер? Гэта ўсё роўна прыйдзецца рабіць, таму што чытанне можа вярнуць Вам абы-што байт (а яшчэ бываюць напрыклад проксі-сервера…). Або з іншага боку, навошта гарадзіць Abridged, калі зверху ў нас усё роўна будуць здаравенныя padding'і ад 16 байт - зэканоміць 3 байта часам ?

Складваецца ўражанне, што Мікалай Дураў вельмі кахае вынаходзіць ровары, у тым ліку сеткавыя пратаколы, без рэальнай практычнай патрэбы.

Астатнія варыянты транспарта, у т.л. Web і MTProxy, мы зараз разглядаць не будзем, можа быць, у іншым пасце, калі будзе запыт. Пра гэты самы MTProxy успомнім зараз толькі, што неўзабаве пасля яго выпуску ў 2018, правайдэры хуценька навучыліся блакаваць менавіта яго, прызначанага для абыходу блакіровак, Па памеры пакета! А таксама той факт, што напісаны (ізноў жа Вальтманам) сервер MTProxy на Сі быў залішне завязаны на лінуксавую спецыфіку, хоць гэта зусім не патрабавалася (Філ Кулін пацвердзіць), і што аналагічны сервер ці то на Go, ці то на Node.js змясціўся менш чым у сотню радкоў.

Але рабіць высновы аб тэхнічнай пісьменнасці гэтых людзей рабіць будзем у канцы раздзела, пасля разгляду іншых пытанняў. Пакуль пяройдзем да 5-га ўзроўню OSI, сесійнага – на які яны змясцілі MTProto session.

Ключы, паведамленні, сесіі, Diffie-Hellman

Змясцілі яны яго туды не зусім карэктна… Сесія – гэта не тая сесія, што бачная ў інтэрфейсе пад Active sessions. Але па парадку.

Крытыка пратакола і аргпадыходаў Telegram. Частка 1, тэхнічная: вопыт напісання кліента з нуля - TL, MT

Вось мы атрымалі з транспартнага ўзроўню радок байт вядомай даўжыні. Гэта альбо шыфраванае паведамленне, альбо plaintext – калі мы яшчэ на стадыі ўзгаднення ключа і ўласна ім і займаемся. Аб якім з кучы паняццяў пад назвай «ключ» ідзе гаворка? Растлумачым гэтае пытанне за саму каманду Telegram (прыношу выбачэнні за пераклад з ангельскай уласнай дакументацыі да лібы стомленым мозгам у 4 раніцы, некаторыя фразы было прасцей пакінуць як ёсць):

Ёсць дзве сутнасці пад назвай Сесія - адна ў UI афіцыйных кліентаў пад "current sessions", дзе кожнай сесіі адпавядае цэлае прылада / OS.
Другая - MTProto session, у якой ёсць sequence number паведамлення (у нізкаўзроўневым сэнсе) у ёй, і якая можа доўжыцца паміж рознымі TCP-злучэннямі. Адначасова могуць быць устаноўлены некалькі MTProto-сесій, напрыклад для паскарэння запампоўкі файлаў.

Паміж гэтымі двума сесій знаходзіцца паняцце дазвол. У выраджаным выпадку, можна сказаць, што UI-сесія ёсць тое ж, што дазвол, Але нажаль, усё складана. Глядзім:

  • Карыстальнік на новай прыладзе спачатку генеруе auth_key і bounds it to account, напрыклад па SMS - таму і дазвол
  • Адбылося гэта ўнутры першай MTProto session, якая мае session_id ўнутры сябе.
  • На дадзеным кроку, камбінацыя дазвол и session_id магла быць названа прыклад - Гэтае слова сустракаецца ў дакументацыі і кодзе некаторых кліентаў
  • Затым, кліент можа адкрыць некалькі MTProto sessions пад адным і тым жа auth_key - Да аднаго і таго ж DC.
  • Затым, аднойчы кліенту спатрэбіцца запытаць файл у іншага DC - і для гэтага DC будзе згенераваны новы auth_key !
  • Каб паведаміць сістэме, што гэта не новы юзэр рэгіструецца, а тая ж самая дазвол (UI-сесія), кліент выкарыстоўвае выклікі API auth.exportAuthorization у хатнім DC auth.importAuthorization у новым DC.
  • Усё гэтак жа, можа быць адкрыта некалькі MTProto sessions (кожная з уласным session_id) да гэтага новага DC, пад яго auth_key.
  • Нарэшце, кліент можа захацець Perfect Forward Secrecy. Кожны auth_key быў пастаянны key - per DC - і кліент можа выклікаць auth.bindTempAuthKey для выкарыстання часовы auth_key - і зноў, толькі адзін temp_auth_key per DC, агульны для ўсіх MTProto sessions да гэтага DC.

Заўважым, што соль (і future salts) таксама адна на auth_key г.зн. shared паміж усімі MTProto sessions да аднаго і таго ж DC.

Што значыць "паміж рознымі TCP-злучэннямі"? Значыць, што гэта нешта накшталт аўтарызацыі кукой на вэб-сайце - яна захоўваецца (перажывае) шмат TCP-злучэнняў да дадзенага сервера, але аднойчы пратухне. Толькі ў адрозненне ад HTTP, у MTProto усярэдзіне сесій паведамлення паслядоўна нумаруюцца і пацвярджаюцца, заехалі ў тунэль, разарвалася злучэнне — пасля ўсталявання новага злучэння сервер ласкава адправіць усё тое ў гэтай сесіі, што не даставіў у мінулым TCP-злучэнні.

Аднак, інфармацыя вышэй прыведзена выцісканнем пасля доўгіх месяцаў разбораў. А пакуль што - мы ж рэалізуем свой кліент з нуля? - вернемся да пачатку.

Так што, генеруем auth_key па версіі Дыфі-Хеллмана ад Telegram. Паспрабуем зразумець дакументацыю...

Vasily, [19.06.18 20:05] data_with_hash := SHA1(data) + data + (any random bytes); such that the length equal 255 bytes;
encrypted_data := RSA (data_with_hash, server_public_key); a 255-byte long number (big endian) is raised to requisite power over the requisite modulus, and the result is stored as 256-byte number.

У іх нейкі наркаманскі DH

Не падобна на DH здаровага чалавека
У дх няма двух публічных ключоў

Ну з гэтым у выніку разабраліся але асадак застаўся – робіцца proof of work кліентам, што ён змог фактарызаваць лік. Тыпу абарона ад DoS-нападаў. І RSA-ключ выкарыстоўваецца толькі адзін раз у адным кірунку, па сутнасці, для шыфравання new_nonce. Але пакуль гэтая быццам бы простая аперацыя атрымаецца, з чым давядзецца сутыкнуцца?

Vasily, [20.06.18 00:26] Я яшчэ не дайшоў да запыту appid

Гэта я запыт на DH адправіў

А, у доку на транспарт напісана, што можа адказаць 4 байтамі кода памылкі. І ўсё

Ну вось сказаў ён мне -404, і што?

Вось я яму: «лаві сваю ефігню шыфраваную ключом сервера з адбіткам вось такім, жадаю DH», а яно ў адказ тупа 404

Што б Вы падумалі на такі адказ сэрвера? Што рабіць? Спытаць няма ў каго (але пра гэта ў другой частцы).

Тут усю цікавасць па доку зрабіць

Мне вось больш заняцца няма чым, толькі і марыў лікі туды-сюды канвертаваць

Два 32 бітныя лікі. Я іх і спакаваў як усе астатнія

Але не, менавіта гэтыя два трэба спачатку ў радок як BE

Vadim Goncharov, [20.06.18 15:49] і з-за гэтага 404?

Vasily, [20.06.18 15:49] ТАК!

Vadim Goncharov, [20.06.18 15:50] вось я і не разумею, што ён можа «не знайшла»

Vasily, [20.06.18 15:50] прыкладна

Не знайшла такога раскладання на простыя дзельнікі %)

Нават error reporting не здужалі

Vasily, [20.06.18 20:18] О, там яшчэ і MD5. Ужо тры розныя хэшы

У key fingerprint is computed as follows:

digest = md5(key + iv)
fingerprint = substr(digest, 0, 4) XOR substr(digest, 4, 4)

SHA1 і sha2

Такім чынам, напрыклад, auth_key памерам 2048 біт мы па Дыфі-Хеллману атрымалі. Што далей? Далей мы выяўляем, што малодшыя 1024 біта гэтага ключа ніяк не выкарыстоўваюцца… але падумаем пакуль вось пра што. На дадзеным кроку ў нас ёсць з серверам агульны сакрэт. Усталяваны аналаг TLS-сесіі, вельмі затратнай працэдурай. Але сервер яшчэ нічога не ведае аб тым, хто мы такія! Яшчэ не, уласна, аўтарызацыі. Г.зн. калі Вы думалі ў паняццях «лагін-пароль», як калісьці ў ICQ, ці хаця б «лагін-ключ», як у SSH (напрыклад на які-небудзь gitlab/github). Мы атрымалі ананімус. А калі сервер адкажа нам "дадзеныя тэлефонныя нумары абслугоўваюцца іншым DC"? Ці ўвогуле «ваш тэлефонны нумар забанены»? Лепшае, што мы можам зрабіць - гэта захаваць ключ у надзеі, што яшчэ спатрэбіцца і не пратухне да таго моманту.

Дарэчы, "атрымалі" мы яго з агаворкамі. Вось напрыклад, мы давяраем серверу? Раптам ён падроблены? Патрэбны б крыптаграфічныя праверкі:

Vasily, [21.06.18 17:53] Яны прапануюць мабільным кліентам правяраць 2кбитное лік на прастату %)

Але наогул незразумела, нафейхаа

Vasily, [21.06.18 18:02] У доку не сказана, што рабіць, калі яно не простае аказалася

Не сказана. Давайце паглядзім, што ў гэтым выпадку робіць афіцыйны кліент пад андроідам? А вось што (і так, тамака ўвесь файл цікавы) — як гаворыцца, я проста пакіну гэта тут:

278     static const char *goodPrime = "c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d930f48198a0aa7c14058229493d22530f4dbfa336f6e0ac925139543aed44cce7c3720fd51f69458705ac68cd4fe6b6b13abdc9746512969328454f18faf8c595f642477fe96bb2a941d5bcd1d4ac8cc49880708fa9b378e3c4f3a9060bee67cf9a4a4a695811051907e162753b56b0f6b410dba74d8a84b2a14b3144e0ef1284754fd17ed950d5965b4b9dd46582db1178d169c6bc465b0d6ff9ca3928fef5b9ae4e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851f0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b";
279   if (!strcasecmp(prime, goodPrime)) {

Не, там вядома яшчэ нейкія праверкі прастаты ліку ёсць, але асабіста я дастатковымі ведамі ў матэматыцы ўжо не валодаю.

Добра, мы атрымалі асноўны ключ. Каб аўтарызавацца, г.зн. даслаць запыты, трэба рабіць далейшае шыфраванне, ужо з дапамогай AES.

Message key з'яўляецца 128 сярэдняга бітаў SHA256 Message Body (уключаючы сесія, Message ID, etc.), у тым ліку padding bytes, prepended by 32 bytes take z authorization key.

Vasily, [22.06.18 14:08] Сярэднія, сука, біты

Атрымаў auth_key. Усё. Далей за іх… не зразумела з докі. Дадамо, каб выканаць агульную крыніцу кода.

Памяце пра тое, што MTProto 2.0 патрабуецца ад 12 да 1024 байт Padding, спыняецца на здзелку, што вынікае паведамленне з 16 bytes.

Дык колькі падынгу сыпаць?

І так, тут таксама 404 у выпадку памылкі.

Калі нехта ўважліва вывучыў схему і тэкст дакументацыі, звярнуў увагу, што ніякага MAC там няма. І што AES выкарыстоўваецца ў нейкім, больш нідзе не прымяняецца рэжыме IGE. Яны, вядома, пішуць пра гэта ў сваім FAQ… Тут, тыпу, сам ключ паведамлення заадно і з'яўляецца SHA-хэшам расшыфраваных дадзеных, выкарыстоўваным для праверкі цэласнасці – прычым у выпадку несупадзення дакументацыя чамусьці рэкамендуе silently ignore іх (а як жа бяспека, раптам нас ламаюць?).

Я не крыптограф, можа, у гэтым рэжыме ў дадзеным выпадку і няма нічога дрэннага з тэарэтычнага пункту гледжання. Але я магу дакладна назваць практычную праблему, на прыкладзе Telegram Desktop. Ён лакальны кэш (усе вось гэтыя D877F783D5D3EF8C) шыфруе тым жа спосабам, што паведамленні ў MTProto (толькі ў дадзеным выпадку версіі 1.0), г.зн. спачатку ключ паведамлення, потым самі дадзеныя (і дзесьці ў баку асноўнай вялікай auth_key на 256 байт, без якога msg_key бескарысны). Дык вось, праблема становіцца прыкметная на вялікіх файлах. У прыватнасці, Вам трэба трымаць дзве копіі дадзеных - шыфраваную і расшыфраваную. А калі тамака мегабайты, ці струменевае відэа, напрыклад?.. Класічныя схемы з MAC пасля шыфратэксту дазваляюць Вам лічыць яго струменева, адразу перадаючы. А з MTProto давядзецца спачатку зашыфраваць ці расшыфраваць паведамленне цалкам, толькі потым перадаваць у сетку ці на дыск. Таму ў свежых версіях Telegram Desktop у кэшы ў user_data ужываецца ўжо і іншы фармат - з AES у рэжыме CTR.

Vasily, [21.06.18 01:27] О, я даведаўся, што такое IGE: IGE была першая спроба на "запраўляючы абнаўлення рэжыму", originally for Kerberos. Гэта было нязменна (гэта не гарантуе ўніверсітэт абароны), і мусіць быць выпраўлена. Гэта было наладжванне 20 гадоў quest for authenticating encryption mode, які праца, які цэлай culminated ў рэжымах як OCB і GCM.

А зараз аргументы з боку вазы:

Сістэма прымае Тэлеграм, вядучы Nikolai Durov, складаецца з шасці ACM шампіраў, паўтаго Ph.Ds ў маты. It took them about years to roll out the current version of MTProto.

Чот смешна. Два гады на ніжні ўзровень

А маглі б проста ўзяць tls

Добра, дапусцім, шыфраванне і іншыя нюансы мы зрабілі. Ці можна, нарэшце, пасылаць серыялізаваныя ў TL запыты і дэсерыялізоўваць адказы? Дык а што і як слаць трэба? Вось, дапусцім, метад initConnection, напэўна гэта яно?

Васіль, [25.06.18 18:46] Initializes connection and save information on карыстача прылады і прымянення.

Яно прымае app_id, device_model, system_version, app_version і lang_code.

І нейкі query

Дакументацыя як заўсёды. Feel free k study the open source

Калі з invokeWithLayer усё было прыкладна зразумела, то тут-тое што? Апыняецца, выкажам здагадку ў нас - кліент ужо меў нешта, пра што спытаць сервер - маецца запыт, які мы жадалі паслаць:

Vasily, [25.06.18 19:13] Мяркуючы па кодзе, першы выклік заварочваецца ў гэтую дрысню, а сама дрысня ў invokewithlayer

Чаму initConnection не мог быць асобным выклікам, а абавязкова мусіць быць абгорткай? Так, як аказалася, яго трэба абавязкова кожны раз у пачатку кожнай сесіі рабіць, а не разава, як з асноўным ключом. Але! Яго не можа выклікаць неаўтарызаваны карыстач! Вось мы дабраліся да этапу, у якім дастасавальна вось гэтая старонка дакументацыі - і яна паведамляе нам, што…

Толькі невялікая частка API метадаў можа быць даступнай для неадпаведных карыстачоў:

  • auth.sendCode
  • auth.resendCode
  • account.getPassword
  • auth.checkPassword
  • auth.checkPhone
  • auth.signUp
  • auth.signIn
  • auth.importAuthorization
  • help.getConfig
  • help.getNearestDc
  • help.getAppUpdate
  • help.getCdnConfig
  • langpack.getLangPack
  • langpack.getStrings
  • langpack.getDifference
  • langpack.getLanguages
  • langpack.getLanguage

Самы першы з іх, auth.sendCode, і ёсць той запаветны першы запыт, у якім мы дашлем api_id і api_hash, і пасля якога нам прыходзіць SMS з кодам. А калі мы патрапілі не ў той DC (тэлефонныя нумары гэтай краіны абслугоўвае іншы, напрыклад), то нам прыйдзе памылка з нумарам патрэбнага DC. Каб даведацца, на які IP-адрас па нумары DC трэба злучацца, нам дапаможа help.getConfig. Калісьці там было ўсяго 5 запісаў, але пасля вядомых падзей 2018 года колькасць значна ўзрасла.

Цяпер успомнім тое, што мы патрапілі на гэтым этапе на серверы ананімусам. Ці не занадта затратна для таго, каб проста атрымаць IP-адрас? Чаму было б не рабіць гэта, і іншыя аперацыі, у нешыфраванай частцы MTProto? Чую пярэчанне: "а як пераканацца, што гэта не РКН фальшывымі адрасамі адкажа?". На гэта мы ўспомнім, што ўвогуле-то ў афіцыйныя кліенты. ўшыты RSA-ключы, г.зн. можна проста падпісаць гэтую інфармацыю. Уласна, так ужо і робіцца для інфармацыі па абыходах блакіровак, якую кліенты атрымліваюць па іншых каналах (лагічна, што гэта нельга зрабіць у самім MTProto, яшчэ бо трэба ведаць, куды злучыцца).

Ну добра. На гэтым этапе аўтарызацыі кліента мы яшчэ не аўтарызаваны і не рэгістравалі сваё прыкладанне. Мы жадаем проста пакуль паглядзець, што адказвае сервер на метады, даступныя неаўтарызаванаму карыстачу. І тут…

Vasily, [10.07.18 14:45] https://core.telegram.org/method/help.getConfig

config#7dae33e0 [...] = Config;
help.getConfig#c4f9186b = Config;

https://core.telegram.org/api/datacenter

config#232d5905 [...] = Config;
help.getConfig#c4f9186b = Config;

У схеме першае, прыходзіць другое

У схеме tdesktop трэцяе значэнне

Так, з таго часу, вядома, дакументацыю абнавілі. Хаця хутка яна зноў можа стаць неактуальнай. А адкуль павінен ведаць пачатковец распрацоўшчык? Можа быць, калі зарэгістраваць свой дадатак, то паведамяць? Васіль зрабіў гэта, але нажаль - нічога яму не даслалі (зноў, пагаворым пра гэта ў другой частцы).

… Вы заўважылі, што мы ўжо неяк перайшлі да API, г.зн. да наступнага ўзроўню, і нешта прапусцілі ў тэме MTProto? Нічога дзіўнага:

Vasily, [28.06.18 02:04] Мм, яны мацаюць частку алгарытмаў на e2e

Mtproto вызначае алгарытмы і ключы шыфравання для абодвух даменаў, а таксама крыху структуру абгорткі

Але яны ўвесь час змешваюць розныя ўзроўні стэка, так што не заўсёды зразумела, дзе скончыўся mtproto і пачаўся наступны ўзровень

Як змешваюць? Ну вось той жа часовы ключ для PFS, напрыклад (дарэчы, Telegram Desktop яго не ўмее). Ён выконваецца запытам API auth.bindTempAuthKey, г.зн. з верхняга ўзроўню. Але пры гэтым урываецца ў шыфраванне на ніжнім узроўні - пасля яго, напрыклад, трэба зноўку рабіць initConnection і да т.п., гэта не проста звычайны запыт. Асобна дастаўляе яшчэ і тое, што можна мець толькі АДЗІН часовы ключ на DC, хоць поле auth_key_id у кожным паведамленні дазваляе мяняць ключ хоць кожнае паведамленне, і што сервер мае права ў любы момант "забыцца" часовы ключ - што ў гэтым выпадку рабіць, дакументацыя не кажа… ну чаму нельга было б мець некалькі ключоў, як з наборам future salts, а ?..

Варта адзначыць у тэме MTProto яшчэ некаторыя рэчы.

Паведамленні аб паведамленнях, msg_id, msg_seqno, пацверджання, пінгі не ў той бок і іншыя ідыясінкразіі

Чаму пра іх трэба ведаць? Таму што яны "працякаюць" на ўзровень вышэй, і пра іх трэба ведаць, працуючы з API. Напрыклад, msg_key нас не цікавіць, ніжні ўзровень расшыфраваў усё для нас. Але ўнутры расшыфраваных дадзеных у нас такія палі (яшчэ даўжыня дадзеных, каб ведаць, дзе padding, але гэта не важна):

  • salt - int64
  • session_id - int64
  • message_id - int64
  • seq_no - int32

Нагадаем, соль - адна на ўвесь DC. Навошта пра яе ведаць? Не толькі таму, што ёсць запыт get_future_salts, Які паведамляе, у якія інтэрвалы якія будуць валідныя, але і таму, што калі Ваша соль «пратухла», то паведамленне (запыт) — проста згубіцца. Сервер, вядома, паведаміць новую соль, выдаўшы new_session_created — але са старым давядзецца неяк рабіць перапасылку, напрыклад. І гэтае пытанне ўплывае на архітэктуру дадатку.

Серверу дазволена наогул драпаць сесіі і адказваць такім чынам па шматлікіх падставах. Уласна, што такое сесія MTProto са боку кліента? Гэта два лікі, session_id и seq_no паведамлення ўнутры гэтай сесіі. Ну, і ніжэйлеглае TCP-злучэнне, вядома. Дапушчальны, наш кліент яшчэ шмат чаго не ўмее, адлучыўся, перападлучыўся. Калі гэта адбылося хутка – у новым TCP-злучэнні працягнулася старая сесія, павялічваем seq_no далей. Калі доўга - сервер мог яе выдаліць, таму што на яго баку гэта яшчэ і чарга, як мы высветлілі.

Які павінен быць seq_no? О, гэта хітрае пытанне. Паспрабуйце сумленна зразумець, што мелася на ўвазе:

Content-related Message

A message requiring an explicit acknowledgment. Уключаюць усе карыстача і мноства паслуг, віртуальна ўсе з выпраменьваннем containers and acknowledgments.

Message Sequence Number (msg_seqno)

У 32-бітным нумары эквівалентны два нумары “content-related” messages (those requiring acknowledgment, and in particular those that non containers) content-related message. A container is always generated after its entire contents; Therefore, яго лік лік лічыльнік, што або эквалайт для лічбы нумары нумароў, размешчаных у ім.

Што гэта за цырк з інкрэмэнтам на 1, а потым яшчэ на 2? некалькі пацверджанняў, якія маюць адзін і той жа seq_no! Як? Ну напрыклад сервер нам нешта шле, шле, а мы самі маўчым, толькі адказваем сэрвіснымі паведамленнямі пацверджанняў аб атрыманні яго паведамленняў. У гэтым выпадку нашы выходныя пацверджанні будуць мець адзін і той жа выходны нумар. Калі Вы знаёмыя з TCP і падумалі, што гэта гучыць неяк дзіка, але быццам бы не вельмі і дзіка, бо ў TCP seq_no не мяняецца, а пацвярджэнне ідзе на seq_no таго боку - то паспяшаюся засмуціць. У MTProto пацверджанні ідуць НЕ па seq_no, як у TCP, а па msg_id !

Што ж гэта за msg_id, самае важнае з гэтых палёў? Унікальны ідэнтыфікатар паведамлення, як вынікае з назвы. Вызначаны ён як 64-бітны лік, самыя малодшыя біты якога зноў маюць магію "сервер-не сервер", а астатняе - Unix timestamp, уключаючы дробавую частку, ссунуты на 32 біта налева. Г.зн. пазнака часу ў сутнасці (і паведамленні са занадта адрозным часам будуць адпрэчаныя серверам). З гэтага выходзіць, што ўвогуле-то гэта ідэнтыфікатар, глабальны для кліента. Пры тым, што - успомнім session_id - нам гарантуецца: Under no circumstances can message meant for one session можа быць у той ці іншы пункт. Гэта значыць, атрымліваецца, што ёсць аж 3 ўзроўню - сесія, нумар у сесіі, id паведамлення. Навошта такое пераўскладненне, гэтая таямніца ёсць вельмі вялікая.

Такім чынам, msg_id патрэбен для…

RPC: запыты, адказы, памылкі. Пацверджанні.

Як Вы, можа быць, заўважылі, нідзе ў схеме няма спецыяльнага тыпу або функцыі "зрабіць RPC-запыт", хоць ёсць адказы. Бо ў нас жа ёсць content-related паведамлення! Гэта значыць, любое паведамленне можа быць запытам! Ці не быць. Бо ў кожнага ёсць msg_id. А вось адказы - ёсць:

rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult;

Вось тут і ўказваецца, на якое паведамленне гэта адказ. Таму Вам, на верхнім узроўні API, давядзецца памятаць, які нумар быў у Вашага запыту — думаю, не трэба тлумачыць, што праца асінхронная, і адначасова ў працы можа быць некалькі запытаў, адказы на якія могуць вярнуцца ў любым парадку? У прынцыпе, з гэтага, і паведамленняў аб памылках тыпу no workers, прасочваецца якая стаіць за гэтым архітэктура: які падтрымлівае з Вамі TCP-злучэнне сервер – фронтэнд-балансавальнік, ён накіроўвае запыты на бэкэнды і збірае іх зваротна па message_id. Накшталт тут усё зразумела, лагічна і добра.

Так?.. А калі задумацца? Бо ў самога RPC-адказу таксама ёсць поле msg_id! Ці трэба нам гарлапаніць серверу "вы не адказваеце на мой адказ!"? І так, што там было пра пацверджанні? Старонка пра паведамленні пра паведамленні кажа нам, што ёсць

msgs_ack#62d6b459 msg_ids:Vector long = MsgsAck;

і яго павінен рабіць кожны бок. Але не заўсёды! Калі вы атрымалі RpcResult, ён сам служыць пацверджаннем. Гэта значыць, на Ваш запыт сервер можа адказаць MsgsAck - тыпу, "я атрымаў". Можа адразу адказаць RpcResult. Можа быць і тое і іншае.

І так, Вы ткі павінны адказаць на адказ! Пацвярджэннем. Інакш сервер будзе лічыць яго недастаўленым і вываліць Вам яго зноў. Нават пасля перападлучэння. Але тут, вядома, пытанне таймаўтаў узнікне. Разгледзім іх крыху пазней.

А пакуль разгледзім магчымыя памылкі выканання запытаў.

rpc_error#2144ca19 error_code:int error_message:string = RpcError;

О, усклікне нехта, тут больш чалавечны фармат — ёсць радок! Не спяшайцеся. Вось пералік памылак, Але, вядома, не поўны. З яго мы даведаемся, што код — нешта накшталт HTTP-памылкі (ну зразумела, семантыка адказаў не выконваецца, месцамі яны размеркаваны па кодах як патрапіла), а радок мае выгляд тыпу ВЯЛІКІЯ_БУКВЫ_І_ЛІЧБЫ. Напрыклад, PHONE_NUMBER_OCCUPIED ці FILE_PART_Х_MISSING. Ну гэта значыць, Вам гэты радок яшчэ давядзецца прапарсіць. напрыклад, FLOOD_WAIT_3600 будзе азначаць, што трэба чакаць гадзіну, а PHONE_MIGRATE_5, Што тэлефонным нумары з гэтым прэфіксам трэба рэгістравацца ў 5-м DC. У нас жа мова тыпаў, так? Аргумент з радка нам не патрэбен, рэгуляркамі абыдуцца, чо.

Ізноў жа, на старонцы сэрвісных паведамленняў гэтага няма, але, як ужо звыкла з гэтым праектам, інфармацыя можа знайсціся на іншай старонцы дакументацыі. або навесці на падазрэнне. Па-першае, глядзіце, парушэнне тыпізацыі / пластоў - RpcError можа быць укладзены ў RpcResult. Чаму не звонку? Што мы не ўлічылі?.. Адпаведна, дзе гарантыя, што RpcError можа быць і НЕ ўкладзены ў RpcResult, а быць напрамую або ўкладзены ў іншы тып?.. А калі не можа, чаму ён не верхняга ўзроўню, г.зн. у ім адсутнічае req_msg_id ? ..

Але працягнем аб сэрвісных паведамленнях. Кліент можа палічыць, што сервер доўга думае, і зрабіць вось такі выдатны запыт:

rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;

На яго магчымыя тры варыянты адказу, ізноў перасякальных з механізмам пацверджанняў, паспрабаваць зразумець, якімі яны павінны быць (і які наогул спіс тыпаў, не якія патрабуюць пацверджанняў), чытачу пакідаецца ў якасці хатняга задання (заўвага: у зыходніках Telegram Desktop інфармацыя не поўная).

Наркаманія: статусы паведамленняў аб паведамленнях

Наогул, адчуванне упартасці пакідаюць шматлікія месцы ў TL, MTProto і Telegram у цэлым, але з ветлівасці, тактоўнасці і іншых мяккія навыкі мы пра гэта ветліва прамаўчалі, а мацюкі ў дыялогах адцэнзуравалі. Аднак гэтае месца, бОбольшая частка старонкі пра паведамленні пра паведамленні выклікае збянтэжанасць нават у мяне, даўно які працуе з сеткавымі пратаколамі і бачыў ровары рознай ступені крывасці.

Пачынаецца яна бяскрыўдна, з пацверджанняў. Далей нам расказваюць пра

bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification;
bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification;

Ну, з імі прыйдзецца сутыкнуцца кожнаму пачаткоўцу працаваць з MTProto, у цыкле "паправіў - перакампіляваў - запусціў" атрымаць памылкі нумароў або паспеўшую пратухнуць за час правак соль - звычайная справа. Аднак тут два моманты:

  1. З гэтага вынікае, што арыгінальнае паведамленьне страчана. Трэба гарадзіць нейкія чэргі, разгледзім гэта пазней.
  2. Што за дзіўныя нумары памылак? 16, 17, 18, 19, 20, 32, 33, 34, 35, 48, 64… дзе астатнія нумары, Томі?

Дакументацыя сцвярджае:

З'ява з'яўляецца тое, што error_code цэны знаходзяцца ў групе (error_code >> 4): для вызначэння, коды 0x40 - 0x4f адпавядаюць errors in container decomposition.

але, па-першае, зрух у іншы бок, па-другое, усё роўна, дзе астатнія коды? У галаве аўтара?.. Зрэшты, гэта дробязі.

Наркаманія пачынаецца ў паведамленнях аб статусах паведамленняў і копіях паведамленняў:

  • Request for Message Status Information
    Ak яе частка не задавалася інфармацыяй на статуце яе паступаючых паведамленняў для тых часоў, яна можа быць explicitly request it z other party:
    msgs_state_req#da69fb52 msg_ids:Vector long = MsgsStateReq;
  • Informational Message regarding Status of Messages
    msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;
    Тут, info string that contains exactly one byte of message status for each message from the incoming msg_ids list:

    • 1 = не мае значэння аб тым, што паведамленне (msg_id too low, other party may have forgotten it)
    • 2 = не received message (msg_id falls within range of stored identifiers; however, thether party no certainly no received a message like that)
    • 3 = не атрымліваецца выдавацца (msg_id too high; however, thether party no certainly no received it yet)
    • 4 = received message.
    • +8 = message already acknowledged
    • +16 = не патрабуецца патрэба ў змене
    • +32 = RPC query contained in message being processed or processing already complete
    • +64 = content-related response to message already generated
    • +128 = іншыя часткі здагадак для факту, што паведамленне з'яўляецца выданым
      Гэтыя адказы не патрабуюць змены. Гэта вызначэнне значэння msgs_state_req, у яго.
      Згадайце, што, калі яго беруць ад сутначы, што інша частка не мае ніякага паведамлення, што здарылася б, што ён будзе мець гэта, ён будзе недастаткова. Кожны раз, калі інша частка павінна прывесці два мадыфікацыі паведамленняў у той час, як duplicate будуць ignorаваны. (Калі ўвесь час маецца, і арыгінальны msg_id не мае даўгагады здзейсненага, электронная пошта змяшчаецца ў msg_copy).
  • Voluntary Communication of Status of Messages
    Яе Party могуць быць хворымі на іншую частку статуі паведамленняў, перакладзеных на Іншую Party.
    msgs_all_info#8cc0d131 msg_ids:Vector long info:string = MsgsAllInfo
  • Extended Voluntary Communication of Status of One Message
    ...
    msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo;
    msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo;
  • Explicit Request to Re-Send Messages
    msg_resend_req#7d861a08 msg_ids:Vector long = MsgResendReq;
    Remote party immediately responds by re-sending the requested messages […]
  • Explicit Request to Re-Send Answers
    msg_resend_ans_req#8610baeb msg_ids:Vector long = MsgResendReq;
    Remote party immediately responds by re-sending Адказы to the requested messages […]
  • Message Copies
    У некаторых абставінах, у апошнім месяцы з msg_id, які не маюць даўгагады, неабходна для таго, каб быць адменена. Then, it is wrapped in a copy container:
    msg_copy#e06046b2 orig_message:Message = MessageCopy;
    Адзін received, the message je processed as if the wrapper were not there. However, jestli je známa pro certain that the message orig_message.msg_id быў узнагароджаны, то ў новым message не processed (у той час, калі гэта час, гэта і orig_message.msg_id acknowledged). Вартасць orig_message.msg_id мусіць быць нізкім, чым матэрыялы msg_id.

Нават памаўчым аб тым, што ў msgs_state_info ізноў тырчаць вушы недаробленага TL (патрэбен быў вектар байт, і ў малодшых двух бітах enum, а ў старэйшых сцягі). Сутнасць у іншым. Хто-небудзь разумее, навошта ўсё гэта на практыцы у рэальным кліенце трэба?.. З цяжкасцю, але можна ўявіць сабе нейкую карысць, калі чалавек займаецца адладкай, прычым у інтэрактыўным рэжыме - спытаць у сервера, што ды як. Але тут апісваюцца запыты у абодва бакі.

Адгэтуль выцякае, што кожны бок павінен не проста шыфраваць і адпраўляць паведамленні, але і захоўваць дадзеныя аб іх саміх, аб адказах на іх, прычым невядомая колькасць часу. Дакументацыя ні таймінгі, ні практычную дастасавальнасць гэтых фіч не апісвае ніяк. Што самае дзіўнае, яны сапраўды выкарыстоўваюцца ў кодзе афіцыйных кліентаў! Відаць, ім паведамілі нешта, што не ўвайшло ў адкрытую дакументацыю. Зразумець жа з кода, навошта, ужо не так проста, як у выпадку TL - гэта не (параўнальна) лагічна ізаляваная частка, а кавалак, завязаны на архітэктуру прыкладання, г.зн. запатрабуе значна больш часу на ўнікненне ў код дадатку.

Пінгі і таймінгі. Чэргі.

З усяго, калі ўспомніць здагадкі аб архітэктуры сервера (размеркаванне запытаў па бэкендах), выцякае даволі маркотная рэч – нягледзячы на ​​ўсе гарантыі дастаўкі што ў TCP (альбо дадзеныя дастаўленыя, альбо Вам паведамяць аб разрыве, але дадзеныя да моманту праблемы будуць дастаўлены), што пацверджання ў самым MTProto гарантый няма. Сервер можа проста страціць або выкінуць Ваша паведамленне, і нічога з гэтым зрабіць нельга, толькі гарадзіць мыліцы розных відаў.

І перш за ўсё - чэргі паведамленняў. Ну, з адной усё было відавочна з самага пачатку - непацверджанае паведамленне трэба захоўваць і перасылаць. А праз які час? А блазан яго ведае. Магчыма, вунь тыя наркаманскія сэрвісныя зносіны неяк мыліцамі вырашаюць гэтую праблему, скажам, у Telegram Desktop прыкладна штукі 4 чаргі, ім адпаведных (можа больш, як ужо гаварылася, для гэтага трэба ўнікаць у яго код і архітэктуру больш сур'ёзна; пры гэтым мы ведаем, што за ўзор яго браць нельга, энную колькасць тыпаў са схемы MTProto у ім не выкарыстоўваецца).

Чаму так адбываецца? Верагодна, праграмісты сервера не змаглі забяспечыць надзейнасць усярэдзіне кластара, ці хаця б нават буферызацыю на фронце-балансавальнік, і пераклалі гэтую праблему на кліента. Ад безвыходнасці Васіль паспрабаваў рэалізаваць альтэрнатыўны варыянт, з усяго двума чэргамі, выкарыстоўваючы алгарытмы з TCP – замяраючы RTT да сервера і карэктуючы памер "акна" (у паведамленнях) у залежнасці ад колькасці непацверджаных запытаў. Гэта значыць, грубая такая эўрыстыка для адзнакі загружанасці сервера - колькі адначасова нашых запытаў ён можа жаваць і не губляць.

Ну гэта значыць, Вы разумееце, так? Калі па-над які працуе па TCP пратаколу прыходзіцца рэалізоўваць ізноў TCP — гэта кажа аб вельмі дрэнна спраектаваным пратаколе.

Ах так, чаму трэба больш за адну чаргу, і наогул, што гэта значыць для чалавека, які працуе з высокаўзроўневым API? Глядзіце, Вы робіце запыт, серыялізуеце яго, але адправіць яго неадкладна часта нельга. Чаму? Таму што адказ будзе па msg_id, які ёсць часая пазнака, прызначэнне якой лепш адкласці на як мага пазней - раптам сервер адпрэчыць з-за несупадзення часу ў нас і ў яго кліенты так і робяць, але гэты спосаб грубы і недакладны з-за буферызацыі). Таму, калі Вы робіце запыт лакальным выклікам функцыі з бібліятэкі, паведамленне праходзіць наступныя стадыі:

  1. Ляжыць у адной чарзе і чакае шыфравання.
  2. прызначаны msg_id і паведамленне лягло ў іншую чаргу - магчымай перапасылкі; адпраўляем у сокет.
  3. а) Сервер адказаў MsgsAck - паведамленне дастаўлена, выдаляем з "іншай чаргі".
    б) Або наадварот, нешта яму не спадабалася, ён адказаў badmsg - перасылаем з "іншай чаргі"
    в) Нічога невядома, трэба пераслаць паведамленне з іншай чаргі - але невядома дакладна, калі.
  4. Сервер нарэшце адказаў RpcResult - уласна адказам (ці памылкай) - не проста дастаўлена, але і апрацавана.

Магчыма, часткова вырашыць праблему магло б выкарыстанне кантэйнераў. Гэта калі пачак паведамленняў пакуецца ў адно, і сервер адказаў пацвярджэннем на ўсю адразу, адным msg_id. Але і адкіне ён гэты пачак, калі нешта пайшло не так, таксама ўвесь цалкам.

І тут уступаюць у дзеянне ўжо не тэхнічныя меркаванні. Па досведзе, мы бачылі шмат мыліц, а акрамя таго, цяпер убачым яшчэ прыклады дрэнных парадаў і архітэктуры — у такіх умовах, ці варта давяраць і прымаць такія рашэнні? Пытанне рытарычнае (вядома, не).

Пра што гаворка? Калі па тэме «наркаманскія паведамленні аб паведамленнях» яшчэ можна спекуляваць пярэчаннямі выгляду «гэта вы тупыя, не зразумелі наш геніяльны задума!» (так напішыце спачатку дакументацыю, як належыць у нармальных людзей, з rationale і прыкладамі абмену пакетаў, тады і пагаворым), то таймінгі/таймаўты - пытанне асабліва практычнае і канкрэтнае, тут усё даўно вядома. А што ж нам кажа дакументацыя аб таймаўтах?

Server звычайна вызначае выдачу паведамлення ад кліента (заразна, на RPC Query) шляхам RPC response. If a response is a long time coming, a server may first send a receipt acknowledgment, and somewhat later, the RPC response itself.

Client неадназначна ўстанаўлівае выдачу паведамленняў ад сервера (звычайна, на RPC response) з пункту гледжання ўводу ў наступную RPC не з'яўляецца, што гэта не перакладзеная тэхналогія (як гэта генеруецца, прымае, 60-120 seconds following the recei of a message from the server). However, калі для доўгага перыяду часу няма ніякай адказнасці на паведамленні для сервера або калі ёсць вялікая колькасць неад'емных паведамленняў ад сервера (прыкладна, больш за 16), кліент transmits a stand-alone acknowledgment.

… Перакладаю: мы самі не ведаем, колькі і як трэба, ну давайце прыкінем, што няхай будзе вось так.

І аб пінгах:

Ping Messages (PING/PONG)

ping#7abe77ec ping_id:long = Pong;

A response is usually returned to the same connection:

pong#347773c5 msg_id:long ping_id:long = Pong;

Гэтыя паведамленні не патрабуюць зменаў. Pong is transmitted only in response to a ping while a ping can be initiated by ether side.

Deferred Connection Closure + PING

ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;

Works like ping. У сувязі з тым, што гэта received, server starts a timer which will close the current connection disconnect_delay seconds later unless it receives new message of the same typ which automatically reset all previous timers. Калі кліенты маюць pings once every 60 seconds, for example, it may set disconnect_delay equal to 75 seconds.

Ды вы звар'яцелі?! За 60 секунд цягнік заедзе на станцыю, высадзіць-возьме пасажыраў, і зноў страціць сувязь у тунэлі. За 120 секунд, пакуль прачухаецеся, ён прыедзе на іншую, і злучэнне хутчэй за ўсё парвецца. Ну, зразумела адкуль ногі растуць – «чуў звон, ды не ведае дзе ён», ёсць алгарытм Нахабнік і опцыя TCP_NODELAY, якая прызначалася для інтэрактыўнай працы. Але, прабачце, яе дэфолтнае значэнне затрымай - 200 мілісекунд. Калі вам так ужо жадаецца адлюстраваць нешта падобнае і зэканоміць на магчымай пары пакетаў - ну адкладзяце, скрайне, на 5 секунд, ці чаму там цяпер роўны таймаўт паведамлення «User is typing…». Але не болей.

І нарэшце, пінгі. Гэта значыць, праверка жвавасці TCP-злучэнні. Пацешна, але прыкладна 10 гадоў таму я пісаў крытычны тэкст аб мэсэнджэры інтэрната нашага факультэта - там аўтары таксама пінгавалі сервер з кліента, а не наадварот. Але адна справа студэнты 3 курса, а іншая - міжнародная кантора, так?..

Спачатку невялікі лікбез. TCP-злучэнне, пры адсутнасці абмену пакетамі, можа жыць тыднямі. Гэта і добра, і дрэнна, у залежнасці ад мэты. Добра, калі ў Вас было адчынена SSH-злучэнне на сервер, Вы ўсталі з-за кампутара, перазагрузілі роўтэр па сілкаванні, вярнуліся на месца - сесія праз гэты сервер не парвалася (нічога не набіралі, пакетаў не было), зручна. Дрэнна, калі на серверы тысячы кліентаў, кожны займае рэсурсы (прывітанне, Постгрэс!), І хост кліента, магчыма, даўно ўжо перазагрузіўся – але мы пра гэта не даведаемся.

Сістэмы чатаў / IM ставяцца да другога выпадку па яшчэ адной, дадатковай прычыне – анлайн-статусы. Калі карыстач «адваліўся», трэба паведаміць пра гэта яго суразмоўцам. Інакш атрымаецца памылка, якую дапусцілі стваральнікі Jabber (і 20 гадоў выпраўлялі) – карыстач адлучыўся, але яму працягваюць пісаць паведамленні, лічачы, што ён online (якія яшчэ і цалкам губляліся ў гэтыя некалькі хвілін да выяўлення парыву). Не, опцыя TCP_KEEPALIVE, якую шматлікія не якія разумеюць, як працуюць таймеры TCP, соваюць куды патрапіла (ставячы дзікія значэнні тыпу дзясяткаў секунд), тут не дапаможа - Вам трэба пераканацца, што жыва не толькі ядро ​​АС машыны карыстача, але і звычайна функцыянуе, у стане адказаць, і само прыкладанне (думаеце, яно не можа завіснуць? Telegram Desktop на Ubuntu 18.04/XNUMX ў мяне завісаў неаднаразова).

Менавіта таму пінгаваць павінен сервер кліента, а не наадварот - калі гэта робіць кліент, пры разрыве злучэння пінг не будзе дастаўлены, мэта не дасягнута.

А што ж мы бачым у Telegram? Усё роўна наадварот! Ну г.зн. фармальна, вядома, абодва бакі могуць пінгаваць адзін аднаго. На практыцы ж - кліенты карыстаюцца мыліцай ping_delay_disconnect, Які ўзводзіць на серверы таймер. Ну прабачце, гэта не справа кліента вырашаць, колькі ён тамака жадае жыць без пінгу. Серверу, зыходзячы са сваёй нагрузкі, лепш відаць. Але, вядома, калі рэсурсаў не шкада, то самі сабе зласлівыя бураціны, і мыліца сыдзе…

А як трэба было праектаваць?

Мяркую, вышэйпрыведзеныя факты дастаткова выразна сведчаць аб не вельмі высокай кампетэнцыі каманды Telegram/УКантакце ў галіне транспартнага (і ніжэй) узроўню кампутарных сетак і іх нізкай кваліфікацыі ў адпаведных пытаннях.

Чаму ж яно такое складанае адбылося, і чым архітэктары Telegram могуць паспрабаваць запярэчыць? Тым, што яны спрабавалі зрабіць сесію, якая перажывае парывы ​​TCP-злучэнняў, т. е. што не даставілі зараз – даставім пазней. Верагодна, яшчэ паспрабавалі зрабіць UDP-транспарт, праўда сутыкнуліся са складанасцямі і закінулі (таму і ў дакументацыі пуста - няма чым пахваліцца было). Але з-за неразумення таго, як працуюць сеткі наогул і TCP у прыватнасці, дзе можна на яго спадзявацца, а дзе трэба рабіць самому (і як), і спробы сумясціць гэта з крыптаграфіяй "адным стрэлам двух зайцаў" - атрымаўся вось такі кадаўр.

А як трэба было? Зыходзячы з таго, што msg_id з'яўляецца пазнакай часу, неабходнай з крыптаграфічнага пункта гледжання для прадухілення replay-нападаў, памылкай з'яўляецца навешванне на яго функцыі ўнікальнага ідэнтыфікатара. Таму, без кардынальнай змены бягучай архітэктуры (калі фармуецца струмень Updates, гэта тэма высокаўзроўневага API для іншай часткі гэтай серыі пастоў), трэба было б:

  1. Сервер, які трымае TCP-злучэнне з кліентам, бярэ на сябе адказнасць - калі вычытаў з сокета, дазвольце пацвердзіць, апрацаваць або вярнуць памылку, ніякіх страт. Тады пацверджаннем становіцца не вектар id'ов, а проста "апошні атрыманы seq_no" - проста лік, як у TCP (два лікі - свой seq і пацверджаны). Мы ж у рамках сесіі заўсёды, ці не так?
  2. Пазнака часу для прадухілення replay-нападаў становіцца асобным полем, а-ля nonce. Правяраецца, але больш ні на што іншае не ўплывае. Хопіць і uint32 - калі ў нас соль змяняецца не радзей кожных паўсутак, можна адвесці 16 біт на малодшыя біты цэлай часткі бягучага часу, астатняе - на дробавую частку секунды (як і цяпер).
  3. Прыбіраецца msg_id зусім - з пункту гледжання адрознівання запытаў на бэкендах ёсць, па-першае, id кліента, па-другое, id сесіі, іх і канкатэнуйце. Адпаведна, у якасці ідэнтыфікатара запыту дастаткова адно толькі seq_no.

Таксама не самы ўдалы варыянт, ідэнтыфікатарам мог бы служыць і поўны рандом – так ужо робіцца ў высокаўзроўневым API пры адпраўцы паведамлення, дарэчы. Лепш было б увогуле перарабіць архітэктуру з адноснай на абсалютную, але гэта тэму ўжо для іншай часткі, не гэтай пасады.

API?

Та-даам! Такім чынам, прадзершыся праз шлях, поўны болі і мыліц, мы нарэшце змаглі адпраўляць на сервер любыя запыты і атрымліваць на іх любыя адказы, а таксама атрымліваць ад сервера апдэйты (не ў адказ на запыт, а ён сам нам дасылае, тыпу PUSH, калі каму -то так больш зразумела).

Увага, зараз будзе адзіны ў артыкуле прыклад на Perl! (Для тых, хто не знаёмы з сінтаксісам, першы аргумент bless - структура дадзеных аб'екта, другі - яго клас):

2019.10.24 12:00:51 $1 = {
'cb' => 'TeleUpd::__ANON__',
'out' => bless( {
'filter' => bless( {}, 'Telegram::ChannelMessagesFilterEmpty' ),
'channel' => bless( {
'access_hash' => '-6698103710539760874',
'channel_id' => '1380524958'
}, 'Telegram::InputPeerChannel' ),
'pts' => '158503',
'flags' => 0,
'limit' => 0
}, 'Telegram::Updates::GetChannelDifference' ),
'req_id' => '6751291954012037292'
};
2019.10.24 12:00:51 $1 = {
'in' => bless( {
'req_msg_id' => '6751291954012037292',
'result' => bless( {
'pts' => 158508,
'flags' => 3,
'final' => 1,
'new_messages' => [],
'users' => [],
'chats' => [
bless( {
'title' => 'Хулиномика',
'username' => 'hoolinomics',
'flags' => 8288,
'id' => 1380524958,
'access_hash' => '-6698103710539760874',
'broadcast' => 1,
'version' => 0,
'photo' => bless( {
'photo_small' => bless( {
'volume_id' => 246933270,
'file_reference' => '
'secret' => '1854156056801727328',
'local_id' => 228648,
'dc_id' => 2
}, 'Telegram::FileLocation' ),
'photo_big' => bless( {
'dc_id' => 2,
'local_id' => 228650,
'file_reference' => '
'secret' => '1275570353387113110',
'volume_id' => 246933270
}, 'Telegram::FileLocation' )
}, 'Telegram::ChatPhoto' ),
'date' => 1531221081
}, 'Telegram::Channel' )
],
'timeout' => 300,
'other_updates' => [
bless( {
'pts_count' => 0,
'message' => bless( {
'post' => 1,
'id' => 852,
'flags' => 50368,
'views' => 8013,
'entities' => [
bless( {
'length' => 20,
'offset' => 0
}, 'Telegram::MessageEntityBold' ),
bless( {
'length' => 18,
'offset' => 480,
'url' => 'https://alexeymarkov.livejournal.com/[url_вырезан].html'
}, 'Telegram::MessageEntityTextUrl' )
],
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'text' => '???? 165',
'data' => 'send_reaction_0'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 9'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'message' => 'А вот и новая книга! 
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
напечатаю.',
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'date' => 1571724559,
'edit_date' => 1571907562
}, 'Telegram::Message' ),
'pts' => 158508
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'pts' => 158508,
'message' => bless( {
'edit_date' => 1571907589,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'date' => 1571807301,
'message' => 'Почему Вы считаете Facebook плохой компанией? Можете прокомментировать? По-моему, это шикарная компания. Без долгов, с хорошей прибылью, а если решат дивы платить, то и еще могут нехило подорожать.
Для меня ответ совершенно очевиден: потому что Facebook делает ужасный по качеству продукт. Да, у него монопольное положение и да, им пользуется огромное количество людей. Но мир не стоит на месте. Когда-то владельцам Нокии было смешно от первого Айфона. Они думали, что лучше Нокии ничего быть не может и она навсегда останется самым удобным, красивым и твёрдым телефоном - и доля рынка это красноречиво демонстрировала. Теперь им не смешно.
Конечно, рептилоиды сопротивляются напору молодых гениев: так Цукербергом был пожран Whatsapp, потом Instagram. Но всё им не пожрать, Паша Дуров не продаётся!
Так будет и с Фейсбуком. Нельзя всё время делать говно. Кто-то когда-то сделает хороший продукт, куда всё и уйдут.
#соцсети #facebook #акции #рептилоиды',
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 452'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'text' => '???? 21',
'data' => 'send_reaction_1'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'entities' => [
bless( {
'length' => 199,
'offset' => 0
}, 'Telegram::MessageEntityBold' ),
bless( {
'length' => 8,
'offset' => 919
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'offset' => 928,
'length' => 9
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 6,
'offset' => 938
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 11,
'offset' => 945
}, 'Telegram::MessageEntityHashtag' )
],
'views' => 6964,
'flags' => 50368,
'id' => 854,
'post' => 1
}, 'Telegram::Message' ),
'pts_count' => 0
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'message' => bless( {
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 213'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 8'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'views' => 2940,
'entities' => [
bless( {
'length' => 609,
'offset' => 348
}, 'Telegram::MessageEntityItalic' )
],
'flags' => 50368,
'post' => 1,
'id' => 857,
'edit_date' => 1571907636,
'date' => 1571902479,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'message' => 'Пост про 1С вызвал бурную полемику. Человек 10 (видимо, 1с-программистов) единодушно написали:
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
Я бы добавил, что блестящая у 1С дистрибуция, а маркетинг... ну, такое.'
}, 'Telegram::Message' ),
'pts_count' => 0,
'pts' => 158508
}, 'Telegram::UpdateEditChannelMessage' ),
bless( {
'pts' => 158508,
'pts_count' => 0,
'message' => bless( {
'message' => 'Здравствуйте, расскажите, пожалуйста, чем вредит экономике 1С?
// [текст сообщения вырезан чтоб не нарушать правил Хабра о рекламе]
#софт #it #экономика',
'edit_date' => 1571907650,
'date' => 1571893707,
'to_id' => bless( {
'channel_id' => 1380524958
}, 'Telegram::PeerChannel' ),
'flags' => 50368,
'post' => 1,
'id' => 856,
'reply_markup' => bless( {
'rows' => [
bless( {
'buttons' => [
bless( {
'data' => 'send_reaction_0',
'text' => '???? 360'
}, 'Telegram::KeyboardButtonCallback' ),
bless( {
'data' => 'send_reaction_1',
'text' => '???? 32'
}, 'Telegram::KeyboardButtonCallback' )
]
}, 'Telegram::KeyboardButtonRow' )
]
}, 'Telegram::ReplyInlineMarkup' ),
'views' => 4416,
'entities' => [
bless( {
'offset' => 0,
'length' => 64
}, 'Telegram::MessageEntityBold' ),
bless( {
'offset' => 1551,
'length' => 5
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'length' => 3,
'offset' => 1557
}, 'Telegram::MessageEntityHashtag' ),
bless( {
'offset' => 1561,
'length' => 10
}, 'Telegram::MessageEntityHashtag' )
]
}, 'Telegram::Message' )
}, 'Telegram::UpdateEditChannelMessage' )
]
}, 'Telegram::Updates::ChannelDifference' )
}, 'MTProto::RpcResult' )
};
2019.10.24 12:00:51 $1 = {
'in' => bless( {
'update' => bless( {
'user_id' => 2507460,
'status' => bless( {
'was_online' => 1571907651
}, 'Telegram::UserStatusOffline' )
}, 'Telegram::UpdateUserStatus' ),
'date' => 1571907650
}, 'Telegram::UpdateShort' )
};
2019.10.24 12:05:46 $1 = {
'in' => bless( {
'chats' => [],
'date' => 1571907946,
'seq' => 0,
'updates' => [
bless( {
'max_id' => 141719,
'channel_id' => 1295963795
}, 'Telegram::UpdateReadChannelInbox' )
],
'users' => []
}, 'Telegram::Updates' )
};
2019.10.24 13:01:23 $1 = {
'in' => bless( {
'server_salt' => '4914425622822907323',
'unique_id' => '5297282355827493819',
'first_msg_id' => '6751307555044380692'
}, 'MTProto::NewSessionCreated' )
};
2019.10.24 13:24:21 $1 = {
'in' => bless( {
'chats' => [
bless( {
'username' => 'freebsd_ru',
'version' => 0,
'flags' => 5440,
'title' => 'freebsd_ru',
'min' => 1,
'photo' => bless( {
'photo_small' => bless( {
'local_id' => 328733,
'volume_id' => 235140688,
'dc_id' => 2,
'file_reference' => '
'secret' => '4426006807282303416'
}, 'Telegram::FileLocation' ),
'photo_big' => bless( {
'dc_id' => 2,
'file_reference' => '
'volume_id' => 235140688,
'local_id' => 328735,
'secret' => '71251192991540083'
}, 'Telegram::FileLocation' )
}, 'Telegram::ChatPhoto' ),
'date' => 1461248502,
'id' => 1038300508,
'democracy' => 1,
'megagroup' => 1
}, 'Telegram::Channel' )
],
'users' => [
bless( {
'last_name' => 'Panov',
'flags' => 1048646,
'min' => 1,
'id' => 82234609,
'status' => bless( {}, 'Telegram::UserStatusRecently' ),
'first_name' => 'Dima'
}, 'Telegram::User' )
],
'seq' => 0,
'date' => 1571912647,
'updates' => [
bless( {
'pts' => 137596,
'message' => bless( {
'flags' => 256,
'message' => 'Создать джейл с именем покороче ??',
'to_id' => bless( {
'channel_id' => 1038300508
}, 'Telegram::PeerChannel' ),
'id' => 119634,
'date' => 1571912647,
'from_id' => 82234609
}, 'Telegram::Message' ),
'pts_count' => 1
}, 'Telegram::UpdateNewChannelMessage' )
]
}, 'Telegram::Updates' )
};

Так, спецыяльна не пад спойлерам - калі Вы не ўчыталіся, ідзіце і зрабіце гэта!

Oh, wai~~… на што ж гэта падобна? Нешта вельмі знаёмае… можа, гэта структура дадзеных тыповага Web API у JSON, толькі хіба што яшчэ да аб'ектаў класы прычапілі?

Дык гэта ж атрымліваецца... Што ж гэта выходзіць, таварышы?.. Столькі намаганняў — і мы спыніліся перадыхнуць там, дзе Web-праграмісты толькі пачынаюць?.. А проста JSON па-над HTTPS быў бы не прасцей?! А што ж мы атрымалі ў абмен? Ці каштавалі гэтыя намаганні таго?

Давайце ацэнім, што нам далечы TL+MTProto, і якія магчымыя альтэрнатывы. Ну, HTTP, арыентаваны на мадэль «запыт-адказ», падыходзіць дрэнна, але хаця б нешта па-над TLS ?

Кампактная серыялізацыя. Бачачы вось гэтую структуру дадзеных, падобную на JSON, успамінаецца, што ёсьць яго бінарныя варыянты. Адзначым MsgPack як нядосыць які пашыраецца, але вось ёсць, напрыклад, CBOR — між іншым, стандарт, апісаны ў RFC 7049. Характэрны ён тым, што ў ім вызначаны тэгі, як механізм пашырэння, і сярод ужо стандартызаваных маюцца:

  • 25 + 256 — замена радкоў, якія паўтараюцца, на спасылку на нумар радка, такі танны метад кампрэсіі
  • 26 - серыялізаваны аб'ект Perl c імем класа і аргументамі канструктара
  • 27 — серыялізаваны мованезалежны аб'ект з імем тыпу і аргументамі канструктара

Ну што ж, я паспрабаваў адны і тыя ж дадзеныя серыялізаваць у TL і ў CBOR з уключаным пакаваннем радкоў і аб'ектаў. Вынік стаў адрознівацца на карысць CBOR дзесьці ад мегабайта:

cborlen=1039673 tl_len=1095092

Такім чынам, выснову: ёсць істотна больш простыя фарматы, не схільныя да праблемы збою сінхранізацыі або невядомага ідэнтыфікатара, з супастаўнай эфектыўнасцю.

Хуткае ўстанаўленне злучэння. Маецца на ўвазе нулявы RTT пасля перападлучэння (калі ключ быў ужо аднойчы выпрацаваны) - дастасавальна з першага ж паведамлення MTProto, але пры некаторых агаворках - патрапілі ў тую ж соль, сесія не пратухла, etc. Што нам наўзамен прапануе TLS? Цытата па тэме:

Пры выкарыстанні PFS у TLS могуць ужывацца TLS session tickets (RFC 5077) для аднаўлення зашыфраванай сесіі без паўторнага ўзгаднення ключоў і без захавання ключавой інфармацыі на серверы. Пры адкрыцці першага злучэння і стварэння ключоў, сервер шыфруе стан злучэння і перадае яго кліенту (у выглядзе session ticket). Адпаведна, пры аднаўленні злучэння кліент пасылае session ticket, які змяшчае ў тым ліку сесійны ключ, зваротна серверу. Сам ticket шыфруецца часовым ключом (session ticket key), які захоўваецца на серверы і павінен размяркоўвацца па ўсіх frontend-серверах, якія апрацоўваюць SSL у кластарызаваных рашэннях.[10]. Такім чынам, уводзіны session ticket можа парушаць PFS у выпадку кампраметацыі часавых серверных ключоў, напрыклад, пры іх працяглым захоўванні (OpenSSL, nginx, Apache па змаўчанні захоўваюць іх на працягу ўсяго часу працы праграмы; папулярныя сайты выкарыстаюць ключ на працягу некалькіх гадзін, аж да сутак).

Тут RTT не нулявы, трэба абмяняцца прынамсі ClientHello і ServerHello, пасля чаго разам з Finished кліент ужо можа слаць дадзеныя. Але тут варта ўспомніць, што ў нас не Web, з яго кучай ізноў адчыняных злучэнняў, а мэсанджар, злучэнне ў якога часта адно і больш-менш даўгавечнае, адносна кароткіх запытаў на Web-старонкі - усё мультыплексуецца ўсярэдзіне. Гэта значыць, суцэль прымальна, калі нам не патрапіўся зусім ужо дрэнны перагон мятро.

Нешта яшчэ забыўся? Пішыце ў каментах.

To be continued!

У другой частцы гэтай серыі пастоў мы разгледзім больш не тэхнічныя, а арганізацыйныя моманты - падыходы, ідэалогія, інтэрфейс, стаўленне да карыстальнікаў і г.д. Абапіраючыся, зрэшты, на тую тэхнічную інфармацыю, што была выкладзена тут.

У трэцяй частцы будзе працяг разбору тэхнічнай складніку / вопыту распрацоўкі. Вы даведаецеся, у прыватнасці:

  • працяг свістапляскі з разнастайнасцю TL-тыпаў
  • невядомыя рэчы пра каналы і супергрупы
  • чым dialogs горш roster
  • аб абсалютнай vs адноснай адрасацыі паведамленняў
  • чым адрозніваецца photo ад image
  • як эмодзі перашкаджаюць размячаць тэкст курсівам

і іншыя мыліцы! Stay tuned!

Крыніца: habr.com

Дадаць каментар