├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── Dockerfile ├── LICENSE.txt ├── Makefile ├── README.md ├── config ├── epmd.service ├── mtproto-proxy.service ├── sys.config.example └── vm.args.example ├── rebar.config ├── rebar.lock ├── rebar3 ├── src ├── gen_timeout.erl ├── mtp_abridged.erl ├── mtp_aes_cbc.erl ├── mtp_codec.erl ├── mtp_config.erl ├── mtp_dc_pool.erl ├── mtp_dc_pool_sup.erl ├── mtp_down_conn.erl ├── mtp_down_conn_sup.erl ├── mtp_fake_tls.erl ├── mtp_full.erl ├── mtp_handler.erl ├── mtp_intermediate.erl ├── mtp_metric.erl ├── mtp_noop_codec.erl ├── mtp_obfuscated.erl ├── mtp_policy.erl ├── mtp_policy_counter.erl ├── mtp_policy_table.erl ├── mtp_rpc.erl ├── mtp_secure.erl ├── mtp_session_storage.erl ├── mtproto_proxy.app.src ├── mtproto_proxy_app.erl └── mtproto_proxy_sup.erl ├── start.sh └── test ├── bench_codec_decode.erl ├── bench_codec_encode.erl ├── mtp_prop_gen.erl ├── mtp_test_client.erl ├── mtp_test_cmd_rpc.erl ├── mtp_test_datacenter.erl ├── mtp_test_echo_rpc.erl ├── mtp_test_metric.erl ├── mtp_test_middle_server.erl ├── prop_mtp_abridged.erl ├── prop_mtp_aes_cbc.erl ├── prop_mtp_codec.erl ├── prop_mtp_fake_tls.erl ├── prop_mtp_full.erl ├── prop_mtp_intermediate.erl ├── prop_mtp_obfuscated.erl ├── prop_mtp_rpc.erl ├── prop_mtp_statefull.erl ├── single_dc_SUITE.erl └── test-sys.config /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org/ 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | [*.{erl,hrl}] 9 | indent_style = space 10 | indent_size = 4 11 | max_line_length = 100 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: seriyps 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # install: 2 | # - sudo sysctl -w net.ipv6.conf.all.disable_ipv6=0 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | jobs: 14 | test: 15 | name: OTP-${{matrix.otp}}, OS-${{matrix.os}} 16 | runs-on: ${{matrix.os}} 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | os: 21 | - "ubuntu-20.04" 22 | rebar3: ["3.20.0"] 23 | otp: 24 | - "26.2" 25 | - "25.3" 26 | - "24.3" 27 | include: 28 | - otp: "23.3" 29 | rebar3: "3.18.0" 30 | os: "ubuntu-20.04" 31 | env: 32 | SHELL: /bin/sh # needed for erlexec 33 | steps: 34 | - uses: actions/checkout@v2 35 | 36 | - uses: erlef/setup-beam@v1 37 | with: 38 | otp-version: ${{matrix.otp}} 39 | rebar3-version: ${{matrix.rebar3}} 40 | 41 | - name: compile 42 | run: rebar3 compile 43 | 44 | - name: xref 45 | run: rebar3 xref 46 | 47 | - name: eunit 48 | run: rebar3 eunit 49 | 50 | - name: ct 51 | run: rebar3 ct 52 | 53 | - name: dialyzer 54 | run: rebar3 dialyzer 55 | 56 | - name: proper 57 | run: rebar3 proper 58 | 59 | - name: Upload test logs artifact 60 | uses: actions/upload-artifact@v4 61 | if: failure() 62 | with: 63 | name: test_logs_otp-${{matrix.otp}} 64 | path: | 65 | _build/test/logs/* 66 | !_build/test/logs/ct_run*/datadir 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | logs 15 | _build 16 | .idea 17 | *.iml 18 | rebar3.crashdump 19 | *~ 20 | config/prod* 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/erlang/docker-erlang-example 2 | 3 | FROM erlang:21-alpine as builder 4 | 5 | RUN apk add --no-cache git 6 | 7 | RUN mkdir -p /build/mtproto_proxy 8 | 9 | WORKDIR /build/mtproto_proxy 10 | COPY src src 11 | COPY rebar3 rebar3 12 | COPY rebar.config rebar.config 13 | COPY rebar.lock rebar.lock 14 | COPY config config 15 | RUN if [ ! -f config/prod-sys.config ]; then cp config/sys.config.example config/prod-sys.config; fi 16 | RUN if [ ! -f config/prod-vm.args ]; then cp config/vm.args.example config/prod-vm.args; fi 17 | 18 | RUN rebar3 as prod release 19 | 20 | FROM alpine:3.9 21 | RUN apk add --no-cache openssl && \ 22 | apk add --no-cache ncurses-libs && \ 23 | apk add --no-cache dumb-init 24 | 25 | RUN mkdir -p /opt /var/log/mtproto-proxy 26 | COPY start.sh /bin/start.sh 27 | COPY --from=builder /build/mtproto_proxy/_build/prod/rel/mtp_proxy /opt/mtp_proxy 28 | 29 | ENTRYPOINT ["/usr/bin/dumb-init", "--", "/bin/start.sh"] 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DESTDIR:= 2 | prefix:=$(DESTDIR)/opt 3 | REBAR3:=./rebar3 4 | SERVICE:=$(DESTDIR)/etc/systemd/system/mtproto-proxy.service 5 | EPMD_SERVICE:=$(DESTDIR)/etc/systemd/system/epmd.service 6 | LOGDIR:=$(DESTDIR)/var/log/mtproto-proxy 7 | USER:=mtproto-proxy 8 | 9 | 10 | all: config/prod-sys.config config/prod-vm.args 11 | $(REBAR3) as prod release 12 | 13 | .PHONY: test 14 | test: 15 | $(REBAR3) xref 16 | $(REBAR3) eunit -c 17 | $(REBAR3) ct -c 18 | $(REBAR3) proper -c -n 50 19 | $(REBAR3) dialyzer 20 | $(REBAR3) cover -v 21 | 22 | config/prod-sys.config: config/sys.config.example 23 | [ -f $@ ] && diff -u $@ $^ || true 24 | cp -i -b $^ $@ 25 | config/prod-vm.args: config/vm.args.example 26 | [ -f $@ ] && diff -u $@ $^ || true 27 | cp -i -b $^ $@ 28 | @IP=$(shell curl -s -4 -m 10 http://ip.seriyps.com || curl -s -4 -m 10 https://digitalresistance.dog/myIp) \ 29 | && sed -i s/@0\.0\.0\.0/@$${IP}/ $@ 30 | 31 | user: 32 | sudo useradd -r $(USER) || true 33 | 34 | $(LOGDIR): 35 | mkdir -p $(LOGDIR)/ 36 | chown $(USER) $(LOGDIR)/ 37 | 38 | 39 | install: user $(LOGDIR) 40 | mkdir -p $(prefix) 41 | cp -r _build/prod/rel/mtp_proxy $(prefix)/ 42 | mkdir -p $(prefix)/mtp_proxy/log/ 43 | chmod 777 $(prefix)/mtp_proxy/log/ 44 | install -D config/mtproto-proxy.service $(SERVICE) 45 | # If there is no "epmd" service, install one 46 | if [ -z "`systemctl show -p FragmentPath epmd | cut -d = -f 2`" ]; then \ 47 | install -D config/epmd.service $(EPMD_SERVICE); \ 48 | fi 49 | systemctl daemon-reload 50 | 51 | .PHONY: update-sysconfig 52 | update-sysconfig: config/prod-sys.config $(prefix)/mtp_proxy 53 | REL_VSN=$(shell cut -d " " -f 2 $(prefix)/mtp_proxy/releases/start_erl.data) && \ 54 | install -m 644 config/prod-sys.config "$(prefix)/mtp_proxy/releases/$${REL_VSN}/sys.config" 55 | 56 | uninstall: 57 | # TODO: ensure service is stopped 58 | rm $(SERVICE) 59 | rm -r $(prefix)/mtp_proxy/ 60 | systemctl daemon-reload 61 | -------------------------------------------------------------------------------- /config/epmd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Erlang Port Mapper Daemon 3 | After=network.target 4 | #Requires=epmd.socket 5 | 6 | [Service] 7 | ExecStart=/usr/bin/epmd 8 | Type=simple 9 | User=daemon 10 | Group=daemon 11 | 12 | [Install] 13 | #Also=epmd.socket 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /config/mtproto-proxy.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | SourcePath=/opt/mtp_proxy/bin/mtp_proxy 3 | Description=Starts the mtproto_proxy server 4 | After=local-fs.target 5 | After=remote-fs.target 6 | After=network-online.target 7 | After=systemd-journald-dev-log.socket 8 | After=nss-lookup.target 9 | Wants=network-online.target 10 | Requires=epmd.service 11 | 12 | [Service] 13 | Type=simple 14 | User=mtproto-proxy 15 | Group=mtproto-proxy 16 | Environment="RUNNER_LOG_DIR=/var/log/mtproto-proxy" 17 | Restart=on-failure 18 | TimeoutSec=1min 19 | IgnoreSIGPIPE=no 20 | KillMode=process 21 | GuessMainPID=no 22 | RemainAfterExit=no 23 | LimitNOFILE=40960 24 | AmbientCapabilities=CAP_NET_BIND_SERVICE 25 | ExecStart=/opt/mtp_proxy/bin/mtp_proxy foreground 26 | ExecStop=/opt/mtp_proxy/bin/mtp_proxy stop 27 | ExecReload=/opt/mtp_proxy/bin/mtp_proxy rpcterms mtproto_proxy_app reload_config 28 | TimeoutStopSec=15s 29 | 30 | [Install] 31 | WantedBy=multi-user.target 32 | -------------------------------------------------------------------------------- /config/sys.config.example: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang -*- 2 | [ 3 | {mtproto_proxy, 4 | %% see src/mtproto_proxy.app.src for examples. 5 | [ 6 | %% PUT YOUR CUSTOM SETTINGS BELOW vvvvv 7 | 8 | %% {ports, 9 | %% [#{name => mtp_handler_1, 10 | %% listen_ip => "0.0.0.0", 11 | %% port => 1443, 12 | %% secret => <<"d0d6e111bada5511fcce9584deadbeef">>, 13 | %% tag => <<"dcbe8f1493fa4cd9ab300891c0b5b326">>} 14 | %% ]} 15 | %% ^^^^^ END 16 | ]}, 17 | 18 | %% Logging config 19 | {lager, 20 | [{log_root, "/var/log/mtproto-proxy"}, 21 | {crash_log, "crash.log"}, 22 | {handlers, 23 | [ 24 | {lager_console_backend, 25 | [{level, critical}]}, 26 | 27 | {lager_file_backend, 28 | [{file, "application.log"}, 29 | {level, info}, 30 | 31 | %% Do fsync only on critical messages 32 | {sync_on, critical}, 33 | %% If we logged more than X messages in a second, flush the rest 34 | {high_water_mark, 300}, 35 | %% If we hit hwm and msg queue len is >X, flush the queue 36 | {flush_queue, true}, 37 | {flush_threshold, 2000}, 38 | %% How often to check if log should be rotated 39 | {check_interval, 5000}, 40 | %% Rotate when file size is 100MB+ 41 | {size, 104857600} 42 | ]} 43 | ]}]}, 44 | {sasl, 45 | [{errlog_type, error}]} 46 | ]. 47 | -------------------------------------------------------------------------------- /config/vm.args.example: -------------------------------------------------------------------------------- 1 | -name mtproto_proxy@0.0.0.0 2 | 3 | -setcookie mtproto-proxy-cookie 4 | 5 | ## this one is essential for >500 connections 6 | +K true 7 | +A 2 8 | +SDio 2 9 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | % -*- mode: erlang -*- 2 | {erl_opts, [debug_info, 3 | {d, 'HUT_LAGER'}, 4 | {parse_transform, lager_transform}]}. 5 | 6 | {deps, [{ranch, "1.7.0"}, 7 | {hut, "1.3.0"}, 8 | {lager, "3.9.1"}, 9 | {erlang_psq, "1.0.0"} 10 | ]}. 11 | {project_plugins, [rebar3_proper, 12 | rebar3_bench] 13 | }. 14 | 15 | {xref_checks, 16 | [undefined_function_calls, 17 | undefined_functions, 18 | locals_not_used, 19 | %% exports_not_used, 20 | deprecated_function_calls, 21 | deprecated_functions 22 | ]}. 23 | 24 | {relx, [{release, { mtp_proxy, "0.1.0" }, 25 | [lager, 26 | ranch, 27 | mtproto_proxy, 28 | sasl]}, 29 | 30 | %% {sys_config, "./config/sys.config"}, 31 | %% {vm_args, "./config/vm.args"}, 32 | 33 | {include_erts, false}, 34 | {extended_start_script, true}] 35 | }. 36 | 37 | {profiles, 38 | [{prod, 39 | [{relx, [{dev_mode, false}, 40 | {sys_config, "./config/prod-sys.config"}, 41 | {vm_args, "./config/prod-vm.args"}, 42 | {include_erts, true}]}] 43 | }, 44 | {test, 45 | [{deps, 46 | [{proper, "1.3.0"}]}, 47 | {ct_opts, [{sys_config, ["./test/test-sys.config"]}]}, 48 | {relx, 49 | [{sys_config, "./test/test-sys.config"}]} 50 | ]}] 51 | }. 52 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.2.0", 2 | [{<<"erlang_psq">>,{pkg,<<"erlang_psq">>,<<"1.0.0">>},0}, 3 | {<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},1}, 4 | {<<"hut">>,{pkg,<<"hut">>,<<"1.3.0">>},0}, 5 | {<<"lager">>,{pkg,<<"lager">>,<<"3.9.1">>},0}, 6 | {<<"ranch">>,{pkg,<<"ranch">>,<<"1.7.0">>},0}]}. 7 | [ 8 | {pkg_hash,[ 9 | {<<"erlang_psq">>, <<"995E328461A5949A54BDFC7686609A08EFB82313914F9AEAD494A2644629EA26">>}, 10 | {<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>}, 11 | {<<"hut">>, <<"71F2F054E657C03F959CF1ACC43F436EA87580696528CA2A55C8AFB1B06C85E7">>}, 12 | {<<"lager">>, <<"5885BC71308CD38F9D025C8ECDE4E5CCE1CE8565F80BFC6199865C845D6DBE95">>}, 13 | {<<"ranch">>, <<"9583F47160CA62AF7F8D5DB11454068EAA32B56EEADF984D4F46E61A076DF5F2">>}]}, 14 | {pkg_hash_ext,[ 15 | {<<"erlang_psq">>, <<"03DA24C3AA84313D57603B6A4B51EB46B4B787FA95BF5668D03E101A466DDFB2">>}, 16 | {<<"goldrush">>, <<"99CB4128CFFCB3227581E5D4D803D5413FA643F4EB96523F77D9E6937D994CEB">>}, 17 | {<<"hut">>, <<"7E15D28555D8A1F2B5A3A931EC120AF0753E4853A4C66053DB354F35BF9AB563">>}, 18 | {<<"lager">>, <<"3F59BA75A04A99E5F18BF91C89F46DCE536F83C6CB415FE26E6E75A62BEF37DC">>}, 19 | {<<"ranch">>, <<"59F7501C3A56125B2FC5684C3048FAC9D043C0BF4D173941B12CA927949AF189">>}]} 20 | ]. 21 | -------------------------------------------------------------------------------- /rebar3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seriyps/mtproto_proxy/f9c2d32d4f4ed70621ad7617ebbd9d6180a08fb2/rebar3 -------------------------------------------------------------------------------- /src/gen_timeout.erl: -------------------------------------------------------------------------------- 1 | %%% @author Sergey Prokhorov 2 | %%% @copyright (C) 2018, Sergey Prokhorov 3 | %%% @doc 4 | %%% 5 | %%% @end 6 | %%% Created : 9 Apr 2018 by Sergey Prokhorov 7 | 8 | -module(gen_timeout). 9 | 10 | -export([new/1, 11 | set_timeout/2, 12 | bump/1, 13 | reset/1, 14 | is_expired/1, 15 | time_to_message/1, 16 | time_left/1]). 17 | -export([upgrade/1]). 18 | -export_type([tout/0, opts/0]). 19 | 20 | -record(timeout, 21 | {ref :: reference() | undefined, 22 | last_bump :: integer(), 23 | message :: any(), 24 | unit = second :: erlang:time_unit(), 25 | timeout :: timeout_type()}). 26 | 27 | -type timeout_type() :: 28 | non_neg_integer() 29 | | {env, App :: atom(), Name :: atom(), Default :: non_neg_integer()}. 30 | 31 | -type opts() :: #{message => any(), 32 | unit => erlang:time_unit(), 33 | timeout := timeout_type()}. 34 | 35 | -opaque tout() :: #timeout{}. 36 | 37 | -define(MS_PER_SEC, 1000). 38 | 39 | -spec new(opts()) -> tout(). 40 | new(Opts) -> 41 | Default = #{message => timeout, 42 | unit => second}, 43 | #{message := Message, 44 | timeout := Timeout, 45 | unit := Unit} = maps:merge(Default, Opts), 46 | %% TODO: get rid of 2 system_time/1 calls in `new + reset` 47 | reset(#timeout{message = Message, 48 | unit = Unit, 49 | last_bump = erlang:system_time(Unit), 50 | timeout = Timeout}). 51 | 52 | -spec set_timeout(timeout_type(), tout()) -> tout(). 53 | set_timeout(Timeout, S) -> 54 | reset(S#timeout{timeout = Timeout}). 55 | 56 | -spec bump(tout()) -> tout(). 57 | bump(#timeout{unit = Unit} = S) -> 58 | S#timeout{last_bump = erlang:system_time(Unit)}. 59 | 60 | -spec reset(tout()) -> tout(). 61 | reset(#timeout{ref = Ref, message = Message, unit = Unit} = S) -> 62 | (is_reference(Ref)) 63 | andalso erlang:cancel_timer(Ref), 64 | SendAfter = max(time_left(S), 0), 65 | After = erlang:convert_time_unit(SendAfter, Unit, millisecond), 66 | Ref1 = erlang:send_after(After, self(), Message), 67 | S#timeout{ref = Ref1}. 68 | 69 | -spec is_expired(tout()) -> boolean(). 70 | is_expired(S) -> 71 | time_left(S) =< 0. 72 | 73 | -spec time_to_message(tout()) -> non_neg_integer() | false. 74 | time_to_message(#timeout{ref = Ref}) -> 75 | erlang:read_timer(Ref). 76 | 77 | -spec time_left(tout()) -> integer(). 78 | time_left(#timeout{last_bump = LastBump, unit = Unit} = S) -> 79 | Timeout = get_timeout(S), 80 | Now = erlang:system_time(Unit), 81 | ExpiresAt = LastBump + Timeout, 82 | ExpiresAt - Now. 83 | 84 | upgrade({timeout, Ref, LastBump, Message, Timeout}) -> 85 | Timeout1 = case Timeout of 86 | {sec, Val} -> Val; 87 | _ -> Timeout 88 | end, 89 | #timeout{ref = Ref, 90 | last_bump = LastBump, 91 | message = Message, 92 | timeout = Timeout1, 93 | unit = second}. 94 | 95 | %% Internal 96 | 97 | get_timeout(#timeout{timeout = {env, App, Name, Default}}) -> 98 | application:get_env(App, Name, Default); 99 | get_timeout(#timeout{timeout = Sec}) -> 100 | Sec. 101 | 102 | -ifdef(TEST). 103 | -include_lib("eunit/include/eunit.hrl"). 104 | 105 | new_expire_test() -> 106 | T = new(#{timeout => 100, 107 | unit => millisecond, 108 | message => ?FUNCTION_NAME}), 109 | ?assertNot(is_expired(T)), 110 | ?assert(time_left(T) > 0), 111 | ?assert(time_to_message(T) > 0), 112 | ok= recv(?FUNCTION_NAME), 113 | ?assert(time_left(T) =< 0), 114 | ?assert(is_expired(T)). 115 | 116 | reset_test() -> 117 | T = new(#{timeout => 100, 118 | unit => millisecond, 119 | message => ?FUNCTION_NAME}), 120 | ?assertNot(is_expired(T)), 121 | T1 = reset(T), 122 | ?assertNot(is_expired(T1)), 123 | ok = recv(?FUNCTION_NAME), 124 | ?assert(is_expired(T1)). 125 | 126 | bump_test() -> 127 | T = new(#{timeout => 1000, 128 | unit => millisecond, 129 | message => ?FUNCTION_NAME}), 130 | ?assertNot(is_expired(T)), 131 | TimeToMessage0 = time_to_message(T), 132 | timer:sleep(600), 133 | T1 = bump(T), 134 | ?assert((TimeToMessage0 - 600) >= time_to_message(T1), 135 | "Bump doesn't affect timer message"), 136 | timer:sleep(500), 137 | %% Got message, but not yet expired 138 | ?assertEqual(false, time_to_message(T1)), 139 | ?assertNot(is_expired(T1)), 140 | ok = recv(?FUNCTION_NAME), 141 | ?assertNot(is_expired(T1)), 142 | T2 = reset(T1), 143 | ok = recv(?FUNCTION_NAME), 144 | ?assert(is_expired(T2)). 145 | 146 | recv(What) -> 147 | receive What -> ok 148 | after 5000 -> 149 | error({timeout, What}) 150 | end. 151 | 152 | -endif. 153 | -------------------------------------------------------------------------------- /src/mtp_abridged.erl: -------------------------------------------------------------------------------- 1 | %%% @author Sergey 2 | %%% @copyright (C) 2018, Sergey 3 | %%% @doc 4 | %%% MTProto abridged packet format codec 5 | %%% @end 6 | %%% Created : 29 May 2018 by Sergey 7 | 8 | -module(mtp_abridged). 9 | -behaviour(mtp_codec). 10 | 11 | -export([new/0, 12 | try_decode_packet/2, 13 | encode_packet/2]). 14 | -export_type([codec/0]). 15 | 16 | -dialyzer(no_improper_lists). 17 | 18 | -record(st, 19 | {}). 20 | -define(MAX_PACKET_SIZE, 1 * 1024 * 1024). % 1mb 21 | -define(APP, mtproto_proxy). 22 | 23 | -opaque codec() :: #st{}. 24 | 25 | new() -> 26 | #st{}. 27 | 28 | -spec try_decode_packet(binary(), codec()) -> {ok, binary(), binary(), codec()} 29 | | {incomplete, codec()}. 30 | try_decode_packet(<>, 31 | #st{} = St) when Flag == 127; Flag == 255 -> 32 | Len1 = Len * 4, 33 | try_decode_packet_len(Len1, Rest, St); 34 | try_decode_packet(<>, 35 | #st{} = St) when Len >= 128 -> 36 | Len1 = (Len - 128) * 4, 37 | try_decode_packet_len(Len1, Rest, St); 38 | try_decode_packet(<>, 39 | #st{} = St) when Len < 127 -> 40 | Len1 = Len * 4, 41 | try_decode_packet_len(Len1, Rest, St); 42 | try_decode_packet(_, St) -> 43 | {incomplete, St}. 44 | 45 | try_decode_packet_len(Len, LenStripped, St) -> 46 | (Len < ?MAX_PACKET_SIZE) 47 | orelse error({protocol_error, abridged_max_size, Len}), 48 | case LenStripped of 49 | <> -> 50 | {ok, Packet, Rest, St}; 51 | _ -> 52 | {incomplete, St} 53 | end. 54 | 55 | -spec encode_packet(iodata(), codec()) -> {iodata(), codec()}. 56 | encode_packet(Data, St) -> 57 | Size = iolist_size(Data), 58 | Len = Size div 4, 59 | Packet = 60 | case Len < 127 of 61 | true -> 62 | [Len | Data]; 63 | false -> 64 | [<<127, Len:24/unsigned-little-integer>> | Data] 65 | end, 66 | {Packet, St}. 67 | 68 | -ifdef(TEST). 69 | -include_lib("eunit/include/eunit.hrl"). 70 | 71 | decode_none_test() -> 72 | S = new(), 73 | ?assertEqual( 74 | {incomplete, S}, try_decode_packet(<<>>, S)). 75 | 76 | -endif. 77 | -------------------------------------------------------------------------------- /src/mtp_aes_cbc.erl: -------------------------------------------------------------------------------- 1 | %%% @author Sergey 2 | %%% @copyright (C) 2018, Sergey 3 | %%% @doc 4 | %%% Block CBC AES codec with buffered decoding 5 | %%% @end 6 | %%% Created : 6 Jun 2018 by Sergey 7 | 8 | -module(mtp_aes_cbc). 9 | -behaviour(mtp_codec). 10 | 11 | -export([new/5, 12 | encrypt/2, 13 | decrypt/2, 14 | try_decode_packet/2, 15 | encode_packet/2 16 | ]). 17 | 18 | -export_type([codec/0]). 19 | 20 | -record(baes_st, 21 | {block_size :: pos_integer(), 22 | encrypt :: any(), % aes state 23 | decrypt :: any() % aes state 24 | }). 25 | 26 | -opaque codec() :: #baes_st{}. 27 | 28 | 29 | 30 | new(EncKey, EncIv, DecKey, DecIv, BlockSize) -> 31 | #baes_st{ 32 | block_size = BlockSize, 33 | encrypt = cbc_init(EncKey, EncIv, true), 34 | decrypt = cbc_init(DecKey, DecIv, false) 35 | }. 36 | 37 | -spec encrypt(iodata(), codec()) -> {binary(), codec()}. 38 | encrypt(Data, #baes_st{block_size = BSize, 39 | encrypt = Enc} = S) -> 40 | ((iolist_size(Data) rem BSize) == 0) 41 | orelse error({data_not_aligned, BSize, byte_size(Data)}), 42 | {Enc1, Encrypted} = cbc_encrypt(Enc, Data), 43 | {Encrypted, S#baes_st{encrypt = Enc1}}. 44 | 45 | 46 | -spec decrypt(binary(), codec()) -> {Data :: binary(), Tail :: binary(), codec()}. 47 | decrypt(Data, #baes_st{block_size = BSize} = S) -> 48 | Size = byte_size(Data), 49 | Div = Size div BSize, 50 | Rem = Size rem BSize, 51 | case {Div, Rem} of 52 | {0, _} -> 53 | %% Not enough bytes 54 | {<<>>, Data, S}; 55 | {_, 0} -> 56 | %% Aligned 57 | do_decrypt(Data, <<>>, S); 58 | {_, Tail} -> 59 | %% N blocks + reminder 60 | Head = Size - Tail, 61 | <> = Data, 62 | do_decrypt(ToDecode, Reminder, S) 63 | end. 64 | 65 | do_decrypt(Data, Tail, #baes_st{decrypt = Dec} = S) -> 66 | {Dec1, Decrypted} = cbc_decrypt(Dec, Data), 67 | {Decrypted, Tail, S#baes_st{decrypt = Dec1}}. 68 | 69 | try_decode_packet(Bin, S) -> 70 | case decrypt(Bin, S) of 71 | {<<>>, _Tail, S1} -> 72 | {incomplete, S1}; 73 | {Dec, Tail, S1} -> 74 | {ok, Dec, Tail, S1} 75 | end. 76 | 77 | encode_packet(Bin, S) -> 78 | encrypt(Bin, S). 79 | 80 | -if(?OTP_RELEASE >= 23). 81 | cbc_init(Key, IV, IsEncrypt) -> 82 | crypto:crypto_init(aes_256_cbc, Key, IV, [{encrypt, IsEncrypt}]). 83 | 84 | cbc_encrypt(State, Data) -> 85 | %% Assuming state was created with {encrypt, true} 86 | {State, crypto:crypto_update(State, Data)}. 87 | 88 | cbc_decrypt(State, Data) -> 89 | %% Assuming state was created with {encrypt, false} 90 | {State, crypto:crypto_update(State, Data)}. 91 | -else. 92 | cbc_init(Key, IV, _IsEncrypt) -> 93 | {Key, IV}. 94 | 95 | cbc_encrypt({EncKey, EncIv}, Data) -> 96 | Encrypted = crypto:block_encrypt(aes_cbc, EncKey, EncIv, Data), 97 | {{EncKey, crypto:next_iv(aes_cbc, Encrypted)}, Encrypted}. 98 | 99 | cbc_decrypt({DecKey, DecIv}, Data) -> 100 | Decrypted = crypto:block_decrypt(aes_cbc, DecKey, DecIv, Data), 101 | NewDecIv = crypto:next_iv(aes_cbc, Data), 102 | {{DecKey, NewDecIv}, Decrypted}. 103 | 104 | -endif. 105 | 106 | 107 | -ifdef(TEST). 108 | -include_lib("eunit/include/eunit.hrl"). 109 | 110 | decode_none_test() -> 111 | DecKey = <<21,211,191,127,143,222,184,152,232,213,25,173,243,163,243,224,133,131,199,13,206,156,146,141,67,172,85,114,190,203,176,215>>, 112 | DecIV = <<9,156,175,247,37,161,219,155,52,115,93,76,122,195,158,194>>, 113 | S = new(DecKey, DecIV, DecKey, DecIV, 16), 114 | ?assertEqual( 115 | {incomplete, S}, try_decode_packet(<<>>, S)). 116 | 117 | decrypt_test() -> 118 | DecKey = <<21,211,191,127,143,222,184,152,232,213,25,173,243,163,243,224,133,131,199,13,206,156,146,141,67,172,85,114,190,203,176,215>>, 119 | DecIV = <<9,156,175,247,37,161,219,155,52,115,93,76,122,195,158,194>>, 120 | S = new(DecKey, DecIV, DecKey, DecIV, 16), 121 | Samples = 122 | [{<<36,170,147,95,53,27,44,255,252,105,70,8,90,40,77,226>>, 123 | <<44,0,0,0,255,255,255,255,245,238,130,118,0,0,0,0>>}, 124 | {<<137,187,80,238,110,142,52,130,119,140,210,138,13,72,169,144,63,167,172,19,161,13,231,169,237,34,203,240,8,135,67,29>>, 125 | <<134,153,66,10,95,9,134,49,221,133,21,91,73,80,73,80,80,82,80,68,84,73,77,69,133,250,76,84,4,0,0,0>>} 126 | ], 127 | lists:foldl( 128 | fun({In, Out}, S1) -> 129 | {Dec, <<>>, S2} = decrypt(In, S1), 130 | ?assertEqual(Out, Dec), 131 | S2 132 | end, S, Samples). 133 | 134 | encrypt_test() -> 135 | EncKey = <<89,84,72,247,172,56,204,11,10,242,143,240,111,53,33,162,221,141,148,243,100,21,167,160,132,99,61,189,128,73,138,89>>, 136 | EncIV = <<248,195,42,53,235,104,78,225,84,171,182,125,18,192,251,77>>, 137 | S = new(EncKey, EncIV, EncKey, EncIV, 16), 138 | Samples = 139 | [{<<44,0,0,0,255,255,255,255,245,238,130,118,0,0,0,0,73,80,73,80,80,82,80,68,84,73,77,69,73,80,73,80,80,82,80,68,84,73,77,69,2,118,29,129,4,0,0,0>>, 140 | <<161,206,198,191,175,240,48,162,245,192,234,210,104,195,161,214,55,147,145,157,174,33,243,198,84,188,29,201,116,128,185,149,73,241,149,122,244,193,59,112,153,188,141,134,90,24,75,216>>}, 141 | {<<136,0,0,0,0,0,0,0,238,241,206,54,8,16,2,64,195,43,106,127,211,218,156,102,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,65,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,24,0,0,0,174,38,30,219,16,220,190,143,20,147,250,76,217,171,48,8,145,192,181,179,38,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,56,220,23,91,20,0,0,0,120,151,70,96,26,49,96,74,221,243,104,13,173,13,132,192,238,22,97,126,247,233,151,22,4,0,0,0,4,0,0,0>>, 142 | <<92,173,139,247,1,147,48,108,162,98,125,215,170,185,87,131,65,26,90,205,43,54,115,216,90,101,3,188,151,165,126,144,104,247,57,65,32,107,245,154,77,194,161,157,63,232,169,68,113,64,96,197,10,209,66,117,251,15,10,141,248,122,40,242,195,38,196,237,68,132,189,49,102,53,31,139,56,64,213,107,79,105,210,182,157,73,203,105,165,134,163,116,49,94,143,171,88,132,84,123,196,38,35,53,220,182,232,199,92,29,182,129,239,116,252,31,72,29,120,203,57,49,46,129,142,94,204,121,21,113,211,10,193,126,180,227,139,40,85,223,134,124,152,81>>}], 143 | lists:foldl( 144 | fun({In, Out}, S1) -> 145 | {Enc, S2} = encrypt(In, S1), 146 | ?assertEqual(Out, Enc), 147 | S2 148 | end, S, Samples). 149 | 150 | -endif. 151 | -------------------------------------------------------------------------------- /src/mtp_codec.erl: -------------------------------------------------------------------------------- 1 | %%% @author Sergey 2 | %%% @copyright (C) 2018, Sergey 3 | %%% @doc 4 | %%% This module provieds a combination of crypto and packet codecs. 5 | %%% Crypto is always outer layer and packet is inner: 6 | %%% ( --- packet --- ) 7 | %%% (-- crypto --) 8 | %%% - tcp - 9 | %%% @end 10 | %%% Created : 6 Jun 2018 by Sergey 11 | 12 | -module(mtp_codec). 13 | 14 | -export([new/4, new/7, 15 | info/2, replace/4, push_back/3, is_empty/1, 16 | try_decode_packet/2, 17 | encode_packet/2, 18 | fold_packets/4, fold_packets_if/4]). 19 | -export_type([codec/0, packet_codec/0]). 20 | 21 | -type state() :: any(). 22 | -type crypto_codec() :: mtp_aes_cbc 23 | | mtp_obfuscated 24 | | mtp_noop_codec. 25 | -type packet_codec() :: mtp_abridged 26 | | mtp_full 27 | | mtp_intermediate 28 | | mtp_secure 29 | | mtp_noop_codec. 30 | -type layer() :: tls | crypto | packet. 31 | 32 | -define(MAX_BUFS_SIZE, 2 * 1024 * 1024). 33 | 34 | -record(codec, 35 | {have_tls :: boolean(), 36 | tls_state :: mtp_fake_tls:codec() | undefined, 37 | tls_buf = <<>> :: binary(), 38 | crypto_mod :: crypto_codec(), 39 | crypto_state :: any(), 40 | crypto_buf = <<>> :: binary(), 41 | packet_mod :: packet_codec(), 42 | packet_state :: any(), 43 | packet_buf = <<>> :: binary(), 44 | limit = ?MAX_BUFS_SIZE :: pos_integer()}). 45 | 46 | -define(APP, mtproto_proxy). 47 | 48 | 49 | -callback try_decode_packet(binary(), state()) -> 50 | {ok, Packet :: binary(), Tail :: binary(), state()} 51 | | {incomplete, state()}. 52 | 53 | -callback encode_packet(iodata(), state()) -> 54 | {iodata(), state()}. 55 | 56 | -opaque codec() :: #codec{}. 57 | 58 | new(CryptoMod, CryptoState, PacketMod, PacketState) -> 59 | new(CryptoMod, CryptoState, PacketMod, PacketState, false, undefined, ?MAX_BUFS_SIZE). 60 | 61 | -spec new(crypto_codec(), state(), packet_codec(), state(), boolean(), any(), pos_integer()) -> codec(). 62 | new(CryptoMod, CryptoState, PacketMod, PacketState, UseTls, TlsState, Limit) -> 63 | #codec{have_tls = UseTls, 64 | tls_state = TlsState, 65 | crypto_mod = CryptoMod, 66 | crypto_state = CryptoState, 67 | packet_mod = PacketMod, 68 | packet_state = PacketState, 69 | limit = Limit}. 70 | 71 | -spec replace(layer(), module() | boolean(), any(), codec()) -> codec(). 72 | replace(tls, HaveTls, St, #codec{tls_buf = <<>>} = Codec) -> 73 | Codec#codec{have_tls = HaveTls, tls_state = St}; 74 | replace(crypto, Mod, St, #codec{crypto_buf = <<>>} = Codec) -> 75 | Codec#codec{crypto_mod = Mod, crypto_state = St}; 76 | replace(packet, Mod, St, #codec{packet_buf = <<>>} = Codec) -> 77 | Codec#codec{packet_mod = Mod, packet_state = St}. 78 | 79 | 80 | -spec info(layer(), codec()) -> {module() | boolean(), state()}. 81 | info(tls, #codec{have_tls = HaveTls, tls_state = TlsState}) -> 82 | {HaveTls, TlsState}; 83 | info(crypto, #codec{crypto_mod = CryptoMod, crypto_state = CryptoState}) -> 84 | {CryptoMod, CryptoState}; 85 | info(packet, #codec{packet_mod = PacketMod, packet_state = PacketState}) -> 86 | {PacketMod, PacketState}. 87 | 88 | 89 | %% @doc Push already produced data back to one of codec's input buffers 90 | -spec push_back(layer() | first, binary(), codec()) -> codec(). 91 | push_back(tls, Data, #codec{tls_buf = Buf} = Codec) -> 92 | assert_overflow(Codec#codec{tls_buf = <>}); 93 | push_back(crypto, Data, #codec{crypto_buf = Buf} = Codec) -> 94 | assert_overflow(Codec#codec{crypto_buf = <>}); 95 | push_back(packet, Data, #codec{packet_buf = Buf} = Codec) -> 96 | assert_overflow(Codec#codec{packet_buf = <>}); 97 | push_back(first, Data, #codec{have_tls = HaveTls} = Codec) -> 98 | Destination = 99 | case HaveTls of 100 | true -> tls; 101 | false -> crypto 102 | end, 103 | push_back(Destination, Data, Codec). 104 | 105 | 106 | 107 | -spec try_decode_packet(binary(), codec()) -> {ok, binary(), codec()} | {incomplete, codec()}. 108 | try_decode_packet(Bin, S) -> 109 | decode_tls(Bin, S). 110 | 111 | decode_tls(Bin, #codec{have_tls = false} = S) -> 112 | decode_crypto(Bin, S); 113 | decode_tls(<<>>, #codec{tls_buf = <<>>} = S) -> 114 | decode_crypto(<<>>, S); 115 | decode_tls(Bin, #codec{tls_state = TlsSt, tls_buf = <<>>} = S) -> 116 | {DecIolist, Tail, TlsSt1} = mtp_fake_tls:decode_all(Bin, TlsSt), 117 | decode_crypto(iolist_to_binary(DecIolist), assert_overflow(S#codec{tls_state = TlsSt1, tls_buf = Tail})); 118 | decode_tls(Bin, #codec{tls_buf = Buf} = S) -> 119 | decode_tls(<>, S#codec{tls_buf = <<>>}). 120 | 121 | 122 | decode_crypto(<<>>, #codec{crypto_state = CS, crypto_buf = <<>>} = S) -> 123 | %% There might be smth in packet buffer 124 | decode_packet(<<>>, CS, <<>>, S); 125 | decode_crypto(Bin, #codec{crypto_mod = CryptoMod, 126 | crypto_state = CryptoSt, 127 | crypto_buf = <<>>} = S) -> 128 | case CryptoMod:try_decode_packet(Bin, CryptoSt) of 129 | {incomplete, CryptoSt1} -> 130 | decode_packet(<<>>, CryptoSt1, Bin, S); 131 | {ok, Dec1, Tail1, CryptoSt1} -> 132 | decode_packet(Dec1, CryptoSt1, Tail1, S) 133 | end; 134 | decode_crypto(Bin, #codec{crypto_buf = Buf} = S) -> 135 | decode_crypto(<>, S#codec{crypto_buf = <<>>}). 136 | 137 | 138 | decode_packet(<<>>, CryptoSt, CryptoTail, #codec{packet_buf = <<>>} = S) -> 139 | %% Crypto produced nothing and there is nothing in packet buf 140 | {incomplete, assert_overflow(S#codec{crypto_state = CryptoSt, crypto_buf = CryptoTail})}; 141 | decode_packet(Bin, CryptoSt, CryptoTail, #codec{packet_mod = PacketMod, 142 | packet_state = PacketSt, 143 | packet_buf = <<>>} = S) -> 144 | %% Crypto produced smth, and there is nothing in pkt buf 145 | case PacketMod:try_decode_packet(Bin, PacketSt) of 146 | {incomplete, PacketSt1} -> 147 | {incomplete, assert_overflow( 148 | S#codec{crypto_state = CryptoSt, 149 | crypto_buf = CryptoTail, 150 | packet_state = PacketSt1, 151 | packet_buf = Bin 152 | })}; 153 | {ok, Dec2, Tail, PacketSt1} -> 154 | {ok, Dec2, assert_overflow( 155 | S#codec{crypto_state = CryptoSt, 156 | crypto_buf = CryptoTail, 157 | packet_state = PacketSt1, 158 | packet_buf = Tail})} 159 | end; 160 | decode_packet(Bin, CSt, CTail, #codec{packet_buf = Buf} = S) -> 161 | decode_packet(<>, CSt, CTail, S#codec{packet_buf = <<>>}). 162 | 163 | 164 | %% encode_packet(Outer) |> encode_packet(Inner) 165 | -spec encode_packet(iodata(), codec()) -> {iodata(), codec()}. 166 | encode_packet(Bin, #codec{have_tls = HaveTls, 167 | tls_state = TlsSt, 168 | packet_mod = PacketMod, 169 | packet_state = PacketSt, 170 | crypto_mod = CryptoMod, 171 | crypto_state = CryptoSt} = S) -> 172 | {Enc1, PacketSt1} = PacketMod:encode_packet(Bin, PacketSt), 173 | {Enc2, CryptoSt1} = CryptoMod:encode_packet(Enc1, CryptoSt), 174 | case HaveTls of 175 | false -> 176 | {Enc2, S#codec{crypto_state = CryptoSt1, packet_state = PacketSt1}}; 177 | true -> 178 | {Enc3, TlsSt1} = mtp_fake_tls:encode_packet(Enc2, TlsSt), 179 | {Enc3, S#codec{crypto_state = CryptoSt1, packet_state = PacketSt1, tls_state = TlsSt1}} 180 | end. 181 | 182 | 183 | -spec fold_packets(fun( (binary(), FoldSt, codec()) -> {FoldSt, codec()} ), 184 | FoldSt, binary(), codec()) -> 185 | {ok, FoldSt, codec()} 186 | when 187 | FoldSt :: any(). 188 | fold_packets(Fun, FoldSt, Data, Codec) -> 189 | case try_decode_packet(Data, Codec) of 190 | {ok, Decoded, Codec1} -> 191 | {FoldSt1, Codec2} = Fun(Decoded, FoldSt, Codec1), 192 | fold_packets(Fun, FoldSt1, <<>>, Codec2); 193 | {incomplete, Codec1} -> 194 | {ok, FoldSt, Codec1} 195 | end. 196 | 197 | -spec fold_packets_if(fun( (binary(), FoldSt, codec()) -> {next | stop, FoldSt, codec()} ), 198 | FoldSt, binary(), codec()) -> 199 | {ok, FoldSt, codec()} 200 | when 201 | FoldSt :: any(). 202 | fold_packets_if(Fun, FoldSt0, Data, Codec0) -> 203 | case try_decode_packet(Data, Codec0) of 204 | {ok, Decoded, Codec1} -> 205 | case Fun(Decoded, FoldSt0, Codec1) of 206 | {next, FoldSt1, Codec2} -> 207 | fold_packets(Fun, FoldSt1, <<>>, Codec2); 208 | {stop, FoldSt1, Codec2} -> 209 | {ok, FoldSt1, Codec2} 210 | end; 211 | {incomplete, Codec1} -> 212 | {ok, FoldSt0, Codec1} 213 | end. 214 | 215 | -spec is_empty(codec()) -> boolean(). 216 | is_empty(#codec{packet_buf = <<>>, crypto_buf = <<>>, tls_buf = <<>>}) -> true; 217 | is_empty(_) -> false. 218 | 219 | assert_overflow(#codec{packet_buf = PB, crypto_buf = CB, tls_buf = TB, limit = Limit} = Codec) -> 220 | Size = byte_size(PB) + byte_size(CB) + byte_size(TB), 221 | case Size > Limit of 222 | true -> 223 | error({protocol_error, max_buffers_size, Size}); 224 | false -> 225 | Codec 226 | end. 227 | -------------------------------------------------------------------------------- /src/mtp_config.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Sergey 3 | %%% @copyright (C) 2018, Sergey 4 | %%% @doc 5 | %%% Worker that updates datacenter config and proxy secret from 6 | %%% https://core.telegram.org/getProxySecret 7 | %%% and 8 | %%% https://core.telegram.org/getProxyConfig 9 | %%% @end 10 | %%% Created : 10 Jun 2018 by Sergey 11 | %%%------------------------------------------------------------------- 12 | -module(mtp_config). 13 | 14 | -behaviour(gen_server). 15 | 16 | %% API 17 | -export([start_link/0]). 18 | -export([get_downstream_safe/2, 19 | get_downstream_pool/1, 20 | get_netloc/1, 21 | get_netloc_safe/1, 22 | get_secret/0, 23 | status/0, 24 | update/0]). 25 | 26 | %% gen_server callbacks 27 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 28 | terminate/2, code_change/3]). 29 | -export_type([dc_id/0, netloc/0, netloc_v4v6/0]). 30 | 31 | -type dc_id() :: integer(). 32 | -type netloc() :: {inet:ip4_address(), inet:port_number()}. 33 | -type netloc_v4v6() :: {inet:ip_address(), inet:port_number()}. 34 | 35 | -include_lib("hut/include/hut.hrl"). 36 | 37 | -define(TAB, ?MODULE). 38 | -define(IPS_KEY(DcId), {id, DcId}). 39 | -define(IDS_KEY, dc_ids). 40 | -define(SECRET_URL, "https://core.telegram.org/getProxySecret"). 41 | -define(CONFIG_URL, "https://core.telegram.org/getProxyConfig"). 42 | 43 | -define(APP, mtproto_proxy). 44 | 45 | -record(state, {tab :: ets:tid(), 46 | timer :: gen_timeout:tout()}). 47 | 48 | %%%=================================================================== 49 | %%% API 50 | %%%=================================================================== 51 | 52 | start_link() -> 53 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 54 | 55 | -spec get_downstream_safe(dc_id(), mtp_down_conn:upstream_opts()) -> 56 | {dc_id(), pid(), mtp_down_conn:handle()}. 57 | get_downstream_safe(DcId, Opts) -> 58 | case get_downstream_pool(DcId) of 59 | {ok, Pool} -> 60 | case mtp_dc_pool:get(Pool, self(), Opts) of 61 | Downstream when is_pid(Downstream) -> 62 | {DcId, Pool, Downstream}; 63 | {error, empty} -> 64 | %% TODO: maybe sleep and retry? 65 | error({pool_empty, DcId, Pool}) 66 | end; 67 | not_found -> 68 | [{?IDS_KEY, L}] = ets:lookup(?TAB, ?IDS_KEY), 69 | NewDcId = random_choice(L), 70 | get_downstream_safe(NewDcId, Opts) 71 | end. 72 | 73 | get_downstream_pool(DcId) -> 74 | try whereis(mtp_dc_pool:dc_to_pool_name(DcId)) of 75 | undefined -> not_found; 76 | Pid when is_pid(Pid) -> {ok, Pid} 77 | catch error:invalid_dc_id -> 78 | not_found 79 | end. 80 | 81 | -spec get_netloc_safe(dc_id()) -> {dc_id(), netloc()}. 82 | get_netloc_safe(DcId) -> 83 | case get_netloc(DcId) of 84 | {ok, Addr} -> {DcId, Addr}; 85 | not_found -> 86 | [{?IDS_KEY, L}] = ets:lookup(?TAB, ?IDS_KEY), 87 | NewDcId = random_choice(L), 88 | %% Get random DC; it might return 0 and recurse aggain 89 | get_netloc_safe(NewDcId) 90 | end. 91 | 92 | get_netloc(DcId) -> 93 | Key = ?IPS_KEY(DcId), 94 | case ets:lookup(?TAB, Key) of 95 | [] -> 96 | not_found; 97 | [{Key, [{_, _} = IpPort]}] -> 98 | {ok, IpPort}; 99 | [{Key, L}] -> 100 | IpPort = random_choice(L), 101 | {ok, IpPort} 102 | end. 103 | 104 | 105 | -spec get_secret() -> binary(). 106 | get_secret() -> 107 | [{_, Key}] = ets:lookup(?TAB, key), 108 | Key. 109 | 110 | -spec status() -> [mtp_dc_pool:status()]. 111 | status() -> 112 | [{?IDS_KEY, L}] = ets:lookup(?TAB, ?IDS_KEY), 113 | lists:map( 114 | fun(DcId) -> 115 | {ok, Pid} = get_downstream_pool(DcId), 116 | mtp_dc_pool:status(Pid) 117 | end, L). 118 | 119 | 120 | -spec update() -> ok. 121 | update() -> 122 | gen_server:cast(?MODULE, update). 123 | 124 | 125 | %%%=================================================================== 126 | %%% gen_server callbacks 127 | %%%=================================================================== 128 | init([]) -> 129 | Timer = gen_timeout:new( 130 | #{timeout => {env, ?APP, conf_refresh_interval, 3600}, 131 | unit => second}), 132 | Tab = ets:new(?TAB, [set, 133 | public, 134 | named_table, 135 | {read_concurrency, true}]), 136 | State = #state{tab = Tab, 137 | timer = Timer}, 138 | update(State, force), 139 | {ok, State}. 140 | 141 | %%-------------------------------------------------------------------- 142 | handle_call(_Request, _From, State) -> 143 | Reply = ok, 144 | {reply, Reply, State}. 145 | 146 | handle_cast(update, #state{timer = Timer} = State) -> 147 | update(State, soft), 148 | ?log(info, "Config updated"), 149 | Timer1 = gen_timeout:bump( 150 | gen_timeout:reset(Timer)), 151 | {noreply, State#state{timer = Timer1}}. 152 | 153 | handle_info(timeout, #state{timer = Timer} =State) -> 154 | case gen_timeout:is_expired(Timer) of 155 | true -> 156 | update(State, soft), 157 | ?log(info, "Config updated"), 158 | Timer1 = gen_timeout:bump( 159 | gen_timeout:reset(Timer)), 160 | {noreply, State#state{timer = Timer1}}; 161 | false -> 162 | {noreply, State#state{timer = gen_timeout:reset(Timer)}} 163 | end. 164 | terminate(_Reason, _State) -> 165 | ok. 166 | code_change(_OldVsn, State, _Extra) -> 167 | {ok, State}. 168 | 169 | %%%=================================================================== 170 | %%% Internal functions 171 | %%%=================================================================== 172 | 173 | update(#state{tab = Tab}, force) -> 174 | update_ip(), 175 | update_key(Tab), 176 | update_config(Tab); 177 | update(State, _) -> 178 | try update(State, force) 179 | catch Class:Reason:Stack -> 180 | ?log(error, "Err updating proxy settings: ~s", 181 | [lager:pr_stacktrace(Stack, {Class, Reason})]) %XXX lager-specific 182 | end. 183 | 184 | update_key(Tab) -> 185 | Url = application:get_env(mtproto_proxy, proxy_secret_url, ?SECRET_URL), 186 | {ok, Body} = http_get(Url), 187 | true = ets:insert(Tab, {key, list_to_binary(Body)}). 188 | 189 | update_config(Tab) -> 190 | Url = application:get_env(mtproto_proxy, proxy_config_url, ?CONFIG_URL), 191 | {ok, Body} = http_get(Url), 192 | Downstreams = parse_config(Body), 193 | update_downstreams(Downstreams, Tab), 194 | update_ids(Downstreams, Tab). 195 | 196 | parse_config(Body) -> 197 | Lines = string:lexemes(Body, "\n"), 198 | ProxyLines = lists:filter( 199 | fun("proxy_for " ++ _) -> true; 200 | (_) -> false 201 | end, Lines), 202 | [parse_downstream(Line) || Line <- ProxyLines]. 203 | 204 | parse_downstream(Line) -> 205 | ["proxy_for", 206 | DcId, 207 | IpPort] = string:lexemes(Line, " "), 208 | [Ip, PortWithTrailer] = string:split(IpPort, ":", trailing), 209 | Port = list_to_integer(string:trim(PortWithTrailer, trailing, ";")), 210 | {ok, IpAddr} = inet:parse_ipv4strict_address(Ip), 211 | {list_to_integer(DcId), 212 | IpAddr, 213 | Port}. 214 | 215 | update_downstreams(Downstreams, Tab) -> 216 | ByDc = lists:foldl( 217 | fun({DcId, Ip, Port}, Acc) -> 218 | Netlocs = maps:get(DcId, Acc, []), 219 | Acc#{DcId => [{Ip, Port} | Netlocs]} 220 | end, #{}, Downstreams), 221 | [true = ets:insert(Tab, {?IPS_KEY(DcId), Netlocs}) 222 | || {DcId, Netlocs} <- maps:to_list(ByDc)], 223 | lists:foreach( 224 | fun(DcId) -> 225 | case get_downstream_pool(DcId) of 226 | not_found -> 227 | {ok, _Pid} = mtp_dc_pool_sup:start_pool(DcId); 228 | {ok, _} -> 229 | ok 230 | end 231 | end, 232 | maps:keys(ByDc)). 233 | 234 | update_ids(Downstreams, Tab) -> 235 | Ids = lists:usort([DcId || {DcId, _, _} <- Downstreams]), 236 | true = ets:insert(Tab, {?IDS_KEY, Ids}). 237 | 238 | update_ip() -> 239 | case application:get_env(?APP, ip_lookup_services) of 240 | undefined -> false; 241 | {ok, URLs} -> 242 | update_ip(URLs) 243 | end. 244 | 245 | update_ip([Url | Fallbacks]) -> 246 | try 247 | {ok, Body} = http_get(Url), 248 | IpStr= string:trim(Body), 249 | {ok, _} = inet:parse_ipv4strict_address(IpStr), %assert 250 | application:set_env(?APP, external_ip, IpStr) 251 | catch Class:Reason:Stack -> 252 | ?log(error, "Failed to update IP with ~s service: ~s", 253 | [Url, lager:pr_stacktrace(Stack, {Class, Reason})]), %XXX - lager-specific 254 | update_ip(Fallbacks) 255 | end; 256 | update_ip([]) -> 257 | error(ip_lookup_failed). 258 | 259 | -ifdef(OTP_VERSION). 260 | %% XXX: ipfamily only works on OTP >= 20.3.4; see OTP 2dc08b47e6a5ea759781479593c55bb5776cd828 261 | %% Enable it for OTP 21+ for simplicity 262 | -define(OPTS, [{socket_opts, [{ipfamily, inet}]}]). 263 | -else. 264 | -define(OPTS, []). 265 | -endif. 266 | 267 | http_get(Url) -> 268 | {ok, Vsn} = application:get_key(mtproto_proxy, vsn), 269 | OtpVersion = erlang:system_info(otp_release), 270 | UserAgent = ("MTProtoProxy/" ++ Vsn ++ " OTP-" ++ OtpVersion ++ 271 | " (+https://github.com/seriyps/mtproto_proxy)"), 272 | Headers = [{"User-Agent", UserAgent}], 273 | {ok, {{_, 200, _}, _, Body}} = 274 | httpc:request(get, {Url, Headers}, [{timeout, 3000}], ?OPTS), 275 | {ok, Body}. 276 | 277 | random_choice(L) -> 278 | Idx = rand:uniform(length(L)), 279 | lists:nth(Idx, L). 280 | 281 | 282 | -ifdef(TEST). 283 | -include_lib("eunit/include/eunit.hrl"). 284 | 285 | parse_test() -> 286 | Config = ("# force_probability 1 10 287 | proxy_for 1 149.154.175.50:8888; 288 | proxy_for -1 149.154.175.50:8888; 289 | proxy_for 2 149.154.162.39:80; 290 | proxy_for 2 149.154.162.33:80;"), 291 | Expect = [{1, {149, 154, 175, 50}, 8888}, 292 | {-1, {149, 154, 175, 50}, 8888}, 293 | {2, {149, 154, 162, 39}, 80}, 294 | {2, {149, 154, 162, 33},80}], 295 | ?assertEqual(Expect, parse_config(Config)). 296 | 297 | -endif. 298 | -------------------------------------------------------------------------------- /src/mtp_dc_pool_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Sergey 3 | %%% @copyright (C) 2018, Sergey 4 | %%% @doc 5 | %%% Supervisor for mtp_dc_pool processes 6 | %%% @end 7 | %%% Created : 14 Oct 2018 by Sergey 8 | %%%------------------------------------------------------------------- 9 | -module(mtp_dc_pool_sup). 10 | 11 | -behaviour(supervisor). 12 | 13 | -export([start_link/0, 14 | start_pool/1]). 15 | -export([init/1]). 16 | 17 | -define(SERVER, ?MODULE). 18 | 19 | start_link() -> 20 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 21 | 22 | -spec start_pool(mtp_config:dc_id()) -> {ok, pid()}. 23 | start_pool(DcId) -> 24 | %% Or maybe it should read IPs from mtp_config by itself? 25 | supervisor:start_child(?SERVER, [DcId]). 26 | 27 | init([]) -> 28 | 29 | SupFlags = #{strategy => simple_one_for_one, 30 | intensity => 50, 31 | period => 5}, 32 | 33 | AChild = #{id => mtp_dc_pool, 34 | start => {mtp_dc_pool, start_link, []}, 35 | restart => permanent, 36 | shutdown => 10000, 37 | type => worker}, 38 | 39 | {ok, {SupFlags, [AChild]}}. 40 | -------------------------------------------------------------------------------- /src/mtp_down_conn_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Sergey 3 | %%% @copyright (C) 2018, Sergey 4 | %%% @doc 5 | %%% Supervisor for mtp_down_conn processes 6 | %%% @end 7 | %%% TODO: maybe have one supervisor per-DC 8 | %%% Created : 14 Oct 2018 by Sergey 9 | %%%------------------------------------------------------------------- 10 | -module(mtp_down_conn_sup). 11 | 12 | -behaviour(supervisor). 13 | 14 | -export([start_link/0, 15 | start_conn/2]). 16 | -export([init/1]). 17 | 18 | -define(SERVER, ?MODULE). 19 | 20 | start_link() -> 21 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 22 | 23 | -spec start_conn(pid(), mtp_config:dc_id()) -> {ok, pid()}. 24 | start_conn(Pool, DcId) -> 25 | supervisor:start_child(?SERVER, [Pool, DcId]). 26 | 27 | init([]) -> 28 | 29 | SupFlags = #{strategy => simple_one_for_one, 30 | intensity => 50, 31 | period => 5}, 32 | 33 | AChild = #{id => mtp_down_conn, 34 | start => {mtp_down_conn, start_link, []}, 35 | restart => temporary, 36 | shutdown => 2000, 37 | type => worker}, 38 | 39 | {ok, {SupFlags, [AChild]}}. 40 | -------------------------------------------------------------------------------- /src/mtp_full.erl: -------------------------------------------------------------------------------- 1 | %%% @author Sergey 2 | %%% @copyright (C) 2018, Sergey 3 | %%% @doc 4 | %%% MTProto "full" packet format with padding 5 | %%% ``` 6 | %%% <> 7 | %%% ``` 8 | %%% @end 9 | %%% Created : 6 Jun 2018 by Sergey 10 | 11 | -module(mtp_full). 12 | -behaviour(mtp_codec). 13 | 14 | -export([new/0, new/3, 15 | try_decode_packet/2, 16 | encode_packet/2]). 17 | -export_type([codec/0]). 18 | 19 | -dialyzer(no_improper_lists). 20 | 21 | -record(full_st, 22 | {enc_seq_no :: integer(), 23 | dec_seq_no :: integer(), 24 | check_crc = true :: boolean()}). 25 | 26 | -define(MIN_MSG_LEN, 12). 27 | -define(MAX_MSG_LEN, 16777216). %2^24 - 16mb 28 | 29 | -define(BLOCK_SIZE, 16). 30 | -define(PAD, <<4:32/little>>). 31 | -define(APP, mtproto_proxy). 32 | 33 | -opaque codec() :: #full_st{}. 34 | 35 | 36 | new() -> 37 | new(0, 0, true). 38 | 39 | new(EncSeqNo, DecSeqNo, CheckCRC) -> 40 | #full_st{enc_seq_no = EncSeqNo, 41 | dec_seq_no = DecSeqNo, 42 | check_crc = CheckCRC}. 43 | 44 | try_decode_packet(<<4:32/little, Tail/binary>>, S) -> 45 | %% Skip padding 46 | try_decode_packet(Tail, S); 47 | try_decode_packet(<>, 48 | #full_st{dec_seq_no = SeqNo, check_crc = CheckCRC} = S) -> 49 | ((Len rem byte_size(?PAD)) == 0) 50 | orelse error({wrong_alignement, Len}), 51 | ((?MIN_MSG_LEN =< Len) and (Len =< ?MAX_MSG_LEN)) 52 | orelse error({wrong_msg_len, Len}), 53 | (SeqNo == PktSeqNo) 54 | orelse error({wrong_seq_no, SeqNo, PktSeqNo}), 55 | BodyLen = Len - 4 - 4 - 4, 56 | case Tail of 57 | <> -> 58 | case CheckCRC of 59 | true -> 60 | PacketCrc = erlang:crc32([<> | Body]), 61 | (CRC == PacketCrc) 62 | orelse error({wrong_checksum, CRC, PacketCrc}); 63 | false -> 64 | ok 65 | end, 66 | %% TODO: predict padding size from padding_size(Len) 67 | {ok, Body, trim_padding(Rest), S#full_st{dec_seq_no = SeqNo + 1}}; 68 | _ -> 69 | {incomplete, S} 70 | end; 71 | try_decode_packet(_, S) -> 72 | {incomplete, S}. 73 | 74 | trim_padding(<<4:32/little, Tail/binary>>) -> 75 | trim_padding(Tail); 76 | trim_padding(Bin) -> Bin. 77 | 78 | 79 | encode_packet(Bin, #full_st{enc_seq_no = SeqNo} = S) -> 80 | BodySize = iolist_size(Bin), 81 | ((BodySize rem byte_size(?PAD)) == 0) 82 | orelse error({wrong_alignment, BodySize}), 83 | Len = BodySize + 4 + 4 + 4, 84 | MsgNoChecksum = 85 | [<> 87 | | Bin], 88 | CheckSum = erlang:crc32(MsgNoChecksum), 89 | FullMsg = [MsgNoChecksum | <>], 90 | Len = iolist_size(FullMsg), 91 | NPaddings = padding_size(Len) div byte_size(?PAD), 92 | Padding = lists:duplicate(NPaddings, ?PAD), 93 | {[FullMsg | Padding], S#full_st{enc_seq_no = SeqNo + 1}}. 94 | 95 | padding_size(Len) -> 96 | %% XXX: is there a cleaner way? 97 | (?BLOCK_SIZE - (Len rem ?BLOCK_SIZE)) rem ?BLOCK_SIZE. 98 | 99 | 100 | -ifdef(TEST). 101 | -include_lib("eunit/include/eunit.hrl"). 102 | 103 | encode_nopadding_test() -> 104 | S = new(), 105 | {Enc, _S1} = encode_packet(<<1, 1, 1, 1>>, S), 106 | ?assertEqual( 107 | <<16,0,0,0, 108 | 0,0,0,0, 109 | 1,1,1,1, 110 | 22,39,175,160>>, 111 | iolist_to_binary(Enc)). 112 | 113 | encode_padding_test() -> 114 | S = new(), 115 | {Enc, _S1} = encode_packet(<<1,1,1,1,1,1,1,1>>, S), 116 | ?assertEqual( 117 | <<20,0,0,0,0,0,0,0, %size, seq no 118 | 1,1,1,1,1,1,1,1, %data 119 | 246,196,46,149, %CRC 120 | 4,0,0,0,4,0,0,0,4,0,0,0>>, %padding 121 | iolist_to_binary(Enc)). 122 | 123 | encode_padding_seq_test() -> 124 | S = new(), 125 | {Enc1, S1} = encode_packet(binary:copy(<<9>>, 8), S), 126 | ?assertEqual( 127 | <<20,0,0,0, 128 | 0,0,0,0, 129 | 9,9,9,9,9,9,9,9, 130 | 229,35,162,164, 131 | 4, 0,0,0,4,0,0,0,4,0,0,0>>, 132 | iolist_to_binary(Enc1)), 133 | {Enc2, _S2} = encode_packet(binary:copy(<<8>>, 8), S1), 134 | ?assertEqual( 135 | <<20,0,0,0, 136 | 1,0,0,0, 137 | 8,8,8,8,8,8,8,8, 138 | 48,146,132,116, 139 | 4,0,0,0,4,0,0,0,4,0,0,0>>, 140 | iolist_to_binary(Enc2)). 141 | 142 | decode_none_test() -> 143 | S = new(), 144 | ?assertEqual( 145 | {incomplete, S}, try_decode_packet(<<>>, S)). 146 | 147 | codec_test() -> 148 | %% Overhead is 12b per-packet 149 | S = new(), 150 | Packets = [ 151 | binary:copy(<<0>>, 4), %non-padded 152 | binary:copy(<<1>>, 8), %padded 153 | binary:copy(<<2>>, 4), %non-padded 154 | binary:copy(<<2>>, 100) %padded 155 | ], 156 | lists:foldl( 157 | fun(B, S1) -> 158 | {Encoded, S2} = encode_packet(B, S1), 159 | BinEncoded = iolist_to_binary(Encoded), 160 | {ok, Decoded, <<>>, S3} = try_decode_packet(BinEncoded, S2), 161 | ?assertEqual(B, Decoded, {BinEncoded, S2, S3}), 162 | S3 163 | end, S, Packets). 164 | 165 | codec_stream_test() -> 166 | S = new(), 167 | Packets = [ 168 | binary:copy(<<0>>, 4), %non-padded 169 | binary:copy(<<1>>, 8), %padded 170 | binary:copy(<<2>>, 4), %non-padded 171 | binary:copy(<<2>>, 100) %padded 172 | ], 173 | {Encoded, SS} = 174 | lists:foldl( 175 | fun(B, {Enc1, S1}) -> 176 | {Enc2, S2} = encode_packet(B, S1), 177 | {[Enc1 | Enc2], S2} 178 | end, {[], S}, Packets), 179 | lists:foldl( 180 | fun(B, {Enc, S1}) -> 181 | {ok, Dec, Rest, S2} = try_decode_packet(Enc, S1), 182 | ?assertEqual(B, Dec), 183 | {Rest, S2} 184 | end, {iolist_to_binary(Encoded), SS}, Packets). 185 | 186 | -endif. 187 | -------------------------------------------------------------------------------- /src/mtp_intermediate.erl: -------------------------------------------------------------------------------- 1 | %%% @author Sergey 2 | %%% @copyright (C) 2018, Sergey 3 | %%% @doc 4 | %%% MTProto intermediate protocol 5 | %%% @end 6 | %%% Created : 18 Jun 2018 by Sergey 7 | 8 | -module(mtp_intermediate). 9 | 10 | -behaviour(mtp_codec). 11 | 12 | -export([new/0, 13 | new/1, 14 | try_decode_packet/2, 15 | encode_packet/2]). 16 | -export_type([codec/0]). 17 | 18 | -dialyzer(no_improper_lists). 19 | 20 | -record(int_st, 21 | {padding = false :: boolean()}). 22 | -define(MAX_PACKET_SIZE, 1 * 1024 * 1024). % 1mb 23 | -define(APP, mtproto_proxy). 24 | -define(MAX_SIZE, 16#80000000). 25 | 26 | -opaque codec() :: #int_st{}. 27 | 28 | new() -> 29 | new(#{}). 30 | 31 | new(Opts) -> 32 | #int_st{padding = maps:get(padding, Opts, false)}. 33 | 34 | -spec try_decode_packet(binary(), codec()) -> {ok, binary(), binary(), codec()} 35 | | {incomplete, codec()}. 36 | try_decode_packet(<>, St) -> 37 | Len1 = case Len < ?MAX_SIZE of 38 | true -> Len; 39 | false -> Len - ?MAX_SIZE 40 | end, 41 | (Len1 < ?MAX_PACKET_SIZE) 42 | orelse 43 | begin 44 | error({protocol_error, intermediate_max_size, Len1}) 45 | end, 46 | try_decode_packet_len(Len1, Tail, St); 47 | try_decode_packet(_, St) -> 48 | {incomplete, St}. 49 | 50 | try_decode_packet_len(Len, Data, #int_st{padding = Pad} = St) -> 51 | Padding = case Pad of 52 | true -> Len rem 4; 53 | false -> 0 54 | end, 55 | NopadLen = Len - Padding, 56 | case Data of 57 | <> -> 58 | {ok, Packet, Rest, St}; 59 | _ -> 60 | {incomplete, St} 61 | end. 62 | 63 | -spec encode_packet(iodata(), codec()) -> {iodata(), codec()}. 64 | encode_packet(Data, #int_st{padding = Pad} = St) -> 65 | Size = iolist_size(Data), 66 | Packet = case Pad of 67 | false -> [<> | Data]; 68 | true -> 69 | PadSize = rand:uniform(4) - 1, 70 | Padding = crypto:strong_rand_bytes(PadSize), % 0 is ok 71 | [<<(Size + PadSize):32/little>>, Data | Padding] 72 | end, 73 | {Packet, St}. 74 | -------------------------------------------------------------------------------- /src/mtp_metric.erl: -------------------------------------------------------------------------------- 1 | %%% @author sergey 2 | %%% @copyright (C) 2018, sergey 3 | %%% @doc 4 | %%% Backend-agnostic interface for logging metrics. 5 | %%% Made with prometheus.erl in mind, but might be used with smth else 6 | %%% @end 7 | %%% Created : 15 May 2018 by sergey 8 | 9 | -module(mtp_metric). 10 | 11 | -export([count_inc/3, 12 | gauge_set/3, 13 | histogram_observe/3, 14 | rt/2, rt/3]). 15 | 16 | -export([passive_metrics/0, 17 | active_metrics/0]). 18 | 19 | -define(APP, mtproto_proxy). 20 | -define(PD_KEY, {?MODULE, context_labels}). 21 | 22 | -type metric_type() :: gauge | count | histogram. 23 | -type metric_name() :: [atom()]. 24 | -type metric_doc() :: string(). 25 | 26 | count_inc(Name, Value, Extra) -> 27 | notify(count, Name, Value, Extra). 28 | 29 | gauge_set(Name, Value, Extra) -> 30 | notify(gauge, Name, Value, Extra). 31 | 32 | histogram_observe(Name, Value, Extra) -> 33 | notify(histogram, Name, Value, Extra). 34 | 35 | rt(Name, Fun) -> 36 | rt(Name, Fun, #{}). 37 | 38 | rt(Name, Fun, Extra) -> 39 | Start = erlang:monotonic_time(), 40 | try 41 | Fun() 42 | after 43 | notify(histogram, Name, erlang:monotonic_time() - Start, Extra) 44 | end. 45 | 46 | 47 | notify(Type, Name, Value, Extra) -> 48 | case get_backend() of 49 | undefined -> 50 | false; 51 | Mod -> 52 | Mod:notify(Type, Name, Value, Extra) 53 | end. 54 | 55 | get_backend() -> 56 | %% Cache resutl of application:get_env in process dict because it's on the hot path 57 | case erlang:get(metric_backend) of 58 | undefined -> 59 | case application:get_env(?APP, metric_backend) of 60 | {ok, Mod} when Mod =/= false; 61 | Mod =/= undefined -> 62 | erlang:put(metric_backend, Mod), 63 | Mod; 64 | _ -> 65 | erlang:put(metric_backend, false), 66 | undefined 67 | end; 68 | false -> 69 | undefined; 70 | Mod -> 71 | Mod 72 | end. 73 | 74 | -spec passive_metrics() -> [{metric_type(), metric_name(), metric_doc(), 75 | [{Labels, Value}]}] 76 | when 77 | Labels :: #{atom() => binary() | atom()}, 78 | Value :: integer() | float(). 79 | passive_metrics() -> 80 | DownStatus = mtp_config:status(), 81 | [{gauge, [?APP, dc_num_downstreams], 82 | "Count of connections to downstream", 83 | [{#{dc => DcId}, NDowns} 84 | || #{n_downstreams := NDowns, dc_id := DcId} <- DownStatus]}, 85 | {gauge, [?APP, dc_num_upstreams], 86 | "Count of upstreams connected to DC", 87 | [{#{dc => DcId}, NUps} 88 | || #{n_upstreams := NUps, dc_id := DcId} <- DownStatus]}, 89 | {gauge, [?APP, dc_upstreams_per_downstream], 90 | "Count of upstreams connected to DC", 91 | lists:flatmap( 92 | fun(#{min := Min, 93 | max := Max, 94 | dc_id := DcId}) -> 95 | [{#{dc => DcId, meter => min}, Min}, 96 | {#{dc => DcId, meter => max}, Max}] 97 | end, DownStatus)} 98 | | 99 | [{gauge, [?APP, connections, count], 100 | "Count of ranch connections", 101 | [{#{listener => H}, proplists:get_value(all_connections, P)} 102 | || {H, P} <- mtproto_proxy_app:mtp_listeners()]}] ]. 103 | 104 | -spec active_metrics() -> [{metric_type(), metric_name(), metric_doc(), Opts}] 105 | when 106 | Opts :: #{duration_units => atom(), 107 | buckets => [number()], 108 | labels => [atom()]}. 109 | active_metrics() -> 110 | [{count, [?APP, in_connection, total], 111 | "MTP incoming connection", 112 | #{labels => [listener]}}, 113 | {count, [?APP, in_connection_closed, total], 114 | "MTP incoming connection closed", 115 | #{labels => [listener]}}, 116 | {histogram, [?APP, session_lifetime, seconds], 117 | "Time from in connection open to session process termination", 118 | #{duration_unit => seconds, 119 | buckets => [0.2, 0.5, 1, 5, 10, 30, 60, 150, 300, 600, 1200], 120 | labels => [listener] 121 | }}, 122 | 123 | {count, [?APP, inactive_timeout, total], 124 | "Connection closed by timeout because of no activity", 125 | #{labels => [listener]}}, 126 | {count, [?APP, inactive_hibernate, total], 127 | "Connection goes to hibernate by timeout because of no activity", 128 | #{labels => [listener]}}, 129 | {count, [?APP, timer_switch, total], 130 | "Connection timeout mode switches", 131 | #{labels => [listener, from, to]}}, 132 | {count, [?APP, healthcheck, total], 133 | "Upstream self-healthcheck triggered some action", 134 | #{labels => [action]}}, 135 | 136 | {count, [?APP, received, downstream, bytes], 137 | "Bytes transmitted from downstream socket", 138 | #{labels => [dc_id]}}, 139 | {count, [?APP, received, upstream, bytes], 140 | "Bytes transmitted from upstream socket", 141 | #{labels => [listener]}}, 142 | {count, [?APP, sent, downstream, bytes], 143 | "Bytes sent to downstream socket", 144 | #{labels => [dc_id]}}, 145 | {count, [?APP, sent, upstream, bytes], 146 | "Bytes sent to upstream socket", 147 | #{labels => [listener]}}, 148 | 149 | {histogram, [?APP, tracker_packet_size, bytes], 150 | "Received packet size", 151 | #{labels => [direction], 152 | buckets => {exponential, 8, 4, 8}}}, 153 | 154 | {histogram, [?APP, tg_packet_size, bytes], 155 | "Proxied telegram protocol packet size", 156 | #{labels => [direction], 157 | buckets => {exponential, 8, 4, 8}}}, 158 | 159 | {count, [?APP, protocol_error, total], 160 | "Proxy protocol errors", 161 | #{labels => [listener, reason]}}, 162 | {count, [?APP, protocol_ok, total], 163 | "Proxy upstream protocol type", 164 | #{labels => [listener, protocol]}}, 165 | 166 | {count, [?APP, out_connect_ok, total], 167 | "Proxy out connections", 168 | #{labels => [dc_id]}}, 169 | {count, [?APP, out_connect_error, total], 170 | "Proxy out connect errors", 171 | #{labels => [dc_id, reason]}}, 172 | 173 | 174 | {count, [?APP, down_backpressure, total], 175 | "Times downstream backpressure state was changed", 176 | #{labels => [dc_id, state]}}, 177 | {histogram, [?APP, upstream_send_duration, seconds], 178 | "Duration of tcp send calls to upstream", 179 | #{duration_unit => seconds, 180 | %% buckets => ?MS_BUCKETS 181 | labels => [listener] 182 | }}, 183 | {histogram, [?APP, downstream_send_duration, seconds], 184 | "Duration of tcp send calls to downstream", 185 | #{duration_unit => seconds, 186 | %% buckets => ?MS_BUCKETS 187 | labels => [dc] 188 | }}, 189 | {count, [?APP, upstream_send_error, total], 190 | "Count of tcp send errors to upstream", 191 | #{labels => [listener, reason]}} 192 | ]. 193 | -------------------------------------------------------------------------------- /src/mtp_noop_codec.erl: -------------------------------------------------------------------------------- 1 | %%% @author Sergey 2 | %%% @copyright (C) 2018, Sergey 3 | %%% @doc 4 | %%% Fake codec that returns it's input as output. 5 | %%% Used in downstream handshake flow 6 | %%% @end 7 | %%% Created : 31 Oct 2018 by Sergey 8 | 9 | -module(mtp_noop_codec). 10 | -behaviour(mtp_codec). 11 | -export([new/0, 12 | try_decode_packet/2, 13 | encode_packet/2]). 14 | -export_type([codec/0]). 15 | 16 | -opaque codec() :: ?MODULE. 17 | 18 | -spec new() -> codec(). 19 | new() -> 20 | ?MODULE. 21 | 22 | -spec try_decode_packet(binary(), codec()) -> {ok, binary(), binary(), codec()}. 23 | try_decode_packet(<<>>, ?MODULE) -> 24 | {incomplete, ?MODULE}; 25 | try_decode_packet(Data, ?MODULE) -> 26 | {ok, Data, <<>>, ?MODULE}. 27 | 28 | -spec encode_packet(binary(), codec()) -> {binary(), codec()}. 29 | encode_packet(Data, ?MODULE) -> 30 | {Data, ?MODULE}. 31 | -------------------------------------------------------------------------------- /src/mtp_obfuscated.erl: -------------------------------------------------------------------------------- 1 | %%% @author Sergey 2 | %%% @copyright (C) 2018, Sergey 3 | %%% @doc 4 | %%% MTProto proxy encryption and packet layer; "obfuscated2" protocol lib 5 | %%% @end 6 | %%% Created : 29 May 2018 by Sergey 7 | 8 | -module(mtp_obfuscated). 9 | -behaviour(mtp_codec). 10 | -export([from_header/2, 11 | new/4, 12 | encrypt/2, 13 | decrypt/2, 14 | try_decode_packet/2, 15 | encode_packet/2 16 | ]). 17 | -export([bin_rev/1]). 18 | -ifdef(TEST). 19 | -export([client_create/3, 20 | client_create/4]). 21 | -endif. 22 | 23 | -export_type([codec/0]). 24 | 25 | -record(st, 26 | {encrypt :: any(), % aes state 27 | decrypt :: any() % aes state 28 | }). 29 | 30 | -define(APP, mtproto_proxy). 31 | 32 | -define(KEY_LEN, 32). 33 | -define(IV_LEN, 16). 34 | 35 | -opaque codec() :: #st{}. 36 | 37 | -ifdef(TEST). 38 | client_create(Secret, Protocol, DcId) -> 39 | client_create(crypto:strong_rand_bytes(58), 40 | Secret, Protocol, DcId). 41 | 42 | -spec client_create(binary(), binary(), mtp_codec:packet_codec(), integer()) -> 43 | {Packet, 44 | {EncKey, EncIv}, 45 | {DecKey, DecIv}, 46 | CliCodec} when 47 | Packet :: binary(), 48 | EncKey :: binary(), 49 | EncIv :: binary(), 50 | DecKey :: binary(), 51 | DecIv :: binary(), 52 | CliCodec :: codec(). 53 | client_create(Seed, HexSecret, Protocol, DcId) when byte_size(HexSecret) == 32 -> 54 | client_create(Seed, mtp_handler:unhex(HexSecret), Protocol, DcId); 55 | client_create(Seed, Secret, Protocol, DcId) when byte_size(Seed) == 58, 56 | byte_size(Secret) == 16, 57 | DcId > -10, 58 | DcId < 10, 59 | is_atom(Protocol) -> 60 | <> = Seed, 61 | ProtocolBin = encode_protocol(Protocol), 62 | DcIdBin = encode_dc_id(DcId), 63 | Raw = <>, 64 | 65 | %% init_up_encrypt/2 66 | <<_:8/binary, ToRev:(?KEY_LEN + ?IV_LEN)/binary, _/binary>> = Raw, 67 | <> = bin_rev(ToRev), 68 | DecKey = crypto:hash('sha256', <>), 69 | 70 | %% init_up_decrypt/2 71 | <<_:8/binary, EncKeySeed:?KEY_LEN/binary, EncIv:?IV_LEN/binary, _/binary>> = Raw, 72 | EncKey = crypto:hash('sha256', <>), 73 | 74 | Codec = new(EncKey, EncIv, DecKey, DecIv), 75 | {<<_:56/binary, Encrypted:8/binary>>, Codec1} = encrypt(Raw, Codec), 76 | <> = Raw, 77 | Packet = <>, 78 | {Packet, 79 | {EncKey, EncIv}, 80 | {DecKey, DecIv}, 81 | Codec1}. 82 | 83 | 84 | %% 4byte 85 | encode_protocol(mtp_abridged) -> 86 | <<16#ef, 16#ef, 16#ef, 16#ef>>; 87 | encode_protocol(mtp_intermediate) -> 88 | <<16#ee, 16#ee, 16#ee, 16#ee>>; 89 | encode_protocol(mtp_secure) -> 90 | <<16#dd, 16#dd, 16#dd, 16#dd>>. 91 | 92 | %% 4byte 93 | encode_dc_id(DcId) -> 94 | <>. 95 | -endif. 96 | 97 | %% @doc creates new obfuscated stream (MTProto proxy format) 98 | -spec from_header(binary(), binary()) -> {ok, integer(), mtp_codec:packet_codec(), codec()} 99 | | {error, unknown_protocol}. 100 | from_header(Header, Secret) when byte_size(Header) == 64 -> 101 | %% 1) Encryption key 102 | %% [--- _: 8b ----|---------- b: 48b -------------|-- _: 8b --] = header: 64b 103 | %% b_r: 48b = reverse([---------- b ------------------]) 104 | %% [-- key_seed: 32b --|- iv: 16b -] = b_r 105 | %% key: 32b = sha256( [-- key_seed: 32b --|-- secret: 32b --] ) 106 | %% iv: 16b = iv 107 | %% 108 | %% 2) Decryption key 109 | %% [--- _: 8b ---|-- key_seed: 32b --|- iv: 16b -|-- _: 8b --] = header 110 | %% key: 32b = sha256( [-- key_seed: 32b --|-- secret: 32b --] ) 111 | %% ib: 16b = ib 112 | %% 113 | %% 3) Protocol and datacenter 114 | %% decrypted_header: 64b = decrypt(header) 115 | %% [-------------- _a: 56b ----|-------- b: 6b ---------|- _: 2b -] = decrypted_header 116 | %% [- proto: 4b -|- dc: 2b -] 117 | {EncKey, EncIV} = init_up_encrypt(Header, Secret), 118 | {DecKey, DecIV} = init_up_decrypt(Header, Secret), 119 | St = new(EncKey, EncIV, DecKey, DecIV), 120 | {<<_:56/binary, Bin1:6/binary, _:2/binary>>, <<>>, St1} = decrypt(Header, St), 121 | case get_protocol(Bin1) of 122 | {error, unknown_protocol} = Err -> 123 | Err; 124 | Protocol -> 125 | DcId = get_dc(Bin1), 126 | {ok, DcId, Protocol, St1} 127 | end. 128 | 129 | init_up_encrypt(Bin, Secret) -> 130 | <<_:8/binary, ToRev:(?KEY_LEN + ?IV_LEN)/binary, _/binary>> = Bin, 131 | Rev = bin_rev(ToRev), 132 | <> = Rev, 133 | %% <<_:32/binary, RevIV:16/binary, _/binary>> = Bin, 134 | Key = crypto:hash('sha256', <>), 135 | {Key, IV}. 136 | 137 | init_up_decrypt(Bin, Secret) -> 138 | <<_:8/binary, KeySeed:?KEY_LEN/binary, IV:?IV_LEN/binary, _/binary>> = Bin, 139 | Key = crypto:hash('sha256', <>), 140 | {Key, IV}. 141 | 142 | get_protocol(<<16#ef, 16#ef, 16#ef, 16#ef, _:2/binary>>) -> 143 | mtp_abridged; 144 | get_protocol(<<16#ee, 16#ee, 16#ee, 16#ee, _:2/binary>>) -> 145 | mtp_intermediate; 146 | get_protocol(<<16#dd, 16#dd, 16#dd, 16#dd, _:2/binary>>) -> 147 | mtp_secure; 148 | get_protocol(_) -> 149 | {error, unknown_protocol}. 150 | 151 | get_dc(<<_:4/binary, DcId:16/signed-little-integer>>) -> 152 | DcId. 153 | 154 | 155 | new(EncKey, EncIV, DecKey, DecIV) -> 156 | #st{decrypt = crypto_stream_init('aes_ctr', DecKey, DecIV), 157 | encrypt = crypto_stream_init('aes_ctr', EncKey, EncIV)}. 158 | 159 | -spec encrypt(iodata(), codec()) -> {binary(), codec()}. 160 | encrypt(Data, #st{encrypt = Enc} = St) -> 161 | {Enc1, Encrypted} = crypto_stream_encrypt(Enc, Data), 162 | {Encrypted, St#st{encrypt = Enc1}}. 163 | 164 | -spec decrypt(iodata(), codec()) -> {binary(), binary(), codec()}. 165 | decrypt(Encrypted, #st{decrypt = Dec} = St) -> 166 | {Dec1, Data} = crypto_stream_encrypt(Dec, Encrypted), 167 | {Data, <<>>, St#st{decrypt = Dec1}}. 168 | 169 | -spec try_decode_packet(iodata(), codec()) -> {ok, Decoded :: binary(), Tail :: binary(), codec()} 170 | | {incomplete, codec()}. 171 | try_decode_packet(Encrypted, St) -> 172 | {Decrypted, Tail, St1} = decrypt(Encrypted, St), 173 | {ok, Decrypted, Tail, St1}. 174 | 175 | -spec encode_packet(iodata(), codec()) -> {iodata(), codec()}. 176 | encode_packet(Msg, S) -> 177 | encrypt(Msg, S). 178 | 179 | 180 | %% Helpers 181 | bin_rev(Bin) -> 182 | %% binary:encode_unsigned(binary:decode_unsigned(Bin, little)). 183 | list_to_binary(lists:reverse(binary_to_list(Bin))). 184 | 185 | -if(?OTP_RELEASE >= 23). 186 | crypto_stream_init(aes_ctr, Key, IV) -> 187 | crypto:crypto_init(aes_256_ctr, Key, IV, []). 188 | 189 | crypto_stream_encrypt(State, Data) -> 190 | {State, crypto:crypto_update(State, Data)}. 191 | 192 | -else. 193 | crypto_stream_init(Algo, Key, IV) -> 194 | crypto:stream_init(Algo, Key, IV). 195 | 196 | crypto_stream_encrypt(State, Data) -> 197 | crypto:stream_encrypt(State, Data). 198 | 199 | -endif. 200 | 201 | -ifdef(TEST). 202 | -include_lib("eunit/include/eunit.hrl"). 203 | 204 | client_server_test() -> 205 | Secret = crypto:strong_rand_bytes(16), 206 | DcId = 4, 207 | Protocol = mtp_secure, 208 | {Packet, _, _, _CliCodec} = client_create(Secret, Protocol, DcId), 209 | Srv = from_header(Packet, Secret), 210 | ?assertMatch({ok, DcId, Protocol, _}, Srv). 211 | 212 | -endif. 213 | -------------------------------------------------------------------------------- /src/mtp_policy.erl: -------------------------------------------------------------------------------- 1 | %%% @author Sergey 2 | %%% @copyright (C) 2019, Sergey 3 | %%% @doc 4 | %%% Evaluator for "policy" config 5 | %%% @end 6 | %%% Created : 20 Aug 2019 by Sergey 7 | 8 | -module(mtp_policy). 9 | -export([check/4]). 10 | -export([dec/4]). 11 | -export([convert/2]). 12 | 13 | -export_type([rule/0, 14 | key/0, 15 | db_val/0]). 16 | 17 | -record(vars, 18 | {listener :: atom(), 19 | client_ip :: inet:ip_address(), 20 | ip_family :: inet | inet6, 21 | tls_domain :: undefined | binary()}). 22 | 23 | -type key() :: 24 | port_name | 25 | tls_domain | 26 | client_ipv4 | 27 | client_ipv6 | 28 | {client_ipv4_subnet, 1..32} | 29 | {client_ipv6_subnet, 8..128}. 30 | 31 | -type rule() :: 32 | {max_connections, [key()], pos_integer()} | 33 | {in_table, key(), mtp_policy_table:sub_tab()} | 34 | {not_in_table, key(), mtp_policy_table:sub_tab()}. 35 | 36 | -type db_val() :: binary() | integer() | atom(). 37 | 38 | -include_lib("hut/include/hut.hrl"). 39 | 40 | -spec check([rule()], any(), inet:ip_address(), binary() | undefined) -> [rule()]. 41 | check(Rules, ListenerName, ClientIp, TlsDomain) -> 42 | Vars = vars(ListenerName, ClientIp,TlsDomain), 43 | lists:dropwhile( 44 | fun(Rule) -> 45 | try check(Rule, Vars) 46 | catch throw:not_applicable -> 47 | true 48 | end 49 | end, Rules). 50 | 51 | dec(Rules, ListenerName, ClientIp,TlsDomain) -> 52 | Vars = vars(ListenerName, ClientIp,TlsDomain), 53 | lists:foreach( 54 | fun({max_connections, Keys, _Max}) -> 55 | try 56 | Key = [val(K, Vars) || K <- Keys], 57 | mtp_policy_counter:decrement(Key) 58 | catch throw:not_applicable -> 59 | ok 60 | end; 61 | (_) -> 62 | ok 63 | end, Rules). 64 | 65 | vars(ListenerName, ClientIp, TlsDomain) -> 66 | IpFamily = case tuple_size(ClientIp) of 67 | 4 -> inet; 68 | 8 -> inet6 69 | end, 70 | #vars{listener = ListenerName, 71 | client_ip = ClientIp, 72 | ip_family = IpFamily, 73 | tls_domain = TlsDomain}. 74 | 75 | check({max_connections, Keys, Max}, Vars) -> 76 | Key = [val(K, Vars) || K <- Keys], 77 | case mtp_policy_counter:increment(Key) of 78 | N when N > Max -> 79 | mtp_policy_counter:decrement(Key), 80 | false; 81 | _ -> 82 | true 83 | end; 84 | check({in_table, Key, Tab}, Vars) -> 85 | Val = val(Key, Vars), 86 | mtp_policy_table:exists(Tab, Val); 87 | check({not_in_table, Key, Tab}, Vars) -> 88 | Val = val(Key, Vars), 89 | not mtp_policy_table:exists(Tab, Val). 90 | 91 | 92 | val(port_name = T, #vars{listener = Listener}) -> 93 | convert(T, Listener); 94 | val(tls_domain = T, #vars{tls_domain = Domain}) when is_binary(Domain) -> 95 | convert(T, Domain); 96 | val(client_ipv4 = T, #vars{ip_family = inet, client_ip = Ip}) -> 97 | convert(T, Ip); 98 | val(client_ipv6 = T, #vars{ip_family = inet6, client_ip = Ip}) -> 99 | convert(T, Ip); 100 | val({client_ipv4_subnet, Mask} = T, #vars{ip_family = inet, client_ip = Ip}) when Mask > 0, 101 | Mask =< 32 -> 102 | convert(T, Ip); 103 | val({client_ipv6_subnet, Mask} = T, #vars{ip_family = inet6, client_ip = Ip}) when Mask > 8, 104 | Mask =< 128 -> 105 | convert(T, Ip); 106 | val(Policy, Vars) when is_atom(Policy); 107 | is_tuple(Policy) -> 108 | ?log(debug, "Policy ~p not applicable ~p", [Policy, Vars]), 109 | throw(not_applicable). 110 | 111 | 112 | -spec convert(key(), any()) -> db_val(). 113 | convert(port_name, PortName) -> 114 | PortName; 115 | convert(tls_domain, Domain) when is_binary(Domain) -> 116 | string:casefold(Domain); 117 | convert(tls_domain, DomainStr) when is_list(DomainStr) -> 118 | convert(tls_domain, list_to_binary(DomainStr)); 119 | convert(client_ipv4, Ip0) -> 120 | Ip = parse_ip(v4, Ip0), 121 | <> = mtp_rpc:inet_pton(Ip), 122 | I; 123 | convert(client_ipv6, Ip0) -> 124 | Ip = parse_ip(v6, Ip0), 125 | <> = mtp_rpc:inet_pton(Ip), 126 | I; 127 | convert({client_ipv4_subnet, Mask}, Ip0) -> 128 | Ip = parse_ip(v4, Ip0), 129 | <> = mtp_rpc:inet_pton(Ip), 130 | I; 131 | convert({client_ipv6_subnet, Mask}, Ip0) -> 132 | Ip = parse_ip(v6, Ip0), 133 | <> = mtp_rpc:inet_pton(Ip), 134 | I. 135 | 136 | parse_ip(v4, Tup) when is_tuple(Tup), 137 | tuple_size(Tup) == 4 -> 138 | Tup; 139 | parse_ip(v6, Tup) when is_tuple(Tup), 140 | tuple_size(Tup) == 8 -> 141 | Tup; 142 | parse_ip(v4, Str) when is_list(Str) -> 143 | {ok, Ip} = inet:parse_ipv4_address(Str), 144 | Ip; 145 | parse_ip(v6, Str) when is_list(Str) -> 146 | {ok, Ip} = inet:parse_ipv6_address(Str), 147 | Ip. 148 | 149 | -------------------------------------------------------------------------------- /src/mtp_policy_counter.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Sergey 3 | %%% @copyright (C) 2019, Sergey 4 | %%% @doc 5 | %%% Storage for `max_connections` policy. 6 | %%% It's quite simple wrapper for public ETS counter. 7 | %%% @end 8 | %%% Created : 20 Aug 2019 by Sergey 9 | %%%------------------------------------------------------------------- 10 | -module(mtp_policy_counter). 11 | 12 | -behaviour(gen_server). 13 | 14 | %% API 15 | -export([start_link/0]). 16 | -export([increment/1, 17 | decrement/1, 18 | get/1, 19 | flush/0]). 20 | 21 | %% gen_server callbacks 22 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 23 | terminate/2, code_change/3]). 24 | -type key() :: [mtp_policy:db_val()]. 25 | 26 | -define(TAB, ?MODULE). 27 | 28 | -record(state, {tab :: ets:tid()}). 29 | 30 | %%%=================================================================== 31 | %%% API 32 | %%%=================================================================== 33 | -spec increment(key()) -> integer(). 34 | increment(Key) -> 35 | ets:update_counter(?TAB, Key, 1, {Key, 0}). 36 | 37 | -spec decrement(key()) -> integer(). 38 | decrement(Key) -> 39 | try ets:update_counter(?TAB, Key, -1) of 40 | New when New =< 0 -> 41 | ets:delete(?TAB, Key), 42 | 0; 43 | New -> New 44 | catch error:badarg -> 45 | %% already removed 46 | 0 47 | end. 48 | 49 | -spec get(key()) -> non_neg_integer(). 50 | get(Key) -> 51 | case ets:lookup(?TAB, Key) of 52 | [] -> 0; 53 | [{_, V}] -> V 54 | end. 55 | 56 | %% @doc Clean all counters 57 | flush() -> 58 | gen_server:call(?MODULE, flush). 59 | 60 | start_link() -> 61 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 62 | 63 | %%%=================================================================== 64 | %%% gen_server callbacks 65 | %%%=================================================================== 66 | init([]) -> 67 | Tab = ets:new(?TAB, [named_table, {write_concurrency, true}, public]), 68 | {ok, #state{tab = Tab}}. 69 | 70 | handle_call(flush, _From, #state{tab = Tab} = State) -> 71 | true = ets:delete_all_objects(Tab), 72 | {reply, ok, State}. 73 | 74 | handle_cast(_Msg, State) -> 75 | {noreply, State}. 76 | 77 | handle_info(_Info, State) -> 78 | {noreply, State}. 79 | 80 | terminate(_Reason, _State) -> 81 | ok. 82 | 83 | code_change(_OldVsn, State, _Extra) -> 84 | {ok, State}. 85 | 86 | %%%=================================================================== 87 | %%% Internal functions 88 | %%%=================================================================== 89 | -------------------------------------------------------------------------------- /src/mtp_policy_table.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Sergey 3 | %%% @copyright (C) 2019, Sergey 4 | %%% @doc 5 | %%% Storage for `in_table` and `not_in_table` policies. Implements 2-level nested set. 6 | %%% It's quite simple wrapper for protected ETS set. 7 | %%% @end 8 | %%% Created : 20 Aug 2019 by Sergey 9 | %%%------------------------------------------------------------------- 10 | -module(mtp_policy_table). 11 | 12 | -behaviour(gen_server). 13 | 14 | %% API 15 | -export([start_link/0]). 16 | -export([add/3, 17 | del/3, 18 | exists/2, 19 | table_size/1, 20 | flush/0]). 21 | 22 | %% gen_server callbacks 23 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 24 | terminate/2, code_change/3]). 25 | -export_type([sub_tab/0]). 26 | 27 | -type sub_tab() :: atom(). 28 | -type value() :: mtp_policy:db_val(). 29 | 30 | -include_lib("stdlib/include/ms_transform.hrl"). 31 | -define(TAB, ?MODULE). 32 | 33 | -record(state, {tab :: ets:tid()}). 34 | 35 | %%%=================================================================== 36 | %%% API 37 | %%%=================================================================== 38 | -spec add(sub_tab(), mtp_policy:key(), value()) -> ok. 39 | add(Subtable, Type, Value) -> 40 | gen_server:call(?MODULE, {add, Subtable, mtp_policy:convert(Type, Value)}). 41 | 42 | -spec del(sub_tab(), mtp_policy:key(), value()) -> ok. 43 | del(Subtable, Type, Value) -> 44 | gen_server:call(?MODULE, {del, Subtable, mtp_policy:convert(Type, Value)}). 45 | 46 | -spec exists(sub_tab(), value()) -> boolean(). 47 | exists(Subtable, Value) -> 48 | case ets:lookup(?TAB, {Subtable, Value}) of 49 | [] -> false; 50 | [_] -> true 51 | end. 52 | 53 | -spec table_size(sub_tab()) -> non_neg_integer(). 54 | table_size(SubTable) -> 55 | MS = ets:fun2ms(fun({{Tab, _}, _}) when Tab =:= SubTable -> true end), 56 | ets:select_count(?TAB, MS). 57 | 58 | %% @doc Clean all counters 59 | flush() -> 60 | gen_server:call(?MODULE, flush). 61 | 62 | start_link() -> 63 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 64 | 65 | %%%=================================================================== 66 | %%% gen_server callbacks 67 | %%%=================================================================== 68 | init([]) -> 69 | Tab = ets:new(?TAB, [named_table, {read_concurrency, true}, protected]), 70 | {ok, #state{tab = Tab}}. 71 | 72 | handle_call({add, SubTab, Data}, _From, #state{tab = Tab} = State) -> 73 | true = ets:insert(Tab, {{SubTab, Data}, erlang:system_time(millisecond)}), 74 | {reply, ok, State}; 75 | handle_call({del, SubTab, Data}, _From, #state{tab = Tab} = State) -> 76 | true = ets:delete(Tab, {SubTab, Data}), 77 | {reply, ok, State}; 78 | handle_call(flush, _From, #state{tab = Tab} = State) -> 79 | true = ets:delete_all_objects(Tab), 80 | {reply, ok, State}. 81 | 82 | handle_cast(_Msg, State) -> 83 | {noreply, State}. 84 | 85 | handle_info(_Info, State) -> 86 | {noreply, State}. 87 | 88 | terminate(_Reason, _State) -> 89 | ok. 90 | 91 | code_change(_OldVsn, State, _Extra) -> 92 | {ok, State}. 93 | 94 | %%%=================================================================== 95 | %%% Internal functions 96 | %%%=================================================================== 97 | -------------------------------------------------------------------------------- /src/mtp_rpc.erl: -------------------------------------------------------------------------------- 1 | %%% @author Sergey 2 | %%% @copyright (C) 2018, Sergey 3 | %%% @doc 4 | %%% MTProto RPC codec 5 | %%% @end 6 | %%% Created : 6 Jun 2018 by Sergey 7 | 8 | -module(mtp_rpc). 9 | 10 | -export([decode_packet/1, 11 | decode_nonce/1, 12 | decode_handshake/1, 13 | encode_nonce/1, 14 | encode_handshake/1, 15 | encode_packet/2]). 16 | %% For tests 17 | -export([srv_decode_packet/1, 18 | srv_encode_packet/1]). 19 | %% Helpers 20 | -export([inet_pton/1, 21 | encode_ip_port/2]). 22 | -export_type([conn_id/0, packet/0, codec/0]). 23 | 24 | -dialyzer(no_improper_lists). 25 | 26 | -record(rpc_st, 27 | {client_addr :: binary(), 28 | proxy_addr :: binary(), 29 | proxy_tag :: binary(), 30 | conn_id :: integer()}). 31 | 32 | -define(APP, mtproto_proxy). 33 | -define(RPC_PROXY_REQ, 238,241,206,54). %0x36cef1ee 34 | -define(RPC_PROXY_ANS, 13,218,3,68). %0x4403da0d 35 | -define(RPC_CLOSE_CONN, 93,66,207,31). %0x1fcf425d 36 | -define(RPC_CLOSE_EXT, 162,52,182,94). %0x5eb634a2 37 | -define(RPC_SIMPLE_ACK, 155,64,172,59). %0x3bac409b 38 | -define(TL_PROXY_TAG, 174,38,30,219). 39 | 40 | -define(RPC_NONCE, 170,135,203,122). 41 | -define(RPC_HANDSHAKE, 245,238,130,118). 42 | -define(RPC_FLAGS, 0, 0, 0, 0). 43 | 44 | 45 | -define(FLAG_NOT_ENCRYPTED , 16#2). 46 | -define(FLAG_HAS_AD_TAG , 16#8). 47 | -define(FLAG_MAGIC , 16#1000). 48 | -define(FLAG_EXTMODE2 , 16#20000). 49 | -define(FLAG_PAD , 16#8000000). %TODO: use it 50 | -define(FLAG_INTERMEDIATE , 16#20000000). 51 | -define(FLAG_ABRIDGED , 16#40000000). 52 | -define(FLAG_QUICKACK , 16#80000000). 53 | 54 | 55 | -opaque codec() :: #rpc_st{}. 56 | -type conn_id() :: integer(). 57 | -type packet() :: {proxy_ans, conn_id(), binary()} 58 | | {close_ext, conn_id()} 59 | | {simple_ack, conn_id(), binary()}. 60 | 61 | decode_nonce(<>) -> 66 | {nonce, KeySelector, Schema, CryptoTs, CliNonce}. 67 | 68 | decode_handshake(<>) -> 72 | {handshake, SenderPID, PeerPID}. 73 | 74 | encode_nonce({nonce, KeySelector, Schema, CryptoTs, SrvNonce}) -> 75 | <>. 80 | 81 | encode_handshake({handshake, SenderPID, PeerPID}) -> 82 | <>. 86 | 87 | %% It expects that packet segmentation was done on previous layer 88 | %% See mtproto/mtproto-proxy.c:process_client_packet 89 | -spec decode_packet(binary()) -> packet() | {unknown, binary(), binary()}. 90 | decode_packet(<>) -> 91 | %% mtproto/mtproto-proxy.c:client_send_message 92 | {proxy_ans, ConnId, Data}; 93 | decode_packet(<>) -> 94 | {close_ext, ConnId}; 95 | decode_packet(<>) -> 96 | %% mtproto/mtproto-proxy.c:push_rpc_confirmation 97 | {simple_ack, ConnId, Confirm}; 98 | decode_packet(<>) -> 99 | {unknown, Tag, Tail}. 100 | 101 | 102 | encode_packet({data, Msg}, {{ConnId, ClientAddr, ProxyTag}, ProxyAddr}) -> 103 | %% See mtproto/mtproto-proxy.c:forward_mtproto_packet 104 | ((iolist_size(Msg) rem 4) == 0) 105 | orelse error({not_aligned, Msg}), 106 | Flags1 = (?FLAG_HAS_AD_TAG 107 | bor ?FLAG_MAGIC 108 | bor ?FLAG_EXTMODE2 109 | bor ?FLAG_ABRIDGED), 110 | %% if (auth_key_id) ... 111 | Flags = case Msg of 112 | %% XXX: what if Msg is iolist? 113 | <<0, 0, 0, 0, 0, 0, 0, 0, _/binary>> -> 114 | Flags1 bor ?FLAG_NOT_ENCRYPTED; 115 | _ -> 116 | Flags1 117 | end, 118 | [<>, %long long: 121 | ClientAddr, ProxyAddr, 122 | <<24:32/little, %int: ExtraSize 123 | ?TL_PROXY_TAG, %int: ProxyTag 124 | (byte_size(ProxyTag)), %tls_string_len() 125 | ProxyTag/binary, %tls_string_data() 126 | 0, 0, 0 %tls_pad() 127 | >> 128 | | Msg 129 | ]; 130 | encode_packet(remote_closed, ConnId) -> 131 | <>. 132 | 133 | %% 134 | %% Middle-proxy side encoding and decodong (FOR TESTS ONLY!) 135 | %% 136 | 137 | %% opposite of encode_packet 138 | srv_decode_packet(<>) -> 142 | {data, ConnId, Data}; 143 | srv_decode_packet(<>) -> 144 | {remote_closed, ConId}. 145 | 146 | %% Opposite of decode_packet 147 | srv_encode_packet({proxy_ans, ConnId, Data}) -> 148 | <>; 152 | srv_encode_packet({close_ext, ConnId}) -> 153 | <>. 154 | 155 | %% IP and port as 10 + 2 + 4 + 4 = 20b 156 | -spec encode_ip_port(inet:ip_address(), inet:port_number()) -> iodata(). 157 | encode_ip_port(IPv4, Port) when tuple_size(IPv4) == 4 -> 158 | IpBin = inet_pton(IPv4), 159 | [lists:duplicate(10, <<0>>) 160 | | <<255,255, 161 | IpBin/binary, 162 | Port:32/little>>]; 163 | encode_ip_port(IPv6, Port) when tuple_size(IPv6) == 8 -> 164 | IpBin = inet_pton(IPv6), 165 | [IpBin, <>]. 166 | 167 | inet_pton({X1, X2, X3, X4}) -> 168 | <>; 169 | inet_pton(IPv6) when tuple_size(IPv6) == 8 -> 170 | << <> || I <- tuple_to_list(IPv6)>>. 171 | 172 | -ifdef(TEST). 173 | -include_lib("eunit/include/eunit.hrl"). 174 | 175 | tst_new() -> 176 | {{1, encode_ip_port({109, 238, 131, 159}, 1128), 177 | <<220,190,143,20,147,250,76,217,171,48,8,145,192,181,179,38>>}, 178 | encode_ip_port({80, 211, 29, 34}, 53634)}. 179 | 180 | decode_none_test() -> 181 | ?assertError(function_clause, decode_packet(<<>>)). 182 | 183 | encode_test() -> 184 | S = tst_new(), 185 | Samples = 186 | [{<<0,0,0,0,0,0,0,0,0,0,0,0,61,2,24,91,20,0,0,0,120,151,70,96,153,197,142,238,245,139,85,208,160,241,68,89,106,7,118,167>>, 187 | <<238,241,206,54,10,16,2,64,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,109,238,131,159,104,4,0,0,0,0,0,0,0,0,0,0,0,0,255,255,80,211,29,34,130,209,0,0,24,0,0,0,174,38,30,219,16,220,190,143,20,147,250,76,217,171,48,8,145,192,181,179,38,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,61,2,24,91,20,0,0,0,120,151,70,96,153,197,142,238,245,139,85,208,160,241,68,89,106,7,118,167>>}, 188 | {<<14,146,6,159,99,150,29,221,115,87,68,198,122,39,38,249,153,87,37,105,4,111,147,70,54,179,134,12,90,4,223,155,206,220,167,201,203,176,123,181,103,176,49,216,163,106,54,148,133,51,206,212,81,90,47,26,3,161,149,251,182,90,190,51,213,7,107,176,112,220,25,144,183,249,149,182,172,194,218,146,161,191,247,4,250,123,230,251,41,181,139,177,55,171,253,198,153,183,61,53,119,115,46,174,172,245,90,166,215,99,181,58,236,129,103,80,218,244,81,45,142,128,177,146,26,131,184,155,22,217,218,187,209,155,156,64,219,235,175,40,249,235,77,82,212,73,11,133,52,4,222,157,67,176,251,46,254,241,15,192,215,192,186,82,233,68,147,234,88,250,96,14,172,179,7,159,28,11,237,48,44,33,137,185,166,166,173,103,136,174,31,35,77,151,76,55,176,211,230,176,118,144,139,77,0,213,68,179,73,58,58,80,238,120,197,67,241,210,210,156,72,105,60,125,239,98,7,19,234,249,222,194,166,37,46,100,1,65,225,224,244,57,147,119,49,20,1,160,4,51,247,161,142,11,131,11,27,166,159,110,145,78,55,205,126,246,126,68,44,114,91,191,213,241,242,9,33,16,30,228>>, 189 | <<238,241,206,54,8,16,2,64,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,109,238,131,159,104,4,0,0,0,0,0,0,0,0,0,0,0,0,255,255,80,211,29,34,130,209,0,0,24,0,0,0,174,38,30,219,16,220,190,143,20,147,250,76,217,171,48,8,145,192,181,179,38,0,0,0,14,146,6,159,99,150,29,221,115,87,68,198,122,39,38,249,153,87,37,105,4,111,147,70,54,179,134,12,90,4,223,155,206,220,167,201,203,176,123,181,103,176,49,216,163,106,54,148,133,51,206,212,81,90,47,26,3,161,149,251,182,90,190,51,213,7,107,176,112,220,25,144,183,249,149,182,172,194,218,146,161,191,247,4,250,123,230,251,41,181,139,177,55,171,253,198,153,183,61,53,119,115,46,174,172,245,90,166,215,99,181,58,236,129,103,80,218,244,81,45,142,128,177,146,26,131,184,155,22,217,218,187,209,155,156,64,219,235,175,40,249,235,77,82,212,73,11,133,52,4,222,157,67,176,251,46,254,241,15,192,215,192,186,82,233,68,147,234,88,250,96,14,172,179,7,159,28,11,237,48,44,33,137,185,166,166,173,103,136,174,31,35,77,151,76,55,176,211,230,176,118,144,139,77,0,213,68,179,73,58,58,80,238,120,197,67,241,210,210,156,72,105,60,125,239,98,7,19,234,249,222,194,166,37,46,100,1,65,225,224,244,57,147,119,49,20,1,160,4,51,247,161,142,11,131,11,27,166,159,110,145,78,55,205,126,246,126,68,44,114,91,191,213,241,242,9,33,16,30,228>>}], 190 | lists:foreach( 191 | fun({In, Out}) -> 192 | Enc = encode_packet({data, In}, S), 193 | ?assertEqual(Out, iolist_to_binary(Enc)) 194 | end, Samples). 195 | 196 | decode_test() -> 197 | Samples = 198 | [{<<13,218,3,68,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,52,62,238,60,2,24,91,64,0,0,0,99,36,22,5,153,197,142,238,245,139,85,208,160,241,68,89,106,7,118,167,146,202,163,241,63,158,32,27,246,203,226,70,177,46,106,225,8,34,202,206,241,19,38,121,245,0,0,0,21,196,181,28,1,0,0,0,33,107,232,108,2,43,180,195>>, 199 | <<0,0,0,0,0,0,0,0,1,52,62,238,60,2,24,91,64,0,0,0,99,36,22,5,153,197,142,238,245,139,85,208,160,241,68,89,106,7,118,167,146,202,163,241,63,158,32,27,246,203,226,70,177,46,106,225,8,34,202,206,241,19,38,121,245,0,0,0,21,196,181,28,1,0,0,0,33,107,232,108,2,43,180,195>>}, 200 | {<<13,218,3,68,0,0,0,0,2,0,0,0,0,0,0,0,14,146,6,159,99,150,29,221,85,233,237,52,236,18,11,0,174,214,89,213,69,89,250,18,116,192,128,240,217,221,210,144,123,9,182,152,60,206,88,187,101,178,53,107,44,98,190,195,149,114,0,19,90,218,101,133,183,249,183,170,90,21,86,24,42,81,224,152,13,58,90,84,41,158,177,99,57,83,123,99,138,127,29,238,162,49,71,65,165,168,218,220,245,202,24,135,152,1,28,38,85,197,8,232,201,163,65,118,202,89,204,67,48,21,51,106,188,7,167,61,185,82,39,210,164,21,97,99,63,167,2,143,69,126,214,75,95,142,69,68,243,49,11,121,28,177,159,0,154,134,206,34>>, 201 | <<14,146,6,159,99,150,29,221,85,233,237,52,236,18,11,0,174,214,89,213,69,89,250,18,116,192,128,240,217,221,210,144,123,9,182,152,60,206,88,187,101,178,53,107,44,98,190,195,149,114,0,19,90,218,101,133,183,249,183,170,90,21,86,24,42,81,224,152,13,58,90,84,41,158,177,99,57,83,123,99,138,127,29,238,162,49,71,65,165,168,218,220,245,202,24,135,152,1,28,38,85,197,8,232,201,163,65,118,202,89,204,67,48,21,51,106,188,7,167,61,185,82,39,210,164,21,97,99,63,167,2,143,69,126,214,75,95,142,69,68,243,49,11,121,28,177,159,0,154,134,206,34>>}], 202 | lists:foreach( 203 | fun({In, Out}) -> 204 | {proxy_ans, _ConnId, Packet} = decode_packet(In), 205 | ?assertEqual(Out, iolist_to_binary(Packet)) 206 | end, Samples). 207 | 208 | %% decode_close_test() -> 209 | %% S = tst_new(), 210 | %% In = <<>>, 211 | %% ?assertError(rpc_close, try_decode_packet(In, S)). 212 | 213 | -endif. 214 | -------------------------------------------------------------------------------- /src/mtp_secure.erl: -------------------------------------------------------------------------------- 1 | %%% @author Sergey 2 | %%% @copyright (C) 2018, Sergey 3 | %%% @doc 4 | %%% MTProto intermediate protocol with random padding ("secure") 5 | %%% @end 6 | %%% Created : 29 Jun 2018 by Sergey 7 | 8 | -module(mtp_secure). 9 | 10 | -behaviour(mtp_codec). 11 | 12 | -export([new/0, 13 | try_decode_packet/2, 14 | encode_packet/2]). 15 | -export_type([codec/0]). 16 | 17 | -opaque codec() :: mtp_intermediate:codec(). 18 | 19 | new() -> 20 | mtp_intermediate:new(#{padding => true}). 21 | 22 | -spec try_decode_packet(binary(), codec()) -> {ok, binary(), binary(), codec()} 23 | | {incomplete, codec()}. 24 | try_decode_packet(Data, St) -> 25 | mtp_intermediate:try_decode_packet(Data, St). 26 | 27 | -spec encode_packet(iodata(), codec()) -> {iodata(), codec()}. 28 | encode_packet(Data, St) -> 29 | mtp_intermediate:encode_packet(Data, St). 30 | -------------------------------------------------------------------------------- /src/mtp_session_storage.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @doc 3 | %%% Storage to store last used sessions to protect from replay-attacks 4 | %%% used in some countries to detect mtproto proxy. 5 | %%% 6 | %%% Data is stored in ?DATA_TAB and there is additional index table ?HISTOGRAM_TAB, where 7 | %%% we store "secondary index" histogram: how many sessions have been added in each 5 minute 8 | %%% interval. It is used to make periodic cleanup procedure more efficient. 9 | %%% @end 10 | %%% Created : 19 May 2019 by Sergey 11 | %%%------------------------------------------------------------------- 12 | -module(mtp_session_storage). 13 | 14 | -behaviour(gen_server). 15 | 16 | %% API 17 | -export([start_link/0, 18 | check_add/1, 19 | status/0]). 20 | 21 | %% gen_server callbacks 22 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 23 | terminate/2, code_change/3]). 24 | 25 | -include_lib("stdlib/include/ms_transform.hrl"). 26 | -include_lib("hut/include/hut.hrl"). 27 | 28 | -define(DATA_TAB, ?MODULE). 29 | -define(HISTOGRAM_TAB, mtp_session_storage_histogram). 30 | 31 | %% 5-minute buckets 32 | -define(HISTOGRAM_BUCKET_SIZE, 300). 33 | -define(CHECK_INTERVAL, 60). 34 | 35 | -record(state, {data_tab = ets:tid(), 36 | histogram_tab = ets:tid(), 37 | clean_timer = gen_timeout:tout()}). 38 | 39 | %%%=================================================================== 40 | %%% API 41 | %%%=================================================================== 42 | start_link() -> 43 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 44 | 45 | %% @doc Add secret to the storage. Returns `new' if it was never used and `used' if it was 46 | %% already used before. 47 | -spec check_add(binary()) -> new | used. 48 | check_add(Packet) when byte_size(Packet) == 64 -> 49 | Now = erlang:system_time(second), 50 | check_add_at(Packet, Now). 51 | 52 | check_add_at(Packet, Now) -> 53 | Record = {fingerprint(Packet), Now}, 54 | HistogramBucket = bucket(Now), 55 | ets:update_counter(?HISTOGRAM_TAB, HistogramBucket, 1, {HistogramBucket, 0}), 56 | case ets:insert_new(?DATA_TAB, Record) of 57 | true -> 58 | new; 59 | false -> 60 | %% TODO: should decrement old record's histogram counter, but skip this for simplicity 61 | ets:insert(?DATA_TAB, Record), 62 | used 63 | end. 64 | 65 | -spec status() -> #{tab_size := non_neg_integer(), 66 | tab_memory_kb := non_neg_integer(), 67 | histogram_buckets := non_neg_integer(), 68 | histogram_size := non_neg_integer(), 69 | histogram_oldest := non_neg_integer()}. 70 | status() -> 71 | gen_server:call(?MODULE, status). 72 | 73 | %%%=================================================================== 74 | %%% gen_server callbacks 75 | %%%=================================================================== 76 | init([]) -> 77 | {DataTab, HistTab} = new_storage(), 78 | Timer = gen_timeout:new(#{timeout => ?CHECK_INTERVAL}), 79 | {ok, #state{data_tab = DataTab, 80 | histogram_tab = HistTab, 81 | clean_timer = Timer}}. 82 | 83 | handle_call(status, _From, #state{data_tab = DataTid, histogram_tab = HistTid} = State) -> 84 | Now = erlang:system_time(second), 85 | Size = ets:info(DataTid, size), 86 | Memory = tab_memory(DataTid), 87 | MemoryKb = round(Memory / 1024), 88 | HistSize = ets:info(HistTid, size), 89 | {HistOldest, HistTotal} = 90 | ets:foldl(fun({Bucket, Count}, {Oldest, Total}) -> 91 | {erlang:min(Oldest, bucket_to_ts(Bucket)), Total + Count} 92 | end, {Now, 0}, HistTid), 93 | Status = #{tab_size => Size, 94 | tab_memory_kb => MemoryKb, 95 | histogram_buckets => HistSize, 96 | histogram_size => HistTotal, 97 | histogram_oldest_ts => HistOldest, 98 | histogram_oldest_age => Now - HistOldest}, 99 | {reply, Status, State}. 100 | 101 | handle_cast(_Msg, State) -> 102 | {noreply, State}. 103 | 104 | handle_info(timeout, #state{data_tab = DataTab, histogram_tab = HistTab, clean_timer = Timer0} = State) -> 105 | Timer = 106 | case gen_timeout:is_expired(Timer0) of 107 | true -> 108 | Opts = application:get_env(mtproto_proxy, replay_check_session_storage_opts, 109 | #{max_age_minutes => 360}), 110 | Cleans = clean_storage(DataTab, HistTab, Opts), 111 | Remaining = ets:info(DataTab, size), 112 | ?log(info, "storage cleaned: ~p; remaining: ~p", [Cleans, Remaining]), 113 | gen_timeout:bump(gen_timeout:reset(Timer0)); 114 | false -> 115 | gen_timeout:reset(Timer0) 116 | end, 117 | {noreply, State#state{clean_timer = Timer}}. 118 | 119 | terminate(_Reason, _State) -> 120 | ok. 121 | 122 | code_change(_OldVsn, State, _Extra) -> 123 | {ok, State}. 124 | 125 | %%%=================================================================== 126 | %%% Internal functions 127 | %%%=================================================================== 128 | 129 | fingerprint(<<_:8/binary, KeyIV:(32 + 16)/binary, _:8/binary>>) -> 130 | %% It would be better to use whole 64b packet as fingerprint, but will use only 131 | %% 48b Key + IV part to save some space. 132 | binary:copy(KeyIV). 133 | 134 | bucket(Timestamp) -> 135 | Timestamp div ?HISTOGRAM_BUCKET_SIZE. 136 | 137 | bucket_to_ts(BucketTime) -> 138 | BucketTime * ?HISTOGRAM_BUCKET_SIZE. 139 | 140 | bucket_next(BucketTime) -> 141 | BucketTime + 1. 142 | 143 | 144 | new_storage() -> 145 | DataTab = ets:new(?DATA_TAB, [set, public, named_table, {write_concurrency, true}]), 146 | HistTab = ets:new(?HISTOGRAM_TAB, [set, public, named_table, {write_concurrency, true}]), 147 | {DataTab, HistTab}. 148 | 149 | 150 | clean_storage(DataTid, HistogramTid, CleanOpts) -> 151 | lists:filtermap(fun(Check) -> do_clean(DataTid, HistogramTid, CleanOpts, Check) end, 152 | [space, count, max_age]). 153 | 154 | do_clean(DataTid, HistTid, #{max_memory_mb := MaxMem}, space) -> 155 | TabMemBytes = tab_memory(DataTid), 156 | MaxMemBytes = MaxMem * 1024 * 1024, 157 | case TabMemBytes > MaxMemBytes of 158 | true -> 159 | PercentToShrink = (TabMemBytes - MaxMemBytes) / TabMemBytes, 160 | Removed = shrink_percent(DataTid, HistTid, PercentToShrink), 161 | {true, {space, Removed}}; 162 | false -> 163 | false 164 | end; 165 | do_clean(DataTid, HistTid, #{max_items := MaxItems}, count) -> 166 | Count = ets:info(DataTid, size), 167 | case Count > MaxItems of 168 | true -> 169 | PercentToShrink = (Count - MaxItems) / Count, 170 | Removed = shrink_percent(DataTid, HistTid, PercentToShrink), 171 | {true, {count, Removed}}; 172 | false -> 173 | false 174 | end; 175 | do_clean(DataTid, HistTid, #{max_age_minutes := MaxAge}, max_age) -> 176 | %% First scan histogram table, because it's cheaper 177 | CutBucket = bucket(erlang:system_time(second) - (MaxAge * 60)), 178 | HistMs = ets:fun2ms(fun({BucketTs, _}) when BucketTs =< CutBucket -> true end), 179 | case ets:select_count(HistTid, HistMs) of 180 | 0 -> 181 | false; 182 | _ -> 183 | Removed = remove_older(CutBucket, DataTid, HistTid), 184 | {true, {max_age, Removed}} 185 | end. 186 | 187 | 188 | tab_memory(Tid) -> 189 | WordSize = erlang:system_info(wordsize), 190 | Words = ets:info(Tid, memory), 191 | Words * WordSize. 192 | 193 | shrink_percent(DataTid, HistTid, Percent) when Percent < 1, 194 | Percent >= 0 -> 195 | Count = ets:info(DataTid, size), 196 | ToRemove = trunc(Count * Percent), 197 | HistByTime = lists:sort(ets:tab2list(HistTid)), % oldest first 198 | CutBucketTime = find_cut_bucket(HistByTime, ToRemove, 0), 199 | remove_older(CutBucketTime, DataTid, HistTid). 200 | 201 | %% Find the timestamp such that if we remove buckets that are older than this timestamp then we 202 | %% will remove at least `ToRemove' items. 203 | find_cut_bucket([{BucketTime, _}], _, _) -> 204 | BucketTime; 205 | find_cut_bucket([{BucketTime, Count} | Tail], ToRemove, Total) -> 206 | NewTotal = Total + Count, 207 | case NewTotal >= ToRemove of 208 | true -> 209 | BucketTime; 210 | false -> 211 | find_cut_bucket(Tail, ToRemove, NewTotal) 212 | end. 213 | 214 | %% @doc remove records that are in CutBucketTime bucket or older. 215 | %% Returns number of removed data records. 216 | -spec remove_older(integer(), ets:tid(), ets:tid()) -> non_neg_integer(). 217 | remove_older(CutBucketTime, DataTid, HistTid) -> 218 | %% | --- | --- | --- | -- 219 | %% ^ oldest bucket 220 | %% ^ 2nd bucket 221 | %% ^ 3rd bucket 222 | %% ^ current bucket 223 | %% If CutBucketTime is 2nd bucket, following will be removed: 224 | %% | --- | --- 225 | EdgeBucketTime = bucket_next(CutBucketTime), 226 | HistMs = ets:fun2ms(fun({BucketTs, _}) when BucketTs < EdgeBucketTime -> true end), 227 | DataCutTime = bucket_to_ts(EdgeBucketTime), 228 | DataMs = ets:fun2ms(fun({_, Time}) when Time =< DataCutTime -> true end), 229 | ets:select_delete(HistTid, HistMs), 230 | ets:select_delete(DataTid, DataMs). 231 | -------------------------------------------------------------------------------- /src/mtproto_proxy.app.src: -------------------------------------------------------------------------------- 1 | {application, mtproto_proxy, 2 | [{description, "High-performance Telegram MTProto proxy server"}, 3 | {vsn, "0.7.3"}, 4 | {registered, []}, 5 | {mod, { mtproto_proxy_app, []}}, 6 | {applications, 7 | [lager, 8 | ranch, 9 | erlang_psq, 10 | crypto, 11 | ssl, 12 | inets, 13 | kernel, 14 | stdlib 15 | ]}, 16 | {env,[ 17 | %% Interface to listen for incoming connections 18 | %% If not set, 0.0.0.0 will be used (listen on all IPs) 19 | {listen_ip, "0.0.0.0"}, 20 | %% You can add as much as you want. Names and ports should be unique 21 | {ports, [#{name => mtp_handler_1, 22 | port => 1443, 23 | %% You can tell each port to listen on specific IP. 24 | %% If not set, top-level listen_ip will be used. 25 | %% listen_ip => "1.2.3.4", 26 | 27 | %% secret should be 32 hex chars [0-9a-f] 28 | secret => <<"d0d6e111bada5511fcce9584deadbeef">>, 29 | %% tag is what you get from @MTProxybot 30 | tag => <<"dcbe8f1493fa4cd9ab300891c0b5b326">>}]}, 31 | 32 | %% number of socket acceptors (per-port) 33 | {num_acceptors, 60}, 34 | %% maximum number of open connections (per-port) 35 | {max_connections, 64000}, 36 | 37 | %% It's possible to forbid connection from telegram client to proxy 38 | %% with some of the protocols. Ti's recommended to set this to 39 | %% only `{allowed_protocols, [mtp_secure, mtp_fake_tls]}` because those 40 | %% protocols are more resistant to DPI detection. Connections by other 41 | %% protocols will be immediately disallowed. 42 | {allowed_protocols, [mtp_fake_tls, mtp_secure, mtp_abridged, mtp_intermediate]}, 43 | 44 | %% Connection policies. 45 | %% See README.md for documentation 46 | %% {policy, 47 | %% [{in_table, tls_domain, allowed_domains}, 48 | %% {not_in_table, [client_ipv4], banned_ips}, 49 | %% {max_connections, [port_name, tls_domain], 15} 50 | %% ]}, 51 | 52 | %% Close connection if it failed to perform handshake in this many seconds 53 | {init_timeout_sec, 60}, 54 | %% Switch client to memory-saving mode after this many seconds of inactivity 55 | {hibernate_timeout_sec, 60}, 56 | %% Close client connection after this many seconds of inactivity 57 | {ready_timeout_sec, 1200}, 58 | 59 | %% Telegram server uses your external IP address as part of encryption 60 | %% key, so, you should know it. 61 | %% You can configure IP lookup services by `ip_lookup_services' (should 62 | %% return my IP address as one line from this URL) or set IP address 63 | %% statically by `external_ip' (not both). 64 | %% If both are unset, proxy will try to guess IP address 65 | %% from getsockname(). 66 | %% `ip_lookup_services' will be tried one-by-one: if 1st is not responding, 67 | %% 2nd one will be tried and so on 68 | {ip_lookup_services, 69 | ["http://ipv4.seriyps.com/", 70 | "http://v4.ident.me/", 71 | "http://ipv4.icanhazip.com/", 72 | "https://digitalresistance.dog/myIp"]}, 73 | %% {external_ip, "YOUR.SERVER.EXTERNAL.IP"}, 74 | 75 | %% This option controls how proxy closes client sockets 76 | %% (SO_LINGER timeout=0 socket option) 77 | %% Can be useful if you have too many connection attempts with wrong 78 | %% secret and protocol, which creates lots of sockets in 79 | %% TIME_WAIT, ORPHANED and CLOSED state 80 | %% Allowed values: 81 | %% - off - never send RST 82 | %% - handshake_error - (recommended) only send if client handshake failed because of: 83 | %% - wrong secret 84 | %% - disabled protocol 85 | %% - replay attack detected 86 | %% - policy check failed 87 | %% - always - always close socket with RST 88 | {reset_close_socket, off}, 89 | 90 | %% List of enabled replay-attack checks. See 91 | %% https://habr.com/ru/post/452144/ 92 | 93 | %% server_error_filter - drop server error responses. 94 | %% Values: 95 | %% first - drop server error only if it's 1st server packet 96 | %% on - drop all server error packets 97 | %% off - don't drop server errors 98 | %% Default: off 99 | {replay_check_server_error_filter, first}, 100 | 101 | %% Store last used 1st client packets in special storage, drop 102 | %% connections with same 1st packet 103 | %% Values: on/off 104 | %% Default: off 105 | {replay_check_session_storage, on}, 106 | %% Options for `mtp_session_storage` replay attack check 107 | %% Those settings are not precise! They are checked not in realtime, but 108 | %% once per minute. 109 | {replay_check_session_storage_opts, 110 | #{%% Start to remove oldest items if there are more than max_items 111 | %% records in the storage 112 | max_items => 4000000, 113 | %% Start to remove oldest items if storage occupies more than 114 | %% `max_memory_mb` megabytes of memory 115 | %% One session uses ~130-150bytes on 64bit linux; 116 | %% 1Gb will be enough to store ~8mln sessions, which is 117 | %% 24 hours of ~90 connections per second 118 | max_memory_mb => 512, 119 | %% Remove items used for the last time more than `max_age_minutes` 120 | %% minutes ago. 121 | %% Less than 10 minutes doesn't make much sense 122 | max_age_minutes => 360}} 123 | 124 | %% Should be module with function `notify/4' exported. 125 | %% See mtp_metric:notify/4 for details 126 | %% {metric_backend, my_metric_backend}, 127 | 128 | %% User-space recv socket buffer sizes. Set to higher if you have 129 | %% enough RAM 130 | %% {upstream_socket_buffer_size, 51200}, %50kb 131 | %% {downstream_socket_buffer_size, 512000}, %500kb 132 | 133 | %% Whether we should check CRC32 sum of packets encoded with mtp_full 134 | %% codec. Setting this to `false' decreases CPU usage. Default: true 135 | %% {mtp_full_check_crc32, true}, 136 | 137 | %% Where to fetch telegram proxy configuration 138 | %% Mostly used to testing 139 | %% {proxy_secret_url, "https://core.telegram.org/getProxySecret"}, 140 | %% {proxy_config_url, "https://core.telegram.org/getProxyConfig"}, 141 | 142 | %% Upstream self-healthchecks tuning 143 | %% {upstream_healthchecks, 144 | %% [{qlen, 300}, % if queue length >X - close connection 145 | %% {gc, 409600}, % if connection memory >X - do garbage collection 146 | %% {total_mem, 3145728} % if connection memory >X - close connection 147 | %% ]}, 148 | 149 | %% Multiplexing tuning 150 | %% {init_dc_connections, 2}, 151 | %% {clients_per_dc_connection, 300}, 152 | 153 | %% Downstream backpressure tuning 154 | %% Values are configured per downstream connection, so, for example, if 155 | %% `clients_per_dc_connection' is 300 and current number of connections 156 | %% is 60,000, then there will be 200 downstream connections, each will 157 | %% keep reading data from it's socket unless there is 10mb of data or 158 | %% 600 packets not yet delivered by it's upstreams to clients 159 | %% `*_per_upstream' options are the same, but will be multiplied by the 160 | %% number of upstreams currently connected to this downstream 161 | %% {downstream_backpressure, 162 | %% #{%% 10mb; if not specified, it's 30kb * clients_per_dc_connection 163 | %% bytes_total => 10485760, 164 | %% %% if not specified, it's 2 * clients_per_dc_connection 165 | %% packets_total => 600, 166 | %% %% integer >= 1024 167 | %% %% if not specified this check is skipped 168 | %% bytes_per_upstream => 51200, %50kb 169 | %% %% float >= 1 170 | %% %% if not specified this check is skipped 171 | %% packets_per_upstream => 3}} 172 | ]}, 173 | {modules, []}, 174 | 175 | {licenses, ["Apache 2.0"]}, 176 | {links, [{"GitHub", "https://github.com/seriyps/mtproto_proxy"}]} 177 | ]}. 178 | -------------------------------------------------------------------------------- /src/mtproto_proxy_app.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc mtproto_proxy public API 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | 6 | -module(mtproto_proxy_app). 7 | 8 | -behaviour(application). 9 | 10 | %% Application callbacks 11 | -export([start/2, prep_stop/1, stop/1, config_change/3]). 12 | %% Helpers 13 | -export([mtp_listeners/0, 14 | reload_config/0, 15 | running_ports/0, 16 | start_proxy/1, 17 | build_urls/4, 18 | get_port_secret/1]). 19 | 20 | -define(APP, mtproto_proxy). 21 | 22 | -include_lib("hut/include/hut.hrl"). 23 | 24 | -type proxy_port() :: #{name := any(), 25 | port := inet:port_number(), 26 | secret := binary(), 27 | tag := binary(), 28 | listen_ip => string()}. 29 | 30 | %%==================================================================== 31 | %% Application behaviour API 32 | %%==================================================================== 33 | start(_StartType, _StartArgs) -> 34 | Res = {ok, _} = mtproto_proxy_sup:start_link(), 35 | report("+++++++++++++++++++++++++++++++++++++++~n" 36 | "Erlang MTProto proxy by @seriyps https://github.com/seriyps/mtproto_proxy~n" 37 | "Sponsored by and powers @socksy_bot~n", []), 38 | [start_proxy(Where) || Where <- application:get_env(?APP, ports, [])], 39 | Res. 40 | 41 | 42 | prep_stop(State) -> 43 | [stop_proxy(Where) || Where <- application:get_env(?APP, ports, [])], 44 | State. 45 | 46 | 47 | stop(_State) -> 48 | ok. 49 | 50 | 51 | config_change(Changed, New, Removed) -> 52 | %% app's env is already updated when this callback is called 53 | ok = lists:foreach(fun(K) -> config_changed(removed, K, []) end, Removed), 54 | ok = lists:foreach(fun({K, V}) -> config_changed(changed, K, V) end, Changed), 55 | ok = lists:foreach(fun({K, V}) -> config_changed(new, K, V) end, New). 56 | 57 | %%-------------------------------------------------------------------- 58 | %% Other APIs 59 | 60 | %% XXX: this is ad-hoc helper function; it is simplified version of code from OTP application_controller.erl 61 | reload_config() -> 62 | PreEnv = application:get_all_env(?APP), 63 | NewConfig = read_sys_config(), 64 | [application:set_env(?APP, K, V) || {K, V} <- NewConfig], 65 | NewEnv = application:get_all_env(?APP), 66 | %% TODO: "Removed" will always be empty; to handle it properly we should merge env 67 | %% from .app file with NewConfig 68 | {Changed, New, Removed} = diff_env(NewEnv, PreEnv), 69 | ?log(info, "Updating config; changed=~p, new=~p, deleted=~p", [Changed, New, Removed]), 70 | config_change(Changed, New, Removed). 71 | 72 | read_sys_config() -> 73 | {ok, [[File]]} = init:get_argument(config), 74 | {ok, [Data]} = file:consult(File), 75 | proplists:get_value(?APP, Data, []). 76 | 77 | diff_env(NewEnv, OldEnv) -> 78 | NewEnvMap = maps:from_list(NewEnv), 79 | OldEnvMap = maps:from_list(OldEnv), 80 | NewKeySet = ordsets:from_list(maps:keys(NewEnvMap)), 81 | OldKeySet = ordsets:from_list(maps:keys(OldEnvMap)), 82 | DelKeys = ordsets:subtract(OldKeySet, NewKeySet), 83 | AddKeys = ordsets:subtract(NewKeySet, OldKeySet), 84 | ChangedKeys = 85 | lists:filter( 86 | fun(K) -> 87 | maps:get(K, NewEnvMap) =/= maps:get(K, OldEnvMap) 88 | end, ordsets:intersection(OldKeySet, NewKeySet)), 89 | {[{K, maps:get(K, NewEnvMap)} || K <- ChangedKeys], 90 | [{K, maps:get(K, NewEnvMap)} || K <- AddKeys], 91 | DelKeys}. 92 | 93 | 94 | %% @doc List of ranch listeners running mtproto_proxy 95 | -spec mtp_listeners() -> [tuple()]. 96 | mtp_listeners() -> 97 | lists:filter( 98 | fun({_Name, Opts}) -> 99 | proplists:get_value(protocol, Opts) == mtp_handler 100 | end, 101 | ranch:info()). 102 | 103 | 104 | %% @doc Currently running listeners in a form of proxy_port() 105 | -spec running_ports() -> [proxy_port()]. 106 | running_ports() -> 107 | lists:map( 108 | fun({Name, Opts}) -> 109 | #{protocol_options := ProtoOpts, 110 | ip := Ip, 111 | port := Port} = maps:from_list(Opts), 112 | [Name, Secret, AdTag] = ProtoOpts, 113 | case inet:ntoa(Ip) of 114 | {error, einval} -> 115 | error({invalid_ip, Ip}); 116 | IpAddr -> 117 | #{name => Name, 118 | listen_ip => IpAddr, 119 | port => Port, 120 | secret => Secret, 121 | tag => AdTag} 122 | end 123 | end, mtp_listeners()). 124 | 125 | -spec get_port_secret(atom()) -> {ok, binary()} | not_found. 126 | get_port_secret(Name) -> 127 | case [Secret 128 | || #{name := PortName, secret := Secret} <- application:get_env(?APP, ports, []), 129 | PortName == Name] of 130 | [Secret] -> 131 | {ok, Secret}; 132 | _ -> 133 | not_found 134 | end. 135 | 136 | %%==================================================================== 137 | %% Internal functions 138 | %%==================================================================== 139 | -spec start_proxy(proxy_port()) -> {ok, pid()}. 140 | start_proxy(#{name := Name, port := Port, secret := Secret, tag := Tag} = P) -> 141 | ListenIpStr = maps:get( 142 | listen_ip, P, 143 | application:get_env(?APP, listen_ip, "0.0.0.0")), 144 | {ok, ListenIp} = inet:parse_address(ListenIpStr), 145 | Family = case tuple_size(ListenIp) of 146 | 4 -> inet; 147 | 8 -> inet6 148 | end, 149 | NumAcceptors = application:get_env(?APP, num_acceptors, 60), 150 | MaxConnections = application:get_env(?APP, max_connections, 10240), 151 | Res = 152 | ranch:start_listener( 153 | Name, ranch_tcp, 154 | #{socket_opts => [{ip, ListenIp}, 155 | {port, Port}, 156 | Family], 157 | num_acceptors => NumAcceptors, 158 | max_connections => MaxConnections}, 159 | mtp_handler, [Name, Secret, Tag]), 160 | Urls = build_urls(application:get_env(?APP, external_ip, ListenIpStr), 161 | Port, Secret, application:get_env(?APP, allowed_protocols, [])), 162 | UrlsStr = ["\n" | lists:join("\n", Urls)], 163 | report("Proxy started on ~s:~p with secret: ~s, tag: ~s~nLinks: ~s", 164 | [ListenIpStr, Port, Secret, Tag, UrlsStr]), 165 | Res. 166 | 167 | 168 | stop_proxy(#{name := Name}) -> 169 | ranch:stop_listener(Name). 170 | 171 | config_changed(_, ip_lookup_services, _) -> 172 | mtp_config:update(); 173 | config_changed(_, proxy_secret_url, _) -> 174 | mtp_config:update(); 175 | config_changed(_, proxy_config_url, _) -> 176 | mtp_config:update(); 177 | config_changed(Action, max_connections, N) when Action == new; Action == changed -> 178 | (is_integer(N) and (N >= 0)) orelse error({"max_connections should be non_neg_integer", N}), 179 | lists:foreach(fun({Name, _}) -> 180 | ranch:set_max_connections(Name, N) 181 | end, mtp_listeners()); 182 | config_changed(Action, downstream_socket_buffer_size, N) when Action == new; Action == changed -> 183 | [{ok, _} = mtp_down_conn:set_config(Pid, downstream_socket_buffer_size, N) 184 | || Pid <- downstream_connections()], 185 | ok; 186 | config_changed(Action, downstream_backpressure, BpOpts) when Action == new; Action == changed -> 187 | is_map(BpOpts) orelse error(invalid_downstream_backpressure), 188 | [{ok, _} = mtp_down_conn:set_config(Pid, downstream_backpressure, BpOpts) 189 | || Pid <- downstream_connections()], 190 | ok; 191 | %% Since upstream connections are mostly short-lived, live-update doesn't make much difference 192 | %% config_changed(Action, upstream_socket_buffer_size, N) when Action == new; Action == changed -> 193 | config_changed(Action, ports, Ports) when Action == new; Action == changed -> 194 | %% TODO: update secret or ad_tag without disconnect 195 | RanchPorts = ordsets:from_list(running_ports()), 196 | DefaultListenIp = #{listen_ip => application:get_env(?APP, listen_ip, "0.0.0.0")}, 197 | NewPorts = ordsets:from_list([maps:merge(DefaultListenIp, Port) 198 | || Port <- Ports]), 199 | ToStop = ordsets:subtract(RanchPorts, NewPorts), 200 | ToStart = ordsets:subtract(NewPorts, RanchPorts), 201 | lists:foreach(fun stop_proxy/1, ToStop), 202 | [{ok, _} = start_proxy(Conf) || Conf <- ToStart], 203 | ok; 204 | config_changed(Action, K, V) -> 205 | %% Most of the other config options are applied automatically without extra work 206 | ?log(info, "Config ~p ~p to ~p ignored", [K, Action, V]), 207 | ok. 208 | 209 | downstream_connections() -> 210 | [Pid || {_, Pid, worker, [mtp_down_conn]} <- supervisor:which_children(mtp_down_conn_sup)]. 211 | 212 | 213 | build_urls(Host, Port, Secret, Protocols) -> 214 | MkUrl = fun(ProtoSecret) -> 215 | io_lib:format( 216 | "https://t.me/proxy?server=~s&port=~w&secret=~s", 217 | [Host, Port, ProtoSecret]) 218 | end, 219 | UrlTypes = lists:usort( 220 | lists:map(fun(mtp_abridged) -> normal; 221 | (mtp_intermediate) -> normal; 222 | (Other) -> Other 223 | end, Protocols)), 224 | lists:map( 225 | fun(mtp_fake_tls) -> 226 | Domain = <<"s3.amazonaws.com">>, 227 | ProtoSecret = mtp_fake_tls:format_secret_hex(Secret, Domain), 228 | MkUrl(ProtoSecret); 229 | (mtp_secure) -> 230 | ProtoSecret = ["dd", Secret], 231 | MkUrl(ProtoSecret); 232 | (normal) -> 233 | MkUrl(Secret) 234 | end, UrlTypes). 235 | 236 | -ifdef(TEST). 237 | report(Fmt, Args) -> 238 | ?log(debug, Fmt, Args). 239 | -else. 240 | report(Fmt, Args) -> 241 | io:format(Fmt ++ "\n", Args), 242 | ?log(info, Fmt, Args). 243 | -endif. 244 | 245 | -ifdef(TEST). 246 | -include_lib("eunit/include/eunit.hrl"). 247 | 248 | env_diff_test() -> 249 | Pre = [{a, 1}, 250 | {b, 2}, 251 | {c, 3}], 252 | Post = [{b, 2}, 253 | {c, 4}, 254 | {d, 5}], 255 | ?assertEqual( 256 | {[{c, 4}], 257 | [{d, 5}], 258 | [a]}, 259 | diff_env(Post, Pre)). 260 | 261 | -endif. 262 | -------------------------------------------------------------------------------- /src/mtproto_proxy_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc mtproto_proxy top level supervisor. 3 | %% @end 4 | %%
 5 | %% dc_pool_sup (simple_one_for_one)
 6 | %%   dc_pool_1 [conn1, conn3, conn4, ..]
 7 | %%   dc_pool_-1 [conn2, ..]
 8 | %%   dc_pool_2 [conn5, conn7, ..]
 9 | %%   dc_pool_-2 [conn6, conn8, ..]
10 | %%   ...
11 | %% down_conn_sup (simple_one_for_one)
12 | %%   conn1
13 | %%   conn2
14 | %%   conn3
15 | %%   conn4
16 | %%   ...
17 | %%   connN
18 | %% 
19 | %%%------------------------------------------------------------------- 20 | 21 | -module(mtproto_proxy_sup). 22 | 23 | -behaviour(supervisor). 24 | 25 | %% API 26 | -export([start_link/0]). 27 | 28 | %% Supervisor callbacks 29 | -export([init/1]). 30 | 31 | -define(SERVER, ?MODULE). 32 | 33 | %%==================================================================== 34 | %% API functions 35 | %%==================================================================== 36 | 37 | start_link() -> 38 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 39 | 40 | %%==================================================================== 41 | %% Supervisor callbacks 42 | %%==================================================================== 43 | 44 | init([]) -> 45 | SupFlags = #{strategy => one_for_all, %TODO: maybe change strategy 46 | intensity => 50, 47 | period => 5}, 48 | Childs = [#{id => mtp_down_conn_sup, 49 | type => supervisor, 50 | start => {mtp_down_conn_sup, start_link, []}}, 51 | #{id => mtp_dc_pool_sup, 52 | type => supervisor, 53 | start => {mtp_dc_pool_sup, start_link, []}}, 54 | #{id => mtp_config, 55 | start => {mtp_config, start_link, []}}, 56 | #{id => mtp_session_storage, 57 | start => {mtp_session_storage, start_link, []}}, 58 | #{id => mtp_policy_table, 59 | start => {mtp_policy_table, start_link, []}}, 60 | #{id => mtp_policy_counter, 61 | start => {mtp_policy_counter, start_link, []}} 62 | ], 63 | {ok, {SupFlags, Childs}}. 64 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Script that helps to overwrite port/secret/ad tag from command line without changing config-files 3 | 4 | CMD="/opt/mtp_proxy/bin/mtp_proxy foreground" 5 | # CMD="/opt/mtp_proxy/bin/mtp_proxy console" 6 | THIS=$0 7 | 8 | usage() { 9 | echo "Usage:" 10 | echo "To run with settings from config/prod-sys.config:" 11 | echo "${THIS}" 12 | echo "To start in single-port mode configured from command-line:" 13 | echo "${THIS} -p -s -t " 14 | echo "To only allow connections with randomized protocol (dd-secrets):" 15 | echo "${THIS} -a dd" 16 | echo "Parameters:" 17 | echo "-p : port to listen on. 1-65535" 18 | echo "-s : proxy secret. 32 hex characters 0-9 a-f" 19 | echo "-t : promo tag that you get from @MTProxybot. 32 hex characters" 20 | echo "-a dd: only allow 'secure' connections (with dd-secret) / fake-tls connections (base64 secrets)" 21 | echo "-a tls: only allow 'fake-tls' connections (base64 secrets)" 22 | echo "It's ok to provide both '-a dd -a tls'." 23 | echo "port, secret, tag and allowed protocols can also be configured via environment variables:" 24 | echo "MTP_PORT, MTP_SECRET, MTP_TAG, MTP_DD_ONLY, MTP_TLS_ONLY" 25 | echo "If both command line and environment are set, command line have higher priority." 26 | } 27 | 28 | error() { 29 | echo "ERROR: ${1}" 30 | usage 31 | exit 1 32 | } 33 | 34 | # check environment variables 35 | PORT=${MTP_PORT:-""} 36 | SECRET=${MTP_SECRET:-""} 37 | TAG=${MTP_TAG:-""} 38 | DD_ONLY=${MTP_DD_ONLY:-""} 39 | TLS_ONLY=${MTP_TLS_ONLY:-""} 40 | 41 | # check command line options 42 | while getopts "p:s:t:a:dh" o; do 43 | case "${o}" in 44 | p) 45 | PORT=${OPTARG} 46 | ;; 47 | s) 48 | SECRET=${OPTARG} 49 | ;; 50 | t) 51 | TAG=${OPTARG} 52 | ;; 53 | a) 54 | if [ "${OPTARG}" = "dd" ]; then 55 | DD_ONLY="y" 56 | elif [ "${OPTARG}" = "tls" ]; then 57 | TLS_ONLY="y" 58 | else 59 | error "Invalid -a value: '${OPTARG}'" 60 | fi 61 | ;; 62 | d) 63 | echo "Warning: -d is deprecated! use '-a dd' instead" 64 | DD_ONLY="y" 65 | ;; 66 | h) 67 | usage 68 | exit 0 69 | esac 70 | done 71 | 72 | PROTO_ARG="" 73 | 74 | if [ -n "${DD_ONLY}" -a -n "${TLS_ONLY}" ]; then 75 | PROTO_ARG='-mtproto_proxy allowed_protocols [mtp_fake_tls,mtp_secure]' 76 | elif [ -n "${DD_ONLY}" ]; then 77 | PROTO_ARG='-mtproto_proxy allowed_protocols [mtp_secure]' 78 | elif [ -n "${TLS_ONLY}" ]; then 79 | PROTO_ARG='-mtproto_proxy allowed_protocols [mtp_fake_tls]' 80 | fi 81 | 82 | # if at least one option is set... 83 | if [ -n "${PORT}" -o -n "${SECRET}" -o -n "${TAG}" ]; then 84 | # If at least one of them not set... 85 | [ -z "${PORT}" -o -z "${SECRET}" -o -z "${TAG}" ] && \ 86 | error "Not enough options: -p '${PORT}' -s '${SECRET}' -t '${TAG}'" 87 | 88 | # validate format 89 | [ ${PORT} -gt 0 -a ${PORT} -lt 65535 ] || \ 90 | error "Invalid port value: ${PORT}" 91 | [ -n "`echo $SECRET | grep -x '[[:xdigit:]]\{32\}'`" ] || \ 92 | error "Invalid secret. Should be 32 chars of 0-9 a-f" 93 | [ -n "`echo $TAG | grep -x '[[:xdigit:]]\{32\}'`" ] || \ 94 | error "Invalid tag. Should be 32 chars of 0-9 a-f" 95 | 96 | exec $CMD $PROTO_ARG -mtproto_proxy ports "[#{name => mtproto_proxy, port => $PORT, secret => <<\"$SECRET\">>, tag => <<\"$TAG\">>}]" 97 | else 98 | exec $CMD $PROTO_ARG 99 | fi 100 | -------------------------------------------------------------------------------- /test/bench_codec_decode.erl: -------------------------------------------------------------------------------- 1 | %%% @author Sergey 2 | %%% @copyright (C) 2019, Sergey 3 | %%% @doc 4 | %%% Decoding benchmarks for codecs 5 | %%% @end 6 | %%% Created : 7 Sep 2019 by Sergey 7 | 8 | -module(bench_codec_decode). 9 | 10 | -export([fake_tls/1, bench_fake_tls/2, 11 | intermediate/1, bench_intermediate/2, 12 | secure/1, bench_secure/2, 13 | full/1, bench_full/2, 14 | full_nocheck/1, bench_full_nocheck/2, 15 | aes_cbc/1, bench_aes_cbc/2, 16 | obfuscated/1, bench_obfuscated/2, 17 | fold_dd_codec/1, bench_fold_dd_codec/2, 18 | fold_tls_codec/1, bench_fold_tls_codec/2, 19 | fold_backend_codec/1, bench_fold_backend_codec/2]). 20 | 21 | 22 | %% @doc bench mtp_fake_tls decoding 23 | fake_tls(init) -> 24 | mtp_fake_tls:new(); 25 | fake_tls({input, Codec}) -> 26 | Packets = mk_front_packets(), 27 | encode_all(Packets, mtp_fake_tls, Codec). 28 | 29 | bench_fake_tls(Stream, Codec) -> 30 | mtp_fake_tls:decode_all(Stream, Codec). 31 | 32 | 33 | %% @doc bench mtp_intermediate decoding 34 | intermediate(init) -> 35 | mtp_intermediate:new(); 36 | intermediate({input, Codec}) -> 37 | Packets = mk_front_packets(), 38 | encode_all(Packets, mtp_intermediate, Codec). 39 | 40 | bench_intermediate(Stream, Codec) -> 41 | decode_all_intermediate(Stream, Codec). 42 | 43 | decode_all_intermediate(Stream0, Codec0) -> 44 | case mtp_intermediate:try_decode_packet(Stream0, Codec0) of 45 | {ok, Pkt, Stream1, Codec1} -> 46 | [Pkt | decode_all_intermediate(Stream1, Codec1)]; 47 | {incomplete, _} -> 48 | [] 49 | end. 50 | 51 | 52 | %% @doc bench mtp_secure decoding 53 | secure(init) -> 54 | mtp_secure:new(); 55 | secure({input, Codec}) -> 56 | Packets = mk_front_packets(), 57 | encode_all(Packets, mtp_secure, Codec). 58 | 59 | bench_secure(Stream, Codec) -> 60 | decode_all_secure(Stream, Codec). 61 | 62 | decode_all_secure(Stream0, Codec0) -> 63 | case mtp_secure:try_decode_packet(Stream0, Codec0) of 64 | {ok, Pkt, Stream1, Codec1} -> 65 | [Pkt | decode_all_secure(Stream1, Codec1)]; 66 | {incomplete, _} -> 67 | [] 68 | end. 69 | 70 | 71 | %% @doc bench mtp_full decoding 72 | full(init) -> 73 | mtp_full:new(); 74 | full({input, Codec}) -> 75 | Packets = mk_front_packets(), 76 | encode_all(Packets, mtp_full, Codec). 77 | 78 | bench_full(Stream, Codec) -> 79 | decode_all_full(Stream, Codec). 80 | 81 | decode_all_full(Stream0, Codec0) -> 82 | case mtp_full:try_decode_packet(Stream0, Codec0) of 83 | {ok, Pkt, Stream1, Codec1} -> 84 | [Pkt | decode_all_full(Stream1, Codec1)]; 85 | {incomplete, _} -> 86 | [] 87 | end. 88 | 89 | 90 | %% @doc bench mtp_full with disabled CRC32 verification check 91 | full_nocheck(init) -> 92 | mtp_full:new(0, 0, false); 93 | full_nocheck({input, Codec}) -> 94 | Packets = mk_front_packets(), 95 | encode_all(Packets, mtp_full, Codec). 96 | 97 | bench_full_nocheck(Stream, Codec) -> 98 | decode_all_full(Stream, Codec). 99 | 100 | 101 | %% @doc bench aes_cbc decryption 102 | aes_cbc(init) -> 103 | Key = binary:copy(<<0>>, 32), 104 | IV = binary:copy(<<1>>, 16), 105 | BlockSize = 16, 106 | mtp_aes_cbc:new(Key, IV, Key, IV, BlockSize); 107 | aes_cbc({input, Codec}) -> 108 | Packets = mk_front_packets(), 109 | {Stream, _} = mtp_aes_cbc:encrypt(Packets, Codec), 110 | Stream. 111 | 112 | bench_aes_cbc(Stream, Codec) -> 113 | {Dec, <<>>, _} = mtp_aes_cbc:decrypt(Stream, Codec), 114 | Dec. 115 | 116 | 117 | %% @doc decrypt mtp_obfuscated 118 | obfuscated(init) -> 119 | Key = binary:copy(<<0>>, 32), 120 | IV = binary:copy(<<1>>, 16), 121 | mtp_obfuscated:new(Key, IV, Key, IV); 122 | obfuscated({input, Codec}) -> 123 | Packets = mk_front_packets(), 124 | {Stream, _} = mtp_obfuscated:encrypt(Packets, Codec), 125 | Stream. 126 | 127 | bench_obfuscated(Stream, Codec) -> 128 | {Dec, <<>>, _} = mtp_obfuscated:decrypt(Stream, Codec), 129 | Dec. 130 | 131 | %% @doc "codec" that is used for "dd" secrets 132 | fold_dd_codec(init) -> 133 | Key = binary:copy(<<0>>, 32), 134 | IV = binary:copy(<<1>>, 16), 135 | CryptoSt = mtp_obfuscated:new(Key, IV, Key, IV), 136 | PacketSt = mtp_secure:new(), 137 | mtp_codec:new(mtp_obfuscated, CryptoSt, 138 | mtp_secure, PacketSt); 139 | fold_dd_codec({input, Codec}) -> 140 | Packets = mk_front_packets(), 141 | encode_all(Packets, mtp_codec, Codec). 142 | 143 | bench_fold_dd_codec(Stream, Codec) -> 144 | codec_fold(Stream, Codec). 145 | 146 | 147 | %% @doc "codec" that is used for "fake-tls" connections 148 | fold_tls_codec(init) -> 149 | Key = binary:copy(<<0>>, 32), 150 | IV = binary:copy(<<1>>, 16), 151 | CryptoSt = mtp_obfuscated:new(Key, IV, Key, IV), 152 | PacketSt = mtp_secure:new(), 153 | TlsSt = mtp_fake_tls:new(), 154 | mtp_codec:new(mtp_obfuscated, CryptoSt, 155 | mtp_secure, PacketSt, 156 | true, TlsSt, 157 | 10 * 1024 * 1024); 158 | fold_tls_codec({input, Codec}) -> 159 | Packets = mk_front_packets(), 160 | encode_all(Packets, mtp_codec, Codec). 161 | 162 | bench_fold_tls_codec(Stream, Codec) -> 163 | codec_fold(Stream, Codec). 164 | 165 | 166 | %% @doc codec that is used for connections to telegram datacenter 167 | fold_backend_codec(init) -> 168 | Key = binary:copy(<<0>>, 32), 169 | IV = binary:copy(<<1>>, 16), 170 | BlockSize = 16, 171 | CryptoSt = mtp_aes_cbc:new(Key, IV, Key, IV, BlockSize), 172 | PacketSt = mtp_full:new(), 173 | mtp_codec:new(mtp_aes_cbc, CryptoSt, 174 | mtp_full, PacketSt); 175 | fold_backend_codec({input, Codec}) -> 176 | Packets = mk_front_packets(), 177 | encode_all(Packets, mtp_codec, Codec). 178 | 179 | bench_fold_backend_codec(Stream, Codec) -> 180 | codec_fold(Stream, Codec). 181 | 182 | 183 | %% 184 | %% Helpers 185 | 186 | mk_front_packets() -> 187 | %% Histogram from live server shows that majority of Telegram protocol packets from client to 188 | %% proxy are between 128 to 512 bytes. 189 | %% Network (tcp) data chunk size depends on sock buffewr, but is usually between 128b to 2kb. 190 | Packet = binary:copy(<<0>>, 256), 191 | lists:duplicate(8, Packet). 192 | 193 | encode_all(Pkts, Codec, St0) -> 194 | {Stream, _} = 195 | lists:foldl( 196 | fun(Pkt, {Acc, St}) -> 197 | {Enc, St1} = Codec:encode_packet(Pkt, St), 198 | {<>, St1} 199 | end, {<<>>, St0}, Pkts), 200 | Stream. 201 | 202 | codec_fold(Stream, Codec) -> 203 | mtp_codec:fold_packets( 204 | fun(Pkt, Acc, Codec1) -> 205 | {[Pkt | Acc], Codec1} 206 | end, [], Stream, Codec). 207 | -------------------------------------------------------------------------------- /test/bench_codec_encode.erl: -------------------------------------------------------------------------------- 1 | %%% @author Sergey 2 | %%% @copyright (C) 2019, Sergey 3 | %%% @doc 4 | %%% Encoding benchmark for codecs 5 | %%% @end 6 | %%% Created : 15 Sep 2019 by Sergey 7 | 8 | -module(bench_codec_encode). 9 | 10 | -export([fake_tls/1, bench_fake_tls/2, 11 | intermediate/1, bench_intermediate/2, 12 | secure/1, bench_secure/2, 13 | full/1, bench_full/2, 14 | aes_cbc/1, bench_aes_cbc/2, 15 | obfuscated/1, bench_obfuscated/2, 16 | dd_codec/1, bench_dd_codec/2, 17 | fake_tls_codec/1, bench_fake_tls_codec/2, 18 | backend_codec/1, bench_backend_codec/2 19 | ]). 20 | 21 | 22 | fake_tls({input, _}) -> 23 | mk_back_packets(). 24 | 25 | bench_fake_tls(Packets, _) -> 26 | Codec0 = mtp_fake_tls:new(), 27 | lists:foldl( 28 | fun(Pkt, Codec1) -> 29 | {_Enc, Codec2} = mtp_fake_tls:encode_packet(Pkt, Codec1), 30 | Codec2 31 | end, Codec0, Packets). 32 | 33 | 34 | intermediate({input, _}) -> 35 | mk_back_packets(). 36 | 37 | bench_intermediate(Packets, _) -> 38 | Codec0 = mtp_intermediate:new(), 39 | lists:foldl( 40 | fun(Pkt, Codec1) -> 41 | {_Enc, Codec2} = mtp_intermediate:encode_packet(Pkt, Codec1), 42 | Codec2 43 | end, Codec0, Packets). 44 | 45 | 46 | secure({input, _}) -> 47 | mk_back_packets(). 48 | 49 | bench_secure(Packets, _) -> 50 | Codec0 = mtp_secure:new(), 51 | lists:foldl( 52 | fun(Pkt, Codec1) -> 53 | {_Enc, Codec2} = mtp_secure:encode_packet(Pkt, Codec1), 54 | Codec2 55 | end, Codec0, Packets). 56 | 57 | 58 | full({input, _}) -> 59 | mk_back_packets(). 60 | 61 | bench_full(Packets, _) -> 62 | Codec0 = mtp_full:new(), 63 | lists:foldl( 64 | fun(Pkt, Codec1) -> 65 | {_Enc, Codec2} = mtp_full:encode_packet(Pkt, Codec1), 66 | Codec2 67 | end, Codec0, Packets). 68 | 69 | 70 | aes_cbc(init) -> 71 | Key = binary:copy(<<0>>, 32), 72 | IV = binary:copy(<<1>>, 16), 73 | BlockSize = 16, 74 | mtp_aes_cbc:new(Key, IV, Key, IV, BlockSize); 75 | aes_cbc({input, _}) -> 76 | mk_back_packets(). 77 | 78 | bench_aes_cbc(Packets, Codec0) -> 79 | lists:foldl( 80 | fun(Pkt, Codec1) -> 81 | {_Enc, Codec2} = mtp_aes_cbc:encrypt(Pkt, Codec1), 82 | Codec2 83 | end, Codec0, Packets). 84 | 85 | 86 | obfuscated(init) -> 87 | Key = binary:copy(<<0>>, 32), 88 | IV = binary:copy(<<1>>, 16), 89 | mtp_obfuscated:new(Key, IV, Key, IV); 90 | obfuscated({input, _}) -> 91 | mk_back_packets(). 92 | 93 | bench_obfuscated(Packets, Codec0) -> 94 | lists:foldl( 95 | fun(Pkt, Codec1) -> 96 | {_Enc, Codec2} = mtp_obfuscated:encrypt(Pkt, Codec1), 97 | Codec2 98 | end, Codec0, Packets). 99 | 100 | 101 | dd_codec(init) -> 102 | Key = binary:copy(<<0>>, 32), 103 | IV = binary:copy(<<1>>, 16), 104 | CryptoSt = mtp_obfuscated:new(Key, IV, Key, IV), 105 | PacketSt = mtp_secure:new(), 106 | mtp_codec:new(mtp_obfuscated, CryptoSt, 107 | mtp_secure, PacketSt); 108 | dd_codec({input, _}) -> 109 | mk_back_packets(). 110 | 111 | bench_dd_codec(Packets, Codec) -> 112 | codec_encode(Packets, Codec). 113 | 114 | 115 | fake_tls_codec(init) -> 116 | Key = binary:copy(<<0>>, 32), 117 | IV = binary:copy(<<1>>, 16), 118 | CryptoSt = mtp_obfuscated:new(Key, IV, Key, IV), 119 | PacketSt = mtp_secure:new(), 120 | TlsSt = mtp_fake_tls:new(), 121 | mtp_codec:new(mtp_obfuscated, CryptoSt, 122 | mtp_secure, PacketSt, 123 | true, TlsSt, 124 | 10 * 1024 * 1024); 125 | fake_tls_codec({input, _}) -> 126 | mk_back_packets(). 127 | 128 | bench_fake_tls_codec(Packets, Codec) -> 129 | codec_encode(Packets, Codec). 130 | 131 | 132 | backend_codec(init) -> 133 | Key = binary:copy(<<0>>, 32), 134 | IV = binary:copy(<<1>>, 16), 135 | BlockSize = 16, 136 | CryptoSt = mtp_aes_cbc:new(Key, IV, Key, IV, BlockSize), 137 | PacketSt = mtp_full:new(), 138 | mtp_codec:new(mtp_aes_cbc, CryptoSt, 139 | mtp_full, PacketSt); 140 | backend_codec({input, _}) -> 141 | mk_back_packets(). 142 | 143 | bench_backend_codec(Packets, Codec) -> 144 | codec_encode(Packets, Codec). 145 | 146 | 147 | %% 148 | %% Helpers 149 | 150 | mk_back_packets() -> 151 | %% Histogram from live server shows that majority of Telegram protocol packets from server to 152 | %% proxy are between 32b to 2kb. 153 | %% Network (tcp) data chunk size depends on sock buffewr, but is usually distributed 154 | %% evenly in logarithmic scale: 32b - 128b - 512b - 2kb - 8kb - 33kb 155 | %% Majority is from 128b to 512b 156 | Packet = binary:copy(<<0>>, 512), 157 | lists:duplicate(4, Packet). 158 | 159 | codec_encode(Packets, Codec0) -> 160 | lists:foldl( 161 | fun(Pkt, Codec1) -> 162 | {_Enc, Codec2} = mtp_codec:encode_packet(Pkt, Codec1), 163 | Codec2 164 | end, Codec0, Packets). 165 | -------------------------------------------------------------------------------- /test/mtp_prop_gen.erl: -------------------------------------------------------------------------------- 1 | %% @doc Common data generators for property-based tests 2 | 3 | -module(mtp_prop_gen). 4 | -include_lib("proper/include/proper.hrl"). 5 | 6 | -export([stream_4b/0, 7 | packet_4b/0, 8 | stream_16b/0, 9 | packet_16b/0, 10 | binary/2, 11 | aligned_binary/3, 12 | key/0, 13 | iv/0, 14 | secret/0, 15 | dc_id/0, 16 | codec/0 17 | ]). 18 | 19 | 20 | %% 4-byte aligned packet: `binary()` 21 | packet_4b() -> 22 | ?LET(IoList, proper_types:non_empty(proper_types:list(proper_types:binary(4))), 23 | iolist_to_binary(IoList)). 24 | 25 | %% List of 4-byte aligned packets: `[binary()]` 26 | stream_4b() -> 27 | proper_types:list(packet_4b()). 28 | 29 | %% 16-byte aligned packet: `binary()` 30 | packet_16b() -> 31 | ?LET(IoList, proper_types:non_empty(proper_types:list(proper_types:binary(16))), 32 | iolist_to_binary(IoList)). 33 | 34 | %% List of 16-byte aligned packets: `[binary()]` 35 | stream_16b() -> 36 | proper_types:list(packet_16b()). 37 | 38 | %% Binary of size between Min and Max 39 | binary(Min, Max) when Min < Max -> 40 | ?LET(Size, proper_types:integer(Min, Max), proper_types:binary(Size)). 41 | 42 | %% Binary of size between Min and Max aligned by Align 43 | aligned_binary(Align, Min0, Max0) when Min0 > Align, 44 | Max0 > Min0 -> 45 | Ceil = fun(V) -> V - (V rem Align) end, 46 | Min = Ceil(Min0), 47 | Max = Ceil(Max0), 48 | ?LET(Size, proper_types:integer(Min, Max), proper_types:binary(Ceil(Size))). 49 | 50 | %% 32-byte encryption key: `binary()` 51 | key() -> 52 | proper_types:binary(32). 53 | 54 | %% 16-byte encryption initialization vector: `binary()` 55 | iv() -> 56 | proper_types:binary(16). 57 | 58 | %% 16-byte secret: `binary()` 59 | secret() -> 60 | proper_types:binary(16). 61 | 62 | %% Datacenter ID: `[-9..9]` 63 | dc_id() -> 64 | proper_types:integer(-9, 9). 65 | 66 | codec() -> 67 | Protocols = [mtp_abridged, mtp_intermediate, mtp_secure], 68 | proper_types:oneof(Protocols). 69 | -------------------------------------------------------------------------------- /test/mtp_test_client.erl: -------------------------------------------------------------------------------- 1 | %% @doc Simple mtproto proxy client 2 | -module(mtp_test_client). 3 | 4 | -export([connect/5, 5 | connect/6, 6 | send/2, 7 | recv_packet/2, 8 | recv_all/2, 9 | close/1, 10 | ping_session/6]). 11 | 12 | -export([unencrypted_cli_packet/1, 13 | unencrypted_cli_packet/3, 14 | parse_unencrypted_srv_packet/1]). 15 | -export([req_pq/0, 16 | res_pq_matches/2, 17 | ping/0, 18 | pong_matches/2]). 19 | 20 | -export_type([client/0]). 21 | 22 | -record(client, 23 | {sock, 24 | codec}). 25 | 26 | -opaque client() :: #client{}. 27 | -type tcp_error() :: inet:posix() | closed. % | timeout. 28 | 29 | connect(Host, Port, Secret, DcId, Protocol) -> 30 | Seed = crypto:strong_rand_bytes(58), 31 | connect(Host, Port, Seed, Secret, DcId, Protocol). 32 | 33 | -spec connect(inet:socket_address() | inet:hostname(), 34 | inet:port_number(), 35 | binary(), binary(), integer(), 36 | mtp_codec:packet_codec() | {mtp_fake_tls, binary()}) -> client(). 37 | connect(Host, Port, Seed, Secret, DcId, Protocol0) -> 38 | Opts = [{packet, raw}, 39 | {mode, binary}, 40 | {active, false}, 41 | {buffer, 1024}, 42 | {send_timeout, 5000}], 43 | {ok, Sock} = gen_tcp:connect(Host, Port, Opts, 1000), 44 | {Protocol, TlsEnabled, TlsSt} = 45 | case Protocol0 of 46 | {mtp_fake_tls, Domain} -> 47 | ClientHello = mtp_fake_tls:make_client_hello(Secret, Domain), 48 | ok = gen_tcp:send(Sock, ClientHello), 49 | %% Let's hope whole server hello will arrive in a single chunk 50 | {ok, ServerHello} = gen_tcp:recv(Sock, 0, 5000), 51 | %% TODO: if Tail is not empty, use codec:push_back(first, ..) 52 | {_HS, _CC, _D, <<>>} = mtp_fake_tls:parse_server_hello(ServerHello), 53 | {mtp_secure, true, mtp_fake_tls:new()}; 54 | _ -> {Protocol0, false, undefined} 55 | end, 56 | {Header0, _, _, CryptoLayer} = mtp_obfuscated:client_create(Seed, Secret, Protocol, DcId), 57 | NoopSt = mtp_noop_codec:new(), 58 | %% First, create codec with just TLS (which might be noop as well) to encode "obfuscated" header 59 | Codec0 = mtp_codec:new(mtp_noop_codec, NoopSt, 60 | mtp_noop_codec, NoopSt, 61 | TlsEnabled, TlsSt, 62 | 25 * 1024 * 1024), 63 | {Header, Codec1} = mtp_codec:encode_packet(Header0, Codec0), 64 | ok = gen_tcp:send(Sock, Header), 65 | PacketLayer = Protocol:new(), 66 | Codec2 = mtp_codec:replace(crypto, mtp_obfuscated, CryptoLayer, Codec1), 67 | Codec3 = mtp_codec:replace(packet, Protocol, PacketLayer, Codec2), 68 | #client{sock = Sock, 69 | codec = Codec3}. 70 | 71 | send(Data, #client{sock = Sock, codec = Codec} = Client) -> 72 | {Enc, Codec1} = mtp_codec:encode_packet(Data, Codec), 73 | ok = gen_tcp:send(Sock, Enc), 74 | Client#client{codec = Codec1}. 75 | 76 | -spec recv_packet(client(), timeout()) -> {ok, iodata(), client()} | {error, tcp_error() | timeout}. 77 | recv_packet(#client{codec = Codec} = Client, Timeout) -> 78 | case mtp_codec:try_decode_packet(<<>>, Codec) of 79 | {ok, Data, Codec1} -> 80 | %% We already had some data in codec's buffers 81 | {ok, Data, Client#client{codec = Codec1}}; 82 | {incomplete, Codec1} -> 83 | recv_packet_inner(Client#client{codec = Codec1}, Timeout) 84 | end. 85 | 86 | recv_packet_inner(#client{sock = Sock, codec = Codec0} = Client, Timeout) -> 87 | case gen_tcp:recv(Sock, 0, Timeout) of 88 | {ok, Stream} -> 89 | %% io:format("~p: ~p~n", [byte_size(Stream), Stream]), 90 | case mtp_codec:try_decode_packet(Stream, Codec0) of 91 | {ok, Data, Codec} -> 92 | {ok, Data, Client#client{codec = Codec}}; 93 | {incomplete, Codec} -> 94 | %% recurse 95 | recv_packet_inner(Client#client{codec = Codec}, Timeout) 96 | end; 97 | Err -> 98 | Err 99 | end. 100 | 101 | -spec recv_all(client(), timeout()) -> {ok, [iodata()], client()} | {error, tcp_error()}. 102 | recv_all(#client{sock = Sock, codec = Codec0} = Client, Timeout) -> 103 | case tcp_recv_all(Sock, Timeout) of 104 | {ok, Stream} -> 105 | %% io:format("~p: ~p~n", [byte_size(Stream), Stream]), 106 | {ok, Packets, Codec} = 107 | mtp_codec:fold_packets( 108 | fun(Packet, Acc, Codec) -> 109 | {[Packet | Acc], Codec} 110 | end, 111 | [], Stream, Codec0), 112 | {ok, lists:reverse(Packets), 113 | Client#client{codec = Codec}}; 114 | {error, timeout} -> 115 | {ok, [], Client}; 116 | Err -> 117 | Err 118 | end. 119 | 120 | tcp_recv_all(Sock, Timeout) -> 121 | %% io:format("Sock: ~p; Timeout: ~p~n~n~n", [Sock, Timeout]), 122 | case gen_tcp:recv(Sock, 0, Timeout) of 123 | {ok, Stream} -> 124 | tcp_recv_all_inner(Sock, Stream); 125 | Err -> 126 | Err 127 | end. 128 | 129 | tcp_recv_all_inner(Sock, Acc) -> 130 | case gen_tcp:recv(Sock, 0, 0) of 131 | {ok, Stream} -> 132 | tcp_recv_all_inner(Sock, <>); 133 | {error, timeout} -> 134 | {ok, Acc}; 135 | Other -> 136 | Other 137 | end. 138 | 139 | close(#client{sock = Sock}) -> 140 | ok = gen_tcp:close(Sock). 141 | 142 | 143 | ping_session(Host, Port, Secret, DcId, Protocol, Timeout) -> 144 | Cli0 = connect(Host, Port, Secret, DcId, Protocol), 145 | ReqPQ = req_pq(), 146 | Cli1 = send(unencrypted_cli_packet(ReqPQ), Cli0), 147 | {ok, Packet, Cli2} = recv_packet(Cli1, Timeout), 148 | ok = close(Cli2), 149 | {_MsgId, response, ResPQ} = parse_unencrypted_srv_packet(Packet), 150 | {res_pq_matches(ReqPQ, ResPQ), 151 | ReqPQ, ResPQ}. 152 | 153 | %% 154 | %% Messages 155 | %% 156 | 157 | %% @doc encodes payload as unencrypted client message 158 | %% https://core.telegram.org/mtproto/description#unencrypted-message 159 | unencrypted_cli_packet(Payload) -> 160 | Now = erlang:system_time(microsecond), 161 | %% Is 128 enough? 162 | PadSize = rand:uniform(128 div 4) * 4, % should be aligned to 4b 163 | Padding = crypto:strong_rand_bytes(PadSize), 164 | unencrypted_cli_packet(Payload, Now, Padding). 165 | 166 | unencrypted_cli_packet(Payload, Now, Pad) -> 167 | %% Client message identifiers are divisible by 4. 168 | Micro = 1000000, 169 | NowSec = Now div Micro, 170 | MicroFraction = Now rem Micro, 171 | MicroDiv4 = MicroFraction - (MicroFraction rem 4), 172 | %% MsgId = NowSec * (2 bsl 31) + MicroDiv4, 173 | [<<0:64, 174 | MicroDiv4:32/unsigned-little, 175 | NowSec:32/unsigned-little, 176 | (byte_size(Payload) + byte_size(Pad)):32/unsigned-little>>, 177 | Payload | Pad]. 178 | 179 | 180 | %% @doc extracts payload from unencrypted server message 181 | parse_unencrypted_srv_packet(<<0:64, MsgId:64/unsigned-little, 182 | Size:32/unsigned-little, 183 | Payload:Size/binary>>) -> 184 | %% Server message identifiers modulo 4 yield 1 if the message is a response to a 185 | %% client message, and 3 otherwise. 186 | Kind = 187 | case MsgId rem 4 of 188 | 1 -> response; 189 | 3 -> event 190 | end, 191 | {MsgId, Kind, Payload}. 192 | 193 | 194 | 195 | %% https://core.telegram.org/mtproto/serialize#base-types 196 | -define(int, 32/signed-little). 197 | -define(long, 64/signed-little). 198 | 199 | -define(REQ_PQ, 16#60469778:?int). 200 | -define(RES_PQ, 16#05162463:?int). 201 | 202 | %% @doc creates req_pq packet 203 | req_pq() -> 204 | %% req_pq#60469778 nonce:int128 = ResPQ; 205 | Nonce = <<(crypto:strong_rand_bytes(12)):12/binary, 206 | (erlang:unique_integer()):32/little>>, 207 | <>. 208 | 209 | 210 | %% @doc returns `true' if ResPQ nonce matches the nonce for ReqPQ 211 | %% @param ReqPQ: req_pq packet generated by req_pq/0 212 | %% @param ResPQ: resPQ packet received from server 213 | res_pq_matches(<>, <>) -> 214 | %% resPQ#05162463 nonce:int128 server_nonce:int128 pq:bytes \ 215 | %% server_public_key_fingerprints:Vector = ResPQ; 216 | true; 217 | res_pq_matches(_, _) -> 218 | false. 219 | 220 | 221 | -define(PING, 16#7abe77ec:?int). 222 | -define(PONG, 16#347773c5:?int). 223 | %% @doc constructs 'ping' message 224 | ping() -> 225 | %% ping#7abe77ec ping_id:long = Pong; 226 | PingId = erlang:unique_integer(), 227 | <>. 228 | 229 | pong_matches(<>, <>) -> 230 | %% pong#347773c5 msg_id:long ping_id:long = Pong; 231 | true; 232 | pong_matches(_, _) -> 233 | false. 234 | -------------------------------------------------------------------------------- /test/mtp_test_cmd_rpc.erl: -------------------------------------------------------------------------------- 1 | %% @doc Callback module for mtp_test_middle_server that supports some more tricky commands 2 | -module(mtp_test_cmd_rpc). 3 | -export([call/3, 4 | packet_to_term/1]). 5 | -export([init/1, 6 | handle_rpc/2]). 7 | 8 | call(M, F, Opts) -> 9 | true = erlang:function_exported(M, F, 3), 10 | term_to_packet({M, F, Opts}). 11 | 12 | term_to_packet(Term) -> 13 | RespBin = term_to_binary(Term), 14 | RespSize = byte_size(RespBin), 15 | PadSize = case (RespSize rem 16) of 16 | 0 -> 0; 17 | Rem -> 16 - Rem 18 | end, 19 | Pad = binary:copy(<<0>>, PadSize), 20 | <>. 21 | 22 | packet_to_term(<>) -> 23 | binary_to_term(Term). 24 | 25 | init(_) -> 26 | #{}. 27 | 28 | handle_rpc({data, ConnId, Req}, St) -> 29 | {M, F, Opts} = packet_to_term(Req), 30 | case M:F(Opts, ConnId, St) of 31 | {reply, Resp, St1} -> 32 | {rpc, {proxy_ans, ConnId, term_to_packet(Resp)}, St1}; 33 | {close, St1} -> 34 | {rpc, {close_ext, ConnId}, tombstone(ConnId, St1)}; 35 | {return, What} -> 36 | What 37 | end; 38 | handle_rpc({remote_closed, ConnId}, St) -> 39 | {noreply, tombstone(ConnId, St)}. 40 | 41 | tombstone(ConnId, St) -> 42 | ({ok, tombstone} =/= maps:find(ConnId, St)) 43 | orelse error({already_closed, ConnId}), 44 | St#{ConnId => tombstone}. 45 | -------------------------------------------------------------------------------- /test/mtp_test_datacenter.erl: -------------------------------------------------------------------------------- 1 | %% Fake telegram "datacenters" 2 | %% Mock to emulate core.telegram.org and set of telegram 3 | %% "middle-proxies" for each datacenter ID 4 | -module(mtp_test_datacenter). 5 | 6 | -export([start_dc/0, 7 | start_dc/3, 8 | stop_dc/1, 9 | start_config_server/5, 10 | stop_config_server/1]). 11 | -export([middle_connections/1]). 12 | -export([dc_list_to_config/1]). 13 | -export([do/1]). 14 | 15 | -include_lib("inets/include/httpd.hrl"). 16 | 17 | -define(SECRET_PATH, "/getProxySecret"). 18 | -define(CONFIG_PATH, "/getProxyConfig"). 19 | 20 | -type dc_conf() :: [{DcId :: integer(), 21 | Ip :: inet:ip4_address(), 22 | Port :: inet:port_number()}]. 23 | 24 | start_dc() -> 25 | Secret = crypto:strong_rand_bytes(128), 26 | DcConf = [{1, {127, 0, 0, 1}, 8888}], 27 | {ok, _Cfg} = start_dc(Secret, DcConf, #{}). 28 | 29 | -spec start_dc(binary(), dc_conf(), #{}) -> {ok, #{}}. 30 | start_dc(Secret, DcConf, Acc) -> 31 | Cfg = dc_list_to_config(DcConf), 32 | {ok, Acc1} = start_config_server({127, 0, 0, 1}, 0, Secret, Cfg, Acc), 33 | RpcHandler = maps:get(rpc_handler, Acc, mtp_test_echo_rpc), 34 | Ids = 35 | [begin 36 | Id = {?MODULE, DcId}, 37 | {ok, _Pid} = mtp_test_middle_server:start( 38 | Id, #{port => Port, 39 | ip => Ip, 40 | secret => Secret, 41 | rpc_handler => RpcHandler}), 42 | Id 43 | end || {DcId, Ip, Port} <- DcConf], 44 | {ok, Acc1#{srv_ids => Ids}}. 45 | 46 | stop_dc(#{srv_ids := Ids} = Acc) -> 47 | {ok, Acc1} = stop_config_server(Acc), 48 | ok = lists:foreach(fun mtp_test_middle_server:stop/1, Ids), 49 | {ok, maps:without([srv_ids], Acc1)}. 50 | 51 | middle_connections(#{srv_ids := Ids}) -> 52 | lists:flatten([ranch:procs(Id, connections) 53 | || Id <- Ids]). 54 | 55 | %% 56 | %% Inets HTTPD to use as a mock for https://core.telegram.org 57 | %% 58 | 59 | %% Api 60 | start_config_server(Ip, Port, Secret, DcConfig, Acc) -> 61 | application:load(mtproto_proxy), 62 | RootDir = code:lib_dir(mtproto_proxy, test), 63 | {ok, Pid} = 64 | inets:start(httpd, 65 | [{port, Port}, 66 | {server_name, "mtp_config"}, 67 | {server_root, "/tmp"}, 68 | {document_root, RootDir}, 69 | 70 | {bind_address, Ip}, 71 | {modules, [?MODULE]}, 72 | {mtp_secret, Secret}, 73 | {mtp_dc_conf, DcConfig}]), 74 | %% Get listen port in case when Port is 0 (ephemeral) 75 | [{port, RealPort}] = httpd:info(Pid, [port]), 76 | Netloc = lists:flatten(io_lib:format("http://~s:~w", [inet:ntoa(Ip), RealPort])), 77 | Env = [{proxy_secret_url, 78 | Netloc ++ ?SECRET_PATH}, 79 | {proxy_config_url, 80 | Netloc ++ ?CONFIG_PATH}, 81 | {external_ip, "127.0.0.1"}, 82 | {ip_lookup_services, undefined}], 83 | OldEnv = 84 | [begin 85 | %% OldV is undefined | {ok, V} 86 | OldV = application:get_env(mtproto_proxy, K), 87 | case V of 88 | undefined -> application:unset_env(mtproto_proxy, K); 89 | _ -> 90 | application:set_env(mtproto_proxy, K, V) 91 | end, 92 | {K, OldV} 93 | end || {K, V} <- Env], 94 | {ok, Acc#{env => OldEnv, 95 | httpd_pid => Pid}}. 96 | 97 | stop_config_server(#{env := Env, httpd_pid := Pid} = Acc) -> 98 | [case V of 99 | undefined -> 100 | application:unset_env(mtproto_proxy, K); 101 | {ok, Val} -> 102 | application:set_env(mtproto_proxy, K, Val) 103 | end || {K, V} <- Env], 104 | inets:stop(httpd, Pid), 105 | {ok, maps:without([env, httpd_pid], Acc)}. 106 | 107 | dc_list_to_config(List) -> 108 | << 109 | <<(list_to_binary( 110 | io_lib:format("proxy_for ~w ~s:~w;~n", [DcId, inet:ntoa(Ip), Port]) 111 | ))/binary>> 112 | || {DcId, Ip, Port} <- List 113 | >>. 114 | 115 | %% Inets callback 116 | do(#mod{request_uri = ?CONFIG_PATH, config_db = Db}) -> 117 | [{_, DcConf}] = ets:lookup(Db, mtp_dc_conf), 118 | {break, [{response, {200, binary_to_list(DcConf)}}]}; 119 | do(#mod{request_uri = ?SECRET_PATH, config_db = Db}) -> 120 | [{_, Secret}] = ets:lookup(Db, mtp_secret), 121 | {break, [{response, {200, binary_to_list(Secret)}}]}. 122 | -------------------------------------------------------------------------------- /test/mtp_test_echo_rpc.erl: -------------------------------------------------------------------------------- 1 | %% @doc simple callback module for mtp_test_middle_server that echoes received packets back 2 | -module(mtp_test_echo_rpc). 3 | -export([init/1, 4 | handle_rpc/2]). 5 | 6 | init(_) -> 7 | #{}. 8 | 9 | handle_rpc({data, ConnId, Data}, St) -> 10 | Cnt = maps:get(ConnId, St, 0), 11 | {rpc, {proxy_ans, ConnId, Data}, St#{ConnId => Cnt + 1}}; 12 | handle_rpc({remote_closed, ConnId}, St) -> 13 | is_integer(maps:get(ConnId, St)) 14 | orelse error({unexpected_closed, ConnId}), 15 | {noreply, St#{ConnId := tombstone}}. 16 | -------------------------------------------------------------------------------- /test/mtp_test_metric.erl: -------------------------------------------------------------------------------- 1 | %% @doc simple metric backend to be used in tests. 2 | %% XXX: DON'T USE IN PRODUCTION! It can become bottleneck! 3 | -module(mtp_test_metric). 4 | 5 | -behaviour(gen_server). 6 | 7 | %% API 8 | -export([start_link/0]). 9 | -export([notify/4]). 10 | -export([get/2, 11 | get/3, 12 | get_tags/3, 13 | wait_for_value/5, 14 | wait_for/5]). 15 | 16 | %% gen_server callbacks 17 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 18 | terminate/2, code_change/3]). 19 | 20 | -record(state, {count = #{}, 21 | gauge = #{}, 22 | histogram = #{}}). 23 | 24 | start_link() -> 25 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 26 | 27 | notify(Type, Name, Value, Extra) -> 28 | try gen_server:call(?MODULE, {notify, Type, Name, Value, Extra}) 29 | catch _:Reason -> 30 | {error, Reason} 31 | end. 32 | 33 | get(Type, Name) -> 34 | get(Type, Name, #{}). 35 | 36 | -spec get(count | gauge | histogram, [atom()], [atom()]) -> integer() | not_found. 37 | get(Type, Name, Extra) -> 38 | gen_server:call(?MODULE, {get, Type, Name, Extra}). 39 | 40 | get_tags(Type, Name, Tags) -> 41 | get(Type, Name, #{labels => Tags}). 42 | 43 | wait_for_value(Type, Name, Tags, Value, Timeout) -> 44 | Now = erlang:monotonic_time(millisecond), 45 | Test = fun(Current) -> Current == Value end, 46 | wait_for_till(Type, Name, #{labels => Tags}, Test, Now + Timeout). 47 | 48 | wait_for(Type, Name, Tags, Test, Timeout) -> 49 | Now = erlang:monotonic_time(millisecond), 50 | wait_for_till(Type, Name, #{labels => Tags}, Test, Now + Timeout). 51 | 52 | wait_for_till(Type, Name, Extra, Test, Deadline) -> 53 | case Test(get(Type, Name, Extra)) of 54 | true -> ok; 55 | false -> 56 | Now = erlang:monotonic_time(millisecond), 57 | case Now >= Deadline of 58 | true -> 59 | timeout; 60 | false -> 61 | timer:sleep(10), 62 | wait_for_till(Type, Name, Extra, Test, Deadline) 63 | end 64 | end. 65 | 66 | 67 | init([]) -> 68 | {ok, #state{}}. 69 | 70 | handle_call({notify, count, Name, Value, Extra}, _From, #state{count = C} = State) -> 71 | K = {Name, Extra}, 72 | V1 = 73 | case maps:find(K, C) of 74 | {ok, V0} -> 75 | V0 + Value; 76 | error -> 77 | Value 78 | end, 79 | {reply, ok, State#state{count = C#{K => V1}}}; 80 | handle_call({notify, gauge, Name, Value, Extra}, _From, #state{gauge = G} = State) -> 81 | K = {Name, Extra}, 82 | {reply, ok, State#state{gauge = G#{K => Value}}}; 83 | handle_call({notify, histogram, Name, Value, Extra}, _From, #state{histogram = H} = State) -> 84 | K = {Name, Extra}, 85 | V1 = 86 | case maps:find(K, H) of 87 | {ok, {Count, Total, Min, Max}} -> 88 | {Count + 1, 89 | Total + Value, 90 | erlang:min(Min, Value), 91 | erlang:max(Max, Value)}; 92 | error -> 93 | {1, 94 | Value, 95 | Value, 96 | Value} 97 | end, 98 | {reply, ok, State#state{histogram = H#{K => V1}}}; 99 | handle_call({get, Type, Name, Extra}, _From, State) -> 100 | K = {Name, Extra}, 101 | Tab = case Type of 102 | count -> State#state.count; 103 | gauge -> State#state.gauge; 104 | histogram -> State#state.histogram 105 | end, 106 | {reply, maps:get(K, Tab, not_found), State}. 107 | 108 | handle_cast(_Msg, State) -> 109 | {noreply, State}. 110 | handle_info(_Info, State) -> 111 | {noreply, State}. 112 | terminate(_Reason, _State) -> 113 | ok. 114 | code_change(_OldVsn, State, _Extra) -> 115 | {ok, State}. 116 | -------------------------------------------------------------------------------- /test/mtp_test_middle_server.erl: -------------------------------------------------------------------------------- 1 | %% @doc Fake telegram "middle-proxy" server 2 | -module(mtp_test_middle_server). 3 | -behaviour(ranch_protocol). 4 | -behaviour(gen_statem). 5 | 6 | -export([start/2, 7 | stop/1, 8 | get_rpc_handler_state/1]). 9 | -export([start_link/4, 10 | ranch_init/1]). 11 | -export([init/1, 12 | callback_mode/0, 13 | %% handle_call/3, 14 | %% handle_cast/2, 15 | %% handle_info/2, 16 | code_change/3, 17 | terminate/2 18 | ]). 19 | -export([wait_nonce/3, 20 | wait_handshake/3, 21 | on_tunnel/3]). 22 | 23 | -record(hs_state, 24 | {sock, 25 | transport, 26 | secret, 27 | codec :: mtp_codec:codec(), 28 | cli_nonce, 29 | cli_ts, 30 | sender_pid, 31 | peer_pid, 32 | srv_nonce, 33 | rpc_handler}). 34 | -record(t_state, 35 | {sock, 36 | transport, 37 | codec, 38 | rpc_handler, 39 | rpc_handler_state}). 40 | 41 | -define(RPC_NONCE, 170,135,203,122). 42 | -define(RPC_HANDSHAKE, 245,238,130,118). 43 | -define(RPC_FLAGS, 0, 0, 0, 0). 44 | 45 | %% -type state_name() :: wait_nonce | wait_handshake | on_tunnel. 46 | 47 | %% Api 48 | start(Id, #{port := _, secret := _} = Opts) -> 49 | {ok, _} = application:ensure_all_started(ranch), 50 | ranch:start_listener( 51 | Id, ranch_tcp, 52 | #{socket_opts => [{ip, {127, 0, 0, 1}}, 53 | {port, maps:get(port, Opts)}], 54 | num_acceptors => 2, 55 | max_connections => 100}, 56 | ?MODULE, Opts). 57 | 58 | stop(Id) -> 59 | ranch:stop_listener(Id). 60 | 61 | get_rpc_handler_state(Pid) -> 62 | gen_statem:call(Pid, get_rpc_handler_state). 63 | 64 | %% Callbacks 65 | 66 | start_link(Ref, _, Transport, Opts) -> 67 | {ok, proc_lib:spawn_link(?MODULE, ranch_init, [{Ref, Transport, Opts}])}. 68 | 69 | ranch_init({Ref, Transport, Opts}) -> 70 | {ok, Socket} = ranch:handshake(Ref), 71 | {ok, StateName, StateData} = init({Socket, Transport, Opts}), 72 | ok = Transport:setopts(Socket, [{active, once}]), 73 | gen_statem:enter_loop(?MODULE, [], StateName, StateData). 74 | 75 | init({Socket, Transport, Opts}) -> 76 | Codec = mtp_codec:new(mtp_noop_codec, mtp_noop_codec:new(), 77 | mtp_full, mtp_full:new(-2, -2, true)), 78 | State = #hs_state{sock = Socket, 79 | transport = Transport, 80 | secret = maps:get(secret, Opts), 81 | codec = Codec, 82 | rpc_handler = maps:get(rpc_handler, Opts, mtp_test_echo_rpc)}, 83 | {ok, wait_nonce, State}. 84 | 85 | callback_mode() -> 86 | state_functions. 87 | 88 | terminate(_Reason, _State) -> 89 | ok. 90 | 91 | code_change(_OldVsn, State, _Extra) -> 92 | {ok, State}. 93 | 94 | %% 95 | %% State handlers 96 | %% 97 | 98 | wait_nonce(info, {tcp, _Sock, TcpData}, 99 | #hs_state{codec = Codec0, secret = Key, 100 | transport = Transport, sock = Sock} = S) -> 101 | %% Hope whole protocol packet fit in 1 TCP packet 102 | {ok, PacketData, Codec1} = mtp_codec:try_decode_packet(TcpData, Codec0), 103 | <> = Key, 104 | {nonce, KeySelector, Schema, CryptoTs, CliNonce} = mtp_rpc:decode_nonce(PacketData), 105 | SrvNonce = crypto:strong_rand_bytes(16), 106 | Answer = mtp_rpc:encode_nonce({nonce, KeySelector, Schema, CryptoTs, SrvNonce}), 107 | %% Send non-encrypted nonce 108 | {ok, #hs_state{codec = Codec2} = S1} = hs_send(Answer, S#hs_state{codec = Codec1}), 109 | %% Generate keys 110 | {ok, {CliIp, CliPort}} = Transport:peername(Sock), 111 | {ok, {MyIp, MyPort}} = Transport:sockname(Sock), 112 | CliIpBin = mtp_obfuscated:bin_rev(mtp_rpc:inet_pton(CliIp)), 113 | MyIpBin = mtp_obfuscated:bin_rev(mtp_rpc:inet_pton(MyIp)), 114 | 115 | Args = #{srv_n => SrvNonce, clt_n => CliNonce, clt_ts => CryptoTs, 116 | srv_ip => MyIpBin, srv_port => MyPort, 117 | clt_ip => CliIpBin, clt_port => CliPort, secret => Key}, 118 | {DecKey, DecIv} = mtp_down_conn:get_middle_key(Args#{purpose => <<"CLIENT">>}), 119 | {EncKey, EncIv} = mtp_down_conn:get_middle_key(Args#{purpose => <<"SERVER">>}), 120 | %% Add encryption layer to codec 121 | CryptoState = mtp_aes_cbc:new(EncKey, EncIv, DecKey, DecIv, 16), 122 | Codec3 = mtp_codec:replace(crypto, mtp_aes_cbc, CryptoState, Codec2), 123 | 124 | {next_state, wait_handshake, 125 | activate(S1#hs_state{codec = Codec3, 126 | cli_nonce = CliNonce, 127 | cli_ts = CryptoTs, 128 | srv_nonce = SrvNonce})}; 129 | wait_nonce(Type, Event, S) -> 130 | handle_event(Type, Event, ?FUNCTION_NAME, S). 131 | 132 | 133 | wait_handshake(info, {tcp, _Sock, TcpData}, 134 | #hs_state{codec = Codec0} = S) -> 135 | {ok, PacketData, Codec1} = mtp_codec:try_decode_packet(TcpData, Codec0), 136 | {handshake, SenderPID, PeerPID} = mtp_rpc:decode_handshake(PacketData), 137 | Answer = mtp_rpc:encode_handshake({handshake, SenderPID, PeerPID}), 138 | {ok, #hs_state{sock = Sock, 139 | transport = Transport, 140 | codec = Codec2, 141 | rpc_handler = Handler}} = hs_send(Answer, S#hs_state{codec = Codec1}), 142 | {next_state, on_tunnel, 143 | activate(#t_state{sock = Sock, 144 | transport = Transport, 145 | codec = Codec2, 146 | rpc_handler = Handler, 147 | rpc_handler_state = Handler:init([])})}; 148 | wait_handshake(Type, Event, S) -> 149 | handle_event(Type, Event, ?FUNCTION_NAME, S). 150 | 151 | 152 | on_tunnel(info, {tcp, _Sock, TcpData}, #t_state{codec = Codec0} = S) -> 153 | {ok, S2, Codec1} = 154 | mtp_codec:fold_packets( 155 | fun(Packet, S1, Codec1) -> 156 | S2 = handle_rpc(mtp_rpc:srv_decode_packet(Packet), S1#t_state{codec = Codec1}), 157 | {S2, S2#t_state.codec} 158 | end, S, TcpData, Codec0), 159 | {keep_state, activate(S2#t_state{codec = Codec1})}; 160 | on_tunnel({call, From}, get_rpc_handler_state, #t_state{rpc_handler_state = HSt}) -> 161 | {keep_state_and_data, [{reply, From, HSt}]}; 162 | on_tunnel(Type, Event, S) -> 163 | handle_event(Type, Event, ?FUNCTION_NAME, S). 164 | 165 | handle_event(info, {tcp_closed, _Sock}, _EventName, _S) -> 166 | {stop, normal}. 167 | 168 | %% Helpers 169 | 170 | hs_send(Packet, #hs_state{transport = Transport, sock = Sock, 171 | codec = Codec} = St) -> 172 | %% ?log(debug, "Up>Down: ~w", [Packet]), 173 | {Encoded, Codec1} = mtp_codec:encode_packet(Packet, Codec), 174 | ok = Transport:send(Sock, Encoded), 175 | {ok, St#hs_state{codec = Codec1}}. 176 | 177 | t_send(Packet, #t_state{transport = Transport, sock = Sock, 178 | codec = Codec} = St) -> 179 | %% ?log(debug, "Up>Down: ~w", [Packet]), 180 | {Encoded, Codec1} = mtp_codec:encode_packet(Packet, Codec), 181 | ok = Transport:send(Sock, Encoded), 182 | {ok, St#t_state{codec = Codec1}}. 183 | 184 | activate(#hs_state{transport = Transport, sock = Sock} = S) -> 185 | ok = Transport:setopts(Sock, [{active, once}]), 186 | S; 187 | activate(#t_state{transport = Transport, sock = Sock} = S) -> 188 | ok = Transport:setopts(Sock, [{active, once}]), 189 | S. 190 | 191 | handle_rpc(RPC, #t_state{rpc_handler = Handler, rpc_handler_state = HSt} = S) -> 192 | case Handler:handle_rpc(RPC, HSt) of 193 | {rpc, Response, HSt1} -> 194 | {ok, S1} = t_send(mtp_rpc:srv_encode_packet(Response), 195 | S#t_state{rpc_handler_state = HSt1}), 196 | S1; 197 | {rpc_multi, Responses, HSt1} -> 198 | lists:foldl( 199 | fun(Response, S1) -> 200 | {ok, S2} = t_send(mtp_rpc:srv_encode_packet(Response), S1), 201 | S2 202 | end, S#t_state{rpc_handler_state = HSt1}, Responses); 203 | {noreply, HSt1} -> 204 | S#t_state{rpc_handler_state = HSt1} 205 | end. 206 | -------------------------------------------------------------------------------- /test/prop_mtp_abridged.erl: -------------------------------------------------------------------------------- 1 | %% @doc Property-based tests for mtp_abridged 2 | -module(prop_mtp_abridged). 3 | -include_lib("proper/include/proper.hrl"). 4 | 5 | -export([prop_codec/1, prop_stream/1]). 6 | 7 | prop_codec(doc) -> 8 | "Tests that any 4-byte aligned binary can be encoded and decoded back". 9 | 10 | prop_codec() -> 11 | ?FORALL(Bin, mtp_prop_gen:packet_4b(), codec(Bin)). 12 | 13 | codec(Bin) -> 14 | Codec = mtp_abridged:new(), 15 | {Data, Codec1} = mtp_abridged:encode_packet(Bin, Codec), 16 | {ok, Decoded, <<>>, _} = mtp_abridged:try_decode_packet(iolist_to_binary(Data), Codec1), 17 | Decoded == Bin. 18 | 19 | 20 | prop_stream(doc) -> 21 | "Tests that any number of packets can be encoded, concatenated and decoded". 22 | 23 | prop_stream() -> 24 | ?FORALL(Stream, mtp_prop_gen:stream_4b(), stream_codec(Stream)). 25 | 26 | stream_codec(Stream) -> 27 | Codec = mtp_abridged:new(), 28 | {BinStream, Codec1} = 29 | lists:foldl( 30 | fun(Bin, {Acc, Codec1}) -> 31 | {Data, Codec2} = mtp_abridged:encode_packet(Bin, Codec1), 32 | {<>, 33 | Codec2} 34 | end, {<<>>, Codec}, Stream), 35 | DecodedStream = decode_stream(BinStream, Codec1, []), 36 | Stream == DecodedStream. 37 | 38 | decode_stream(BinStream, Codec, Acc) -> 39 | case mtp_abridged:try_decode_packet(BinStream, Codec) of 40 | {incomplete, _} -> 41 | lists:reverse(Acc); 42 | {ok, DecPacket, Tail, Codec1} -> 43 | decode_stream(Tail, Codec1, [DecPacket | Acc]) 44 | end. 45 | -------------------------------------------------------------------------------- /test/prop_mtp_aes_cbc.erl: -------------------------------------------------------------------------------- 1 | %% @doc Property-based tests for mtp_aes_cbc 2 | -module(prop_mtp_aes_cbc). 3 | -include_lib("proper/include/proper.hrl"). 4 | 5 | -export([prop_stream/1]). 6 | 7 | prop_stream(doc) -> 8 | "Tests that any number of packets can be encoded, concatenated and decoded" 9 | " as a stream using the same key for encoding and decoding". 10 | 11 | prop_stream() -> 12 | ?FORALL({Key, Iv, Stream}, arg_set(), stream_codec(Key, Iv, Stream)). 13 | 14 | 15 | arg_set() -> 16 | proper_types:tuple( 17 | [mtp_prop_gen:key(), 18 | mtp_prop_gen:iv(), 19 | mtp_prop_gen:stream_16b() 20 | ]). 21 | 22 | stream_codec(Key, Iv, Stream) -> 23 | Codec = mtp_aes_cbc:new(Key, Iv, Key, Iv, 16), 24 | {BinStream, Codec2} = 25 | lists:foldl( 26 | fun(Bin, {Acc, Codec1}) -> 27 | {Data, Codec2} = mtp_aes_cbc:encrypt(Bin, Codec1), 28 | {<>, 29 | Codec2} 30 | end, {<<>>, Codec}, Stream), 31 | {Decrypted, <<>>, _Codec3} = mtp_aes_cbc:decrypt(BinStream, Codec2), 32 | %% io:format("Dec: ~p~nOrig: ~p~nCodec: ~p~n", [Decrypted, Stream, _Codec3]), 33 | Decrypted == iolist_to_binary(Stream). 34 | -------------------------------------------------------------------------------- /test/prop_mtp_codec.erl: -------------------------------------------------------------------------------- 1 | %% @doc Property-based tests for mtp_codec 2 | -module(prop_mtp_codec). 3 | -include_lib("proper/include/proper.hrl"). 4 | -include_lib("stdlib/include/assert.hrl"). 5 | 6 | -export([prop_obfuscated_secure_stream/1, 7 | prop_obfuscated_secure_duplex/1, 8 | prop_obfuscated_secure_duplex_multi/1, 9 | prop_fullcbc_stream/1, 10 | prop_tls_stream/1, 11 | prop_tls_big_stream/1]). 12 | 13 | 14 | prop_obfuscated_secure_stream(doc) -> 15 | "Tests that any number of packets can be encrypted and decrypted with codec that includes" 16 | " combination of mtp_obfuscated and mtp_secure". 17 | 18 | prop_obfuscated_secure_stream() -> 19 | ?FORALL({Key, Iv, Stream}, stream_arg_set(), obfuscated_secure_stream(Key, Iv, Stream)). 20 | 21 | stream_arg_set() -> 22 | proper_types:tuple( 23 | [mtp_prop_gen:key(), 24 | mtp_prop_gen:iv(), 25 | mtp_prop_gen:stream_4b() 26 | ]). 27 | 28 | obfuscated_secure_stream(Key, Iv, Stream) -> 29 | Codec0 = mk_secure_codec(Key, Iv, Key, Iv), 30 | {BinStream, Codec2} = 31 | lists:foldl( 32 | fun(Bin, {Acc, Codec1}) -> 33 | {Data, Codec2} = mtp_codec:encode_packet(Bin, Codec1), 34 | {<>, 35 | Codec2} 36 | end, {<<>>, Codec0}, Stream), 37 | {ResStream, _Codec3} = parse_stream(BinStream, Codec2), 38 | ?assertEqual(Stream, ResStream), 39 | true. 40 | 41 | 42 | prop_obfuscated_secure_duplex(doc) -> 43 | "Tests that any number of packets can be encrypted and decrypted in both directions using" 44 | " a pair of keys with codec that uses combination of mtp_obfuscated and mtp_secure". 45 | 46 | prop_obfuscated_secure_duplex() -> 47 | ?FORALL({TxKey, TxIv, RxKey, RxIv, Stream}, duplex_arg_set(), 48 | obfuscated_secure_duplex(TxKey, TxIv, RxKey, RxIv, Stream)). 49 | 50 | duplex_arg_set() -> 51 | {mtp_prop_gen:key(), 52 | mtp_prop_gen:iv(), 53 | mtp_prop_gen:key(), 54 | mtp_prop_gen:iv(), 55 | mtp_prop_gen:stream_4b()}. 56 | 57 | obfuscated_secure_duplex(TxKey, TxIv, RxKey, RxIv, Stream) -> 58 | CliCodec0 = mk_secure_codec(TxKey, TxIv, RxKey, RxIv), 59 | SrvCodec0 = mk_secure_codec(RxKey, RxIv, TxKey, TxIv), 60 | {_, _, BackStream} = roundtrip(CliCodec0, SrvCodec0, Stream), 61 | Stream == BackStream. 62 | 63 | 64 | prop_obfuscated_secure_duplex_multi(doc) -> 65 | "Tests that any number of packets can be encrypted and decrypted in both directions multiple" 66 | " times using a pair of keys with codec that uses combination of mtp_obfuscated and" 67 | " mtp_secure". 68 | 69 | prop_obfuscated_secure_duplex_multi() -> 70 | ?FORALL({N, {TxKey, TxIv, RxKey, RxIv, Stream}}, {proper_types:range(1, 100), duplex_arg_set()}, 71 | obfuscated_secure_duplex_multi(N, TxKey, TxIv, RxKey, RxIv, Stream)). 72 | 73 | obfuscated_secure_duplex_multi(N, TxKey, TxIv, RxKey, RxIv, Stream0) -> 74 | CliCodec0 = mk_secure_codec(TxKey, TxIv, RxKey, RxIv), 75 | SrvCodec0 = mk_secure_codec(RxKey, RxIv, TxKey, TxIv), 76 | {_, _, Stream} = 77 | lists:foldl( 78 | fun(I, {CliCodec1, SrvCodec1, Stream1}) -> 79 | {_, _, BackStream} = Res = roundtrip(CliCodec1, SrvCodec1, Stream1), 80 | ?assertEqual(Stream0, BackStream, [{trip_i, I}]), 81 | Res 82 | end, {CliCodec0, SrvCodec0, Stream0}, lists:seq(1, N)), 83 | Stream0 == Stream. 84 | 85 | 86 | %% Helpers 87 | 88 | roundtrip(CliCodec0, SrvCodec0, Stream) -> 89 | %% Client creates a stream of bytes 90 | {CliBinStream, CliCodec1} = 91 | lists:foldl( 92 | fun(Bin, {Acc, Codec1}) -> 93 | {Data, Codec2} = mtp_codec:encode_packet(Bin, Codec1), 94 | {<>, 95 | Codec2} 96 | end, {<<>>, CliCodec0}, Stream), 97 | %% Server decodes stream of bytes and immediately "sends" them back 98 | {ok, SrvBinStream, SrvCodec1} = 99 | mtp_codec:fold_packets( 100 | fun(Decoded, Acc, Codec1) -> 101 | {Encoded, Codec2} = mtp_codec:encode_packet(Decoded, Codec1), 102 | {<>, Codec2} 103 | end, <<>>, CliBinStream, SrvCodec0), 104 | %% Client "receives" and decodes what server sent 105 | {ok, RevCliDecodedStream, CliCodec2} = 106 | mtp_codec:fold_packets( 107 | fun(Decoded, Acc, Codec1) -> 108 | {[Decoded | Acc], Codec1} 109 | end, [], SrvBinStream, CliCodec1), 110 | {CliCodec2, SrvCodec1, lists:reverse(RevCliDecodedStream)}. 111 | 112 | mk_secure_codec(EncKey, EncIv, DecKey, DecIv) -> 113 | Crypto = mtp_obfuscated:new(EncKey, EncIv, DecKey, DecIv), 114 | Packet = mtp_secure:new(), 115 | mtp_codec:new(mtp_obfuscated, Crypto, 116 | mtp_secure, Packet). 117 | 118 | 119 | prop_fullcbc_stream(doc) -> 120 | "Tests that any number of packets can be encrypted and decrypted with mtp_full + mtp_aes_cbc" 121 | " It emulates downstream codec set". 122 | 123 | prop_fullcbc_stream() -> 124 | ?FORALL({Key, Iv, Stream}, fullcbc_arg_set(), fullcbc_stream(Key, Iv, Stream)). 125 | 126 | fullcbc_arg_set() -> 127 | proper_types:tuple( 128 | [mtp_prop_gen:key(), 129 | mtp_prop_gen:iv(), 130 | mtp_prop_gen:stream_16b() 131 | ]). 132 | 133 | fullcbc_stream(Key, Iv, Stream) -> 134 | Codec0 = mk_fullcbc_codec(Key, Iv, Key, Iv), 135 | {BinStream, Codec2} = 136 | lists:foldl( 137 | fun(Bin, {Acc, Codec1}) -> 138 | {Data, Codec2} = mtp_codec:encode_packet(Bin, Codec1), 139 | {<>, 140 | Codec2} 141 | end, {<<>>, Codec0}, Stream), 142 | {ResStream, Codec3} = parse_stream(BinStream, Codec2), 143 | ?assertEqual(Stream, ResStream, #{codec => Codec3}), 144 | true. 145 | 146 | mk_fullcbc_codec(EncKey, EncIv, DecKey, DecIv) -> 147 | Crypto = mtp_aes_cbc:new(EncKey, EncIv, DecKey, DecIv, 16), 148 | Packet = mtp_full:new(1, 1, true), 149 | mtp_codec:new(mtp_aes_cbc, Crypto, 150 | mtp_full, Packet). 151 | 152 | 153 | prop_tls_stream(doc) -> 154 | "Tests combination of fake-tls + mtp_obfuscated + mtp_secure. It emulates fake-tls client". 155 | 156 | prop_tls_stream() -> 157 | ?FORALL({Key, Iv, Stream}, stream_arg_set(), tls_obfuscated_secure_stream(Key, Iv, Stream)). 158 | 159 | 160 | 161 | prop_tls_big_stream(doc) -> 162 | "Tests combination of fake-tls + mtp_obfuscated + mtp_secure with packets >64kb. " 163 | "So, single 'packet-layer' packet will be split to multiple TLS packets. " 164 | "It emulates file uppload with fake-tls client". 165 | 166 | prop_tls_big_stream() -> 167 | ?FORALL({Key, Iv, Stream}, tls_big_stream_arg_set(), tls_obfuscated_secure_stream(Key, Iv, Stream)). 168 | 169 | tls_big_stream_arg_set() -> 170 | %% Packets more than 2^14b but less than 128kb 171 | Min = 16 * 1024 + 10, 172 | Max = 128 * 1024, 173 | proper_types:tuple( 174 | [mtp_prop_gen:key(), 175 | mtp_prop_gen:iv(), 176 | proper_types:list(mtp_prop_gen:aligned_binary(4, Min, Max)) 177 | ]). 178 | 179 | 180 | tls_obfuscated_secure_stream(Key, Iv, Stream) -> 181 | Codec0 = mk_tls_codec(Key, Iv, Key, Iv), 182 | {BinStream, Codec2} = 183 | lists:foldl( 184 | fun(Bin, {Acc, Codec1}) -> 185 | {Data, Codec2} = mtp_codec:encode_packet(Bin, Codec1), 186 | {<>, 187 | Codec2} 188 | end, {<<>>, Codec0}, Stream), 189 | {ResStream, _Codec3} = parse_stream(BinStream, Codec2), 190 | ?assertEqual(Stream, ResStream), 191 | true. 192 | 193 | 194 | parse_stream(Bin, Codec0) -> 195 | %% We want to split solid stream to smaller chunks to emulate network packet fragmentation 196 | Chunks = split_stream(Bin), 197 | {DecodedRev, Codec} = 198 | lists:foldl( 199 | fun(Chunk, {Acc1, Codec1}) -> 200 | {ok, Acc3, Codec3} = 201 | mtp_codec:fold_packets( 202 | fun(Decoded, Acc2, Codec2) -> 203 | {[Decoded | Acc2], Codec2} 204 | end, Acc1, Chunk, Codec1), 205 | {Acc3, Codec3} 206 | end, {[], Codec0}, Chunks), 207 | {lists:reverse(DecodedRev), Codec}. 208 | 209 | mk_tls_codec(EncKey, EncIv, DecKey, DecIv) -> 210 | Crypto = mtp_obfuscated:new(EncKey, EncIv, DecKey, DecIv), 211 | Packet = mtp_secure:new(), 212 | Tls = mtp_fake_tls:new(), 213 | mtp_codec:new(mtp_obfuscated, Crypto, 214 | mtp_secure, Packet, 215 | true, Tls, 216 | 30 * 1024 * 1024). 217 | 218 | split_stream(<<>>) -> []; 219 | split_stream(Bin) when byte_size(Bin) < 4 -> [Bin]; 220 | split_stream(Bin) -> 221 | %% TODO: should have deterministic seed for rand! 222 | Size = rand:uniform(byte_size(Bin) div 2), 223 | <> = Bin, 224 | [Chunk | split_stream(Tail)]. 225 | -------------------------------------------------------------------------------- /test/prop_mtp_fake_tls.erl: -------------------------------------------------------------------------------- 1 | %% @doc property-based tests for mtp_fake_tls 2 | -module(prop_mtp_fake_tls). 3 | -include_lib("proper/include/proper.hrl"). 4 | -include_lib("stdlib/include/assert.hrl"). 5 | 6 | -export([prop_codec_small/1, prop_codec_big/1, prop_stream/1]). 7 | 8 | prop_codec_small(doc) -> 9 | "Tests that any binary below 65535 bytes can be encoded and decoded back as single frame". 10 | 11 | prop_codec_small() -> 12 | ?FORALL(Bin, mtp_prop_gen:binary(8, 16 * 1024), codec_small(Bin)). 13 | 14 | codec_small(Bin) -> 15 | %% fake_tls can split big packets to multiple TLS frames of 2^14b 16 | Codec = mtp_fake_tls:new(), 17 | {Data, Codec1} = mtp_fake_tls:encode_packet(Bin, Codec), 18 | {ok, Decoded, <<>>, _} = mtp_fake_tls:try_decode_packet(iolist_to_binary(Data), Codec1), 19 | Decoded == Bin. 20 | 21 | 22 | prop_codec_big(doc) -> 23 | "Tests that big binaries will be split to multiple chunks". 24 | 25 | prop_codec_big() -> 26 | ?FORALL(Bin, mtp_prop_gen:binary(16 * 1024, 65535), codec_big(Bin)). 27 | 28 | codec_big(Bin) -> 29 | Codec = mtp_fake_tls:new(), 30 | {Data, Codec1} = mtp_fake_tls:encode_packet(Bin, Codec), 31 | Chunks = decode_stream(iolist_to_binary(Data), Codec1, []), 32 | ?assert(length(Chunks) > 1), 33 | ?assertEqual(Bin, iolist_to_binary(Chunks)), 34 | true. 35 | 36 | 37 | prop_stream(doc) -> 38 | "Tests that set of packets of size below 2^14b can be encoded and decoded back". 39 | 40 | prop_stream() -> 41 | ?FORALL(Stream, proper_types:list(mtp_prop_gen:binary(8, 16000)), 42 | codec_stream(Stream)). 43 | 44 | codec_stream(Stream) -> 45 | Codec = mtp_fake_tls:new(), 46 | {BinStream, Codec1} = 47 | lists:foldl( 48 | fun(Bin, {Acc, Codec1}) -> 49 | {Data, Codec2} = mtp_fake_tls:encode_packet(Bin, Codec1), 50 | {<>, 51 | Codec2} 52 | end, {<<>>, Codec}, Stream), 53 | DecodedStream = decode_stream(BinStream, Codec1, []), 54 | Stream == DecodedStream. 55 | 56 | decode_stream(BinStream, Codec, Acc) -> 57 | case mtp_fake_tls:try_decode_packet(BinStream, Codec) of 58 | {incomplete, _} -> 59 | lists:reverse(Acc); 60 | {ok, DecPacket, Tail, Codec1} -> 61 | decode_stream(Tail, Codec1, [DecPacket | Acc]) 62 | end. 63 | -------------------------------------------------------------------------------- /test/prop_mtp_full.erl: -------------------------------------------------------------------------------- 1 | %% @doc property-based tests for mtp_full 2 | -module(prop_mtp_full). 3 | -include_lib("proper/include/proper.hrl"). 4 | 5 | -export([prop_codec/1, prop_stream/1]). 6 | 7 | 8 | prop_codec(doc) -> 9 | "Tests that any 4-byte aligned binary can be encoded and decoded back". 10 | 11 | prop_codec() -> 12 | ?FORALL({CheckCRC, Bin}, {proper_types:boolean(), mtp_prop_gen:packet_4b()}, 13 | codec(Bin, CheckCRC)). 14 | 15 | codec(Bin, CheckCRC) -> 16 | Codec = mtp_full:new(0, 0, CheckCRC), 17 | {Data, Codec1} = mtp_full:encode_packet(Bin, Codec), 18 | {ok, Decoded, <<>>, _} = mtp_full:try_decode_packet(iolist_to_binary(Data), Codec1), 19 | Decoded == Bin. 20 | 21 | 22 | prop_stream(doc) -> 23 | "Tests that any number of packets can be encoded, concatenated and decoded". 24 | 25 | prop_stream() -> 26 | ?FORALL(Stream, mtp_prop_gen:stream_4b(), stream_codec(Stream)). 27 | 28 | stream_codec(Stream) -> 29 | Codec = mtp_full:new(), 30 | {BinStream, Codec1} = 31 | lists:foldl( 32 | fun(Bin, {Acc, Codec1}) -> 33 | {Data, Codec2} = mtp_full:encode_packet(Bin, Codec1), 34 | {<>, 35 | Codec2} 36 | end, {<<>>, Codec}, Stream), 37 | DecodedStream = decode_stream(BinStream, Codec1, []), 38 | Stream == DecodedStream. 39 | 40 | decode_stream(BinStream, Codec, Acc) -> 41 | case mtp_full:try_decode_packet(BinStream, Codec) of 42 | {incomplete, _} -> 43 | lists:reverse(Acc); 44 | {ok, DecPacket, Tail, Codec1} -> 45 | decode_stream(Tail, Codec1, [DecPacket | Acc]) 46 | end. 47 | -------------------------------------------------------------------------------- /test/prop_mtp_intermediate.erl: -------------------------------------------------------------------------------- 1 | %% @doc property-based tests for mtp_intermediate 2 | -module(prop_mtp_intermediate). 3 | -include_lib("proper/include/proper.hrl"). 4 | 5 | -export([prop_codec/1, prop_stream/1, prop_stream_padding/1]). 6 | 7 | 8 | prop_codec(doc) -> 9 | "Tests that any 4-byte aligned binary can be encoded and decoded back". 10 | 11 | prop_codec() -> 12 | ?FORALL(Bin, mtp_prop_gen:packet_4b(), codec(Bin)). 13 | 14 | codec(Bin) -> 15 | Codec = mtp_intermediate:new(), 16 | {Data, Codec1} = mtp_intermediate:encode_packet(Bin, Codec), 17 | {ok, Decoded, <<>>, _} = mtp_intermediate:try_decode_packet(iolist_to_binary(Data), Codec1), 18 | Decoded == Bin. 19 | 20 | 21 | prop_stream(doc) -> 22 | "Tests that any number of packets can be encoded, concatenated and decoded". 23 | 24 | prop_stream() -> 25 | ?FORALL(Stream, mtp_prop_gen:stream_4b(), stream_codec(Stream, false)). 26 | 27 | stream_codec(Stream, Padding) -> 28 | Codec = mtp_intermediate:new(#{padding => Padding}), 29 | {BinStream, Codec1} = 30 | lists:foldl( 31 | fun(Bin, {Acc, Codec1}) -> 32 | {Data, Codec2} = mtp_intermediate:encode_packet(Bin, Codec1), 33 | {<>, 34 | Codec2} 35 | end, {<<>>, Codec}, Stream), 36 | DecodedStream = decode_stream(BinStream, Codec1, []), 37 | Stream == DecodedStream. 38 | 39 | decode_stream(BinStream, Codec, Acc) -> 40 | case mtp_intermediate:try_decode_packet(BinStream, Codec) of 41 | {incomplete, _} -> 42 | lists:reverse(Acc); 43 | {ok, DecPacket, Tail, Codec1} -> 44 | decode_stream(Tail, Codec1, [DecPacket | Acc]) 45 | end. 46 | 47 | 48 | prop_stream_padding(doc) -> 49 | "Tests that any number of packets can be encoded, concatenated and decoded" 50 | " using encoder with random padding enabled". 51 | 52 | prop_stream_padding() -> 53 | ?FORALL(Stream, mtp_prop_gen:stream_4b(), stream_codec(Stream, true)). 54 | -------------------------------------------------------------------------------- /test/prop_mtp_obfuscated.erl: -------------------------------------------------------------------------------- 1 | %% @doc property-based tests for mtp_obfuscated 2 | -module(prop_mtp_obfuscated). 3 | -include_lib("proper/include/proper.hrl"). 4 | 5 | -export([prop_stream/1, 6 | prop_client_server_handshake/1, 7 | prop_client_server_stream/1]). 8 | 9 | prop_stream(doc) -> 10 | "Tests that any number of packets can be encrypted with mtp_obfuscatedcoded," 11 | " concatenated and decoded as a stream using the same key for encoding and decoding". 12 | 13 | prop_stream() -> 14 | ?FORALL({Key, Iv, Stream}, stream_arg_set(), stream_codec(Key, Iv, Stream)). 15 | 16 | 17 | stream_arg_set() -> 18 | proper_types:tuple( 19 | [mtp_prop_gen:key(), 20 | mtp_prop_gen:iv(), 21 | mtp_prop_gen:stream_4b() 22 | ]). 23 | 24 | stream_codec(Key, Iv, Stream) -> 25 | Codec = mtp_obfuscated:new(Key, Iv, Key, Iv), 26 | {BinStream, Codec2} = 27 | lists:foldl( 28 | fun(Bin, {Acc, Codec1}) -> 29 | {Data, Codec2} = mtp_obfuscated:encrypt(Bin, Codec1), 30 | {<>, 31 | Codec2} 32 | end, {<<>>, Codec}, Stream), 33 | {Decrypted, <<>>, _Codec3} = mtp_obfuscated:decrypt(BinStream, Codec2), 34 | %% io:format("Dec: ~p~nOrig: ~p~nCodec: ~p~n", [Decrypted, Stream, _Codec3]), 35 | Decrypted == iolist_to_binary(Stream). 36 | 37 | 38 | prop_client_server_handshake(doc) -> 39 | "Tests that for any secret, protocol and dc_id, it's possible to perform handshake". 40 | 41 | prop_client_server_handshake() -> 42 | ?FORALL({Secret, DcId, Protocol}, cs_hs_arg_set(), 43 | cs_hs_exchange(Secret, DcId, Protocol)). 44 | 45 | cs_hs_arg_set() -> 46 | proper_types:tuple( 47 | [mtp_prop_gen:secret(), 48 | mtp_prop_gen:dc_id(), 49 | mtp_prop_gen:codec()]). 50 | 51 | cs_hs_exchange(Secret, DcId, Protocol) -> 52 | %% io:format("Secret: ~p; DcId: ~p, Protocol: ~p~n", 53 | %% [Secret, DcId, Protocol]), 54 | {Packet, _, _, _CliCodec} = mtp_obfuscated:client_create(Secret, Protocol, DcId), 55 | case mtp_obfuscated:from_header(Packet, Secret) of 56 | {ok, DcId, Protocol, _SrvCodec} -> 57 | true; 58 | _ -> 59 | false 60 | end. 61 | 62 | prop_client_server_stream(doc) -> 63 | "Tests that for any secret, protocol and dc_id, it's possible to perform" 64 | " handshake/key exchange and then do bi-directional encode/decode stream of data". 65 | 66 | prop_client_server_stream() -> 67 | ?FORALL({Secret, DcId, Protocol, Stream}, cs_stream_arg_set(), 68 | cs_stream_exchange(Secret, DcId, Protocol, Stream)). 69 | 70 | cs_stream_arg_set() -> 71 | proper_types:tuple( 72 | [mtp_prop_gen:secret(), 73 | mtp_prop_gen:dc_id(), 74 | mtp_prop_gen:codec(), 75 | mtp_prop_gen:stream_4b()]). 76 | 77 | cs_stream_exchange(Secret, DcId, Protocol, Stream) -> 78 | %% io:format("Secret: ~p; DcId: ~p, Protocol: ~p~n", 79 | %% [Secret, DcId, Protocol]), 80 | {Header, _, _, CliCodec} = mtp_obfuscated:client_create(Secret, Protocol, DcId), 81 | {ok, DcId, Protocol, SrvCodec} = mtp_obfuscated:from_header(Header, Secret), 82 | 83 | %% Client to server 84 | {CliCodec1, 85 | SrvCodec1, 86 | Cli2SrvTransmitted} = transmit_stream(CliCodec, SrvCodec, Stream), 87 | {_CliCodec2, 88 | _SrvCodec2, 89 | Srv2CliTransmitted} = transmit_stream(SrvCodec1, CliCodec1, Stream), 90 | BinStream = iolist_to_binary(Stream), 91 | (Cli2SrvTransmitted == BinStream) 92 | andalso (Srv2CliTransmitted == BinStream). 93 | 94 | transmit_stream(EncCodec, DecCodec, Stream) -> 95 | {EncStream, EncCodec3} = 96 | lists:foldl( 97 | fun(Packet, {Acc, CliCodec1}) -> 98 | {Data, CliCodec2} = mtp_obfuscated:encrypt(Packet, CliCodec1), 99 | {<>, 100 | CliCodec2} 101 | end, {<<>>, EncCodec}, Stream), 102 | {Decrypted, <<>>, DecCodec2} = mtp_obfuscated:decrypt(EncStream, DecCodec), 103 | {EncCodec3, 104 | DecCodec2, 105 | Decrypted}. 106 | -------------------------------------------------------------------------------- /test/prop_mtp_rpc.erl: -------------------------------------------------------------------------------- 1 | %% @doc property-based tests for mtp_rpc 2 | -module(prop_mtp_rpc). 3 | -include_lib("proper/include/proper.hrl"). 4 | 5 | -export([prop_nonce/1, 6 | prop_handshake/1, 7 | prop_s2c_packet/1, 8 | prop_c2s_packet/1]). 9 | 10 | 11 | prop_nonce(doc) -> 12 | "Tests encode/decode of 'nonce' RPC packet". 13 | 14 | prop_nonce() -> 15 | ?FORALL(Packet, nonce_gen(), nonce(Packet)). 16 | 17 | nonce_gen() -> 18 | {nonce, 19 | proper_types:binary(4), 20 | 1, 21 | proper_types:pos_integer(), 22 | proper_types:binary(16)}. 23 | 24 | nonce(Packet) -> 25 | Bin = mtp_rpc:encode_nonce(Packet), 26 | Packet == mtp_rpc:decode_nonce(Bin). 27 | 28 | 29 | prop_handshake(doc) -> 30 | "Tests encode/decode of 'handshake' RPC packet". 31 | 32 | prop_handshake() -> 33 | ?FORALL(Packet, handshake_gen(), handshake(Packet)). 34 | 35 | handshake_gen() -> 36 | {handshake, 37 | proper_types:binary(12), 38 | proper_types:binary(12)}. 39 | 40 | handshake(Packet) -> 41 | Bin = mtp_rpc:encode_handshake(Packet), 42 | Packet == mtp_rpc:decode_handshake(Bin). 43 | 44 | 45 | prop_s2c_packet(doc) -> 46 | "Tests encode/decode of 'proxy_ans'/'close_ext' RPC packets". 47 | 48 | prop_s2c_packet() -> 49 | ?FORALL(Packet, s2c_packet_gen(), s2c_packet(Packet)). 50 | 51 | s2c_packet_gen() -> 52 | proper_types:oneof( 53 | [ 54 | {proxy_ans, 55 | proper_types:integer(), 56 | proper_types:binary()}, 57 | {close_ext, 58 | proper_types:integer()} 59 | ]). 60 | 61 | s2c_packet(Packet) -> 62 | Bin = mtp_rpc:srv_encode_packet(Packet), 63 | Packet == mtp_rpc:decode_packet(Bin). 64 | 65 | 66 | prop_c2s_packet(doc) -> 67 | "Tests encode/decode of 'data'/'remote_closed' RPC packets". 68 | 69 | prop_c2s_packet() -> 70 | ?FORALL(Packet, c2s_packet_gen(), c2s_packet(Packet)). 71 | 72 | c2s_packet_gen() -> 73 | proper_types:oneof( 74 | [ 75 | {{data, 76 | mtp_prop_gen:packet_4b() %Data 77 | }, 78 | {{proper_types:integer(), %ConnId 79 | proper_types:binary(20), %ClientAddr 80 | proper_types:binary(16) %ProxyTag 81 | }, 82 | proper_types:binary(20) %ProxyAddr 83 | }}, 84 | {remote_closed, 85 | proper_types:integer()} 86 | ]). 87 | 88 | c2s_packet({{data, Data} = Packet, {{ConnId, _, _}, _} = Static}) -> 89 | Bin = mtp_rpc:encode_packet(Packet, Static), 90 | {data, ConnId, Data} == mtp_rpc:srv_decode_packet(iolist_to_binary(Bin)); 91 | c2s_packet({remote_closed, ConnId} = Packet) -> 92 | Bin = mtp_rpc:encode_packet(remote_closed, ConnId), 93 | Packet == mtp_rpc:srv_decode_packet(iolist_to_binary(Bin)). 94 | -------------------------------------------------------------------------------- /test/prop_mtp_statefull.erl: -------------------------------------------------------------------------------- 1 | %% @doc Statefull property-based tests 2 | -module(prop_mtp_statefull). 3 | -export([prop_check_pooling/0, 4 | prop_check_pooling/1, 5 | initial_state/0, 6 | command/1, 7 | precondition/2, 8 | postcondition/3, 9 | next_state/3]). 10 | -export([connect/2, 11 | echo_packet/2, 12 | ask_for_close/1, 13 | close/1]). 14 | -export([gen_rpc_echo/3, 15 | gen_rpc_close/3]). 16 | 17 | -include_lib("proper/include/proper.hrl"). 18 | -include_lib("common_test/include/ct.hrl"). 19 | -include_lib("stdlib/include/assert.hrl"). 20 | 21 | -record(st, { 22 | ever_opened = 0, 23 | open = [], 24 | closed = [], 25 | ask_for_close = [], 26 | n_packets = #{} 27 | }). 28 | 29 | -define(PORT, 10800). 30 | -define(SECRET, <<"d0d6e111bada5511fcce9584deadbeef">>). 31 | -define(HOST, {127, 0, 0, 1}). 32 | -define(DC_ID, 1). 33 | -define(APP, mtproto_proxy). 34 | 35 | prop_check_pooling(doc) -> 36 | "Check that connections and packets are 'accounted' correctly". 37 | 38 | prop_check_pooling() -> 39 | ?FORALL(Cmds, commands(?MODULE), aggregate(command_names(Cmds), run_cmds(Cmds))). 40 | 41 | initial_state() -> 42 | #st{}. 43 | 44 | command(#st{open = [], ever_opened = EO}) -> 45 | {call, ?MODULE, connect, [EO, mtp_prop_gen:codec()]}; 46 | command(#st{open = L, ever_opened = EO}) -> 47 | proper_types:frequency( 48 | [ 49 | {1, {call, ?MODULE, connect, [EO, proper_types:oneof( 50 | [mtp_prop_gen:codec(), 51 | {mtp_fake_tls, <<"en.wikipedia.org">>}])]}}, 52 | {5, {call, ?MODULE, echo_packet, [proper_types:oneof(L), proper_types:binary()]}}, 53 | {2, {call, ?MODULE, close, [proper_types:oneof(L)]}}, 54 | {2, {call, ?MODULE, ask_for_close, [proper_types:oneof(L)]}} 55 | ]). 56 | 57 | precondition(#st{open = L}, {call, ?MODULE, close, _}) -> 58 | length(L) > 0; 59 | precondition(#st{open = L}, {call, ?MODULE, echo_packet, _}) -> 60 | length(L) > 0; 61 | precondition(#st{open = L}, {call, ?MODULE, ask_for_close, _}) -> 62 | length(L) > 0; 63 | precondition(_St, {call, _Mod, _Fun, _Args}) -> 64 | true. 65 | 66 | %% Given the state `State' *prior* to the call `{call, Mod, Fun, Args}', 67 | %% determine whether the result `Res' (coming from the actual system) 68 | %% makes sense. 69 | postcondition(_State, {call, ?MODULE, connect, _Args}, _Res) -> 70 | true; 71 | postcondition(_State, {call, ?MODULE, close, _Args}, _Res) -> 72 | true; 73 | postcondition(_State, {call, ?MODULE, ask_for_close, _Args}, _Res) -> 74 | true; 75 | postcondition(_State, {call, ?MODULE, echo_packet, [_Conn, SendBin]}, RecvBin) -> 76 | ?assertEqual(SendBin, RecvBin), 77 | true; 78 | postcondition(_State, {call, _Mod, _Fun, _Args}, _Res) -> 79 | false. 80 | 81 | %% Assuming the postcondition for a call was true, update the model 82 | %% accordingly for the test to proceed. 83 | next_state(#st{open = L, ever_opened = EO} = St, _Res, 84 | {call, ?MODULE, connect, [ConnId, _Proto]}) -> 85 | St#st{open = [ConnId | L], 86 | ever_opened = EO + 1}; 87 | next_state(#st{open = L, closed = Cl} = St, _Res, {call, ?MODULE, close, [ConnId]}) -> 88 | St#st{open = lists:delete(ConnId, L), 89 | closed = [ConnId | Cl]}; 90 | next_state(#st{open = L, closed = Cl, ask_for_close = NA} = St, _Res, 91 | {call, ?MODULE, ask_for_close, [ConnId]}) -> 92 | St#st{open = lists:delete(ConnId, L), 93 | closed = [ConnId | Cl], 94 | ask_for_close = [ConnId | NA]}; 95 | next_state(#st{n_packets = N} = St, _Res, {call, ?MODULE, echo_packet, [ConnId, _]}) -> 96 | NForConn = maps:get(ConnId, N, 0), 97 | St#st{n_packets = N#{ConnId => NForConn + 1}}; 98 | next_state(State, _Res, {call, ?MODULE, _, _}) -> 99 | State. 100 | 101 | run_cmds(Cmds) -> 102 | Cfg = setup(#{rpc_handler => mtp_test_cmd_rpc}), 103 | {History, State, Result} = run_commands(?MODULE, Cmds), 104 | %% Validate final states of proxy and "middle server" 105 | timer:sleep(100), 106 | ServerState = collect_server_state(Cfg), 107 | Metrics = collect_metrics(Cfg), 108 | ShimDump = shim_dump(), 109 | stop(Cfg), 110 | ?WHENFAIL(io:format("History: ~p\n" 111 | "State: ~w\n" 112 | "ServerState: ~p\n" 113 | "Metrics: ~p\n" 114 | "Result: ~p\n", 115 | [History, State, ServerState, Metrics, Result]), 116 | proper:conjunction( 117 | [{state_ok, check_state(State, ServerState, Metrics, ShimDump)}, 118 | {result_ok, Result =:= ok}])). 119 | 120 | %% Post-run checks. Assert that model's final state matches proxy and middle-server state 121 | collect_server_state(Cfg) -> 122 | DcCfg = ?config(dc_conf, Cfg), 123 | Pids = mtp_test_datacenter:middle_connections(DcCfg), 124 | States = [mtp_test_middle_server:get_rpc_handler_state(Pid) || Pid <- Pids], 125 | %% io:format("~p~n", [States]), 126 | %% Can use just maps:merge/2 because connection IDs in different states will not overlap 127 | lists:foldl(fun maps:merge/2, #{}, States). 128 | 129 | collect_metrics(_Cfg) -> 130 | GetTags = fun(Type, Name, Tags) -> 131 | case mtp_test_metric:get_tags(Type, Name, Tags) of 132 | not_found when Type == histogram -> {0, 0, 0, 0}; 133 | not_found -> 0; 134 | Val -> Val 135 | end 136 | end, 137 | #{in_connections => GetTags(count, [?APP, in_connection, total], [?MODULE]), 138 | closed_connections => GetTags(count, [?APP, in_connection_closed, total], [?MODULE]), 139 | tg_in_packet_size => GetTags( 140 | histogram, [?APP, tg_packet_size, bytes], [upstream_to_downstream]), 141 | tg_out_packet_size => GetTags( 142 | histogram, [?APP, tg_packet_size, bytes], [downstream_to_upstream]) 143 | }. 144 | 145 | 146 | check_state(#st{closed = ModClosed, n_packets = ModPackets, ask_for_close = ModAskClose, 147 | open = ModClients, ever_opened = ModOpened} = _St, 148 | SrvState, Metrics, ShimDump) -> 149 | %% io:format("~n~w~n~p~n~p~n~p~n", [St, SrvState, Metrics, ShimDump]), 150 | %% Assert shim is correct 151 | ?assertEqual(length(ModClients), map_size(ShimDump)), 152 | 153 | %% Total number of packets 154 | ModTotalPackets = maps:fold(fun(_K, N, Acc) -> Acc + N end, 0, ModPackets), 155 | SrvTotalPackets = maps:fold(fun({n_packets, _}, N, Acc) -> Acc + N; 156 | (_, _, Acc) -> Acc 157 | end, 0, SrvState), 158 | ?assertEqual(ModTotalPackets, SrvTotalPackets), 159 | 160 | %% Number of connections that ever sent data RPC 161 | SrvConnsWithPackets = maps:fold(fun({n_packets, _}, _, Acc) -> Acc + 1; 162 | (_, _, Acc) -> Acc 163 | end, 0, SrvState), 164 | ?assertEqual(map_size(ModPackets), SrvConnsWithPackets), 165 | 166 | %% Number of sent data RPC per-connection 167 | ModSentPerConn = maps:values(ModPackets), 168 | SrvSentPerConn = maps:fold(fun({n_packets, _}, N, Acc) -> [N | Acc]; 169 | (_, _, Acc) -> Acc 170 | end, [], SrvState), 171 | ?assertEqual(lists:sort(ModSentPerConn), lists:sort(SrvSentPerConn)), 172 | 173 | %% Number of telegram packets send from client to server 174 | ModTgPackets = length(ModAskClose) + ModTotalPackets, 175 | ?assertMatch({ModTgPackets, _, _, _}, maps:get(tg_in_packet_size, Metrics)), 176 | 177 | %% Number of connections that were ever open 178 | %% Can be only asserted by metrics 179 | ?assertEqual(ModOpened, maps:get(in_connections, Metrics)), 180 | 181 | %% Number of connections that were closed 182 | SrvClosed = maps:fold(fun(_, tombstone, Acc) -> Acc + 1; 183 | (_, _, Acc) -> Acc 184 | end, 0, SrvState), 185 | ?assertEqual(length(ModClosed), SrvClosed), 186 | ?assertEqual(length(ModClosed), maps:get(closed_connections, Metrics)), 187 | 188 | %% Number of still open connections 189 | %% On middleproxy side, connection only started to be tracked if it sent any data. 190 | %% So, if we opened a connection and haven't sent anything, middle will not know about it 191 | MAlive = length(ordsets:intersection( 192 | ordsets:from_list(ModClients), 193 | ordsets:from_list(maps:keys(ModPackets)))), 194 | SrvAlive = maps:fold(fun(Id, Num, Acc) when is_integer(Id), is_integer(Num) -> Acc + 1; 195 | (_, _, Acc) -> Acc 196 | end, 0, SrvState), 197 | ?assertEqual(MAlive, SrvAlive), 198 | true. 199 | 200 | %% Connect to proxy 201 | connect(Id, Protocol) -> 202 | Conn = mtp_test_client:connect(?HOST, ?PORT, ?SECRET, ?DC_ID, Protocol), 203 | shim_add(Id, Conn), 204 | ok. 205 | 206 | %% Send and receive back some binary data 207 | echo_packet(Id, RandBin) -> 208 | Cli0 = shim_pop(Id), 209 | Req = mtp_test_cmd_rpc:call(?MODULE, gen_rpc_echo, RandBin), 210 | Cli1 = mtp_test_client:send(Req, Cli0), 211 | {ok, Res, Cli2} = mtp_test_client:recv_packet(Cli1, 1000), 212 | shim_add(Id, Cli2), 213 | mtp_test_cmd_rpc:packet_to_term(Res). 214 | 215 | gen_rpc_echo(RandBin, ConnId, St) -> 216 | Key = {n_packets, ConnId}, 217 | NPackets = maps:get(Key, St, 0), 218 | {reply, RandBin, St#{ConnId => 1, 219 | Key => NPackets + 1}}. 220 | 221 | %% Close from client-side 222 | close(Id) -> 223 | Conn = shim_pop(Id), 224 | mtp_test_client:close(Conn). 225 | 226 | %% Close from telegram-server side 227 | ask_for_close(Id) -> 228 | Cli0 = shim_pop(Id), 229 | Req = mtp_test_cmd_rpc:call(?MODULE, gen_rpc_close, []), 230 | Cli1 = mtp_test_client:send(Req, Cli0), 231 | {error, closed} = mtp_test_client:recv_packet(Cli1, 1000), 232 | ok. 233 | 234 | gen_rpc_close([], _ConnId, St) -> 235 | {close, St}. 236 | 237 | -ifdef(OTP_RELEASE). 238 | disable_log() -> 239 | logger:set_primary_config(level, critical). 240 | -else. 241 | disable_log() -> 242 | ok. 243 | -endif. 244 | 245 | %% Setup / teardown 246 | setup(DcCfg0) -> 247 | application:ensure_all_started(lager), 248 | lager:set_loglevel(lager_console_backend, critical), %XXX lager-specific 249 | disable_log(), 250 | {ok, Pid} = mtp_test_metric:start_link(), 251 | PubKey = crypto:strong_rand_bytes(128), 252 | DcId = ?DC_ID, 253 | Ip = ?HOST, 254 | DcConf = [{DcId, Ip, ?PORT + 5}], 255 | Secret = ?SECRET, 256 | Listeners = [#{name => ?MODULE, 257 | port => ?PORT, 258 | listen_ip => inet:ntoa(Ip), 259 | secret => Secret, 260 | tag => <<"dcbe8f1493fa4cd9ab300891c0b5b326">>}], 261 | application:load(mtproto_proxy), 262 | Cfg1 = single_dc_SUITE:set_env([{ports, Listeners}, 263 | {metric_backend, mtp_test_metric}], []), 264 | {ok, DcCfg} = mtp_test_datacenter:start_dc(PubKey, DcConf, DcCfg0), 265 | application:load(mtproto_proxy), 266 | {ok, _} = application:ensure_all_started(mtproto_proxy), 267 | shim_start(), 268 | [{dc_conf, DcCfg}, {metric, Pid} | Cfg1]. 269 | 270 | stop(Cfg) -> 271 | DcCfg = ?config(dc_conf, Cfg), 272 | MetricPid = ?config(metric, Cfg), 273 | ok = application:stop(mtproto_proxy), 274 | {ok, _} = mtp_test_datacenter:stop_dc(DcCfg), 275 | single_dc_SUITE:reset_env(Cfg), 276 | gen_server:stop(MetricPid), 277 | shim_stop(), 278 | Cfg. 279 | 280 | 281 | %% Process - wrapper holding client connections and states 282 | shim_add(Id, Conn) -> 283 | ?MODULE ! {add, Id, Conn}. 284 | 285 | shim_pop(Id) -> 286 | ?MODULE ! {pop, self(), Id}, 287 | receive {conn, Conn} -> 288 | Conn 289 | end. 290 | 291 | shim_dump() -> 292 | ?MODULE ! {dump, self()}, 293 | receive {dump, Conns} -> 294 | Conns 295 | end. 296 | 297 | shim_start() -> 298 | Pid = proc_lib:spawn_link(fun loop/0), 299 | register(?MODULE, Pid). 300 | 301 | shim_stop() -> 302 | Pid = whereis(?MODULE), 303 | unregister(?MODULE), 304 | exit(Pid, normal). 305 | 306 | loop() -> 307 | loop(#{}). 308 | 309 | 310 | loop(Acc) -> 311 | receive 312 | {dump, From} -> 313 | From ! {dump, Acc}, 314 | loop(Acc); 315 | {add, Id, Conn} -> 316 | false = maps:is_key(Id, Acc), 317 | loop(Acc#{Id => Conn}); 318 | {pop, From, Id} -> 319 | {Conn, Acc1} = maps:take(Id, Acc), 320 | From ! {conn, Conn}, 321 | loop(Acc1) 322 | end. 323 | -------------------------------------------------------------------------------- /test/test-sys.config: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang -*- 2 | [ 3 | {mtproto_proxy, 4 | [ 5 | {ports, []}, 6 | {external_ip, "127.0.0.1"}, 7 | {listen_ip, "127.0.0.1"}, 8 | {num_acceptors, 2}, 9 | {init_dc_connections, 1}, 10 | {tls_allowed_domains, any}, 11 | {metric_backend, mtp_test_metric} 12 | ]}, 13 | 14 | %% Logging config 15 | {lager, 16 | [{log_root, "log"}, 17 | {crash_log, "test-crash.log"}, 18 | {handlers, 19 | [ 20 | {lager_console_backend, 21 | [{level, critical}]}, 22 | 23 | {lager_file_backend, 24 | [{file, "test-application.log"}, 25 | {level, info}, 26 | 27 | %% Do fsync only on critical messages 28 | {sync_on, critical} 29 | ]} 30 | ]}]}, 31 | 32 | {sasl, 33 | [{errlog_type, error}]} 34 | ]. 35 | --------------------------------------------------------------------------------