diff --git a/.travis.yml b/.travis.yml index 7dd05a5..23230cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,7 @@ sudo: false dist: trusty go: - - "1.11.x" - - 1.12.x + - 1.13.x - master before_script: make prepare @@ -14,7 +13,6 @@ before_script: make prepare script: - make all - make lint - - make test matrix: allow_failures: diff --git a/Dockerfile b/Dockerfile index 88f81fc..fa0cb12 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ############################################################################### # BUILD STAGE -FROM golang:1.12-alpine +FROM golang:1.13-alpine RUN set -x \ && apk --no-cache --update add \ diff --git a/Makefile b/Makefile index dfed95d..e56ab75 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ APP_NAME := $(IMAGE_NAME) CC_BINARIES := $(shell bash -c "echo -n $(APP_NAME)-{linux,freebsd,openbsd}-{386,amd64} $(APP_NAME)-linux-{arm,arm64}") -GOLANGCI_LINT_VERSION := v1.15.0 +GOLANGCI_LINT_VERSION := v1.21.0 VERSION_GO := $(shell go version) VERSION_DATE := $(shell date -Ru) @@ -51,10 +51,6 @@ crosscompile: $(CC_BINARIES) crosscompile-dir: @rm -rf "$(CC_DIR)" && mkdir -p "$(CC_DIR)" -.PHONY: test -test: vendor - @$(MOD_ON) go test -v ./... - .PHONY: lint lint: vendor @$(MOD_OFF) golangci-lint run diff --git a/README.md b/README.md index 4428969..22a5e9f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Bullshit-free MTPROTO proxy for Telegram [![Go Report Card](https://goreportcard.com/badge/github.com/9seconds/mtg)](https://goreportcard.com/report/github.com/9seconds/mtg) [![Docker Build Status](https://img.shields.io/docker/build/nineseconds/mtg.svg)](https://hub.docker.com/r/nineseconds/mtg/) +**Please see a guide on upgrading to 1.0 at the end of this README.** + # Rationale There are several available proxies for Telegram MTPROTO available. Here @@ -15,33 +17,33 @@ are the most notable: * [Python](https://github.com/alexbers/mtprotoproxy) * [Erlang](https://github.com/seriyps/mtproto_proxy) -Almost all of them follow the way how official proxy was build. This -includes support of multiple secrets, support of promoted channels etc. +Almost all of them follow the way how official proxy was built. This +includes support of multiple secrets, support of promoted channels, etc. mtg is an implementation in golang which is intended to be: * **Lightweight** - It has to consume as less resources as possible but not by losing + It has to consume as few resources as possible but not by losing maintainability. * **Easily deployable** I strongly believe that Telegram proxies should follow the way of ShadowSocks: promoted channels is a strange way of doing business I suppose. I think the only viable way is to have a proxy with minimum configuration which should work everywhere. -* **Single secret** - I think that multiple secrets solves no problems and just complexify - software. I also believe that in case of throwout proxies, this feature - is useless luxury. +* **A single secret** + I think that multiple secrets solve no problems and just complexify + software. I also believe that in the case of throwout proxies, this + feature is a useless luxury. * **Minimum docker image size** - Official image is less than 3 megabytes. Literally. + Official image is less than 3.5 megabytes. Literally. * **No management WebUI** - This is an implementation of simple lightweight proxy. I won't do that. + This is an implementation of a simple lightweight proxy. I won't do that. This proxy supports 2 modes of work: direct connection to Telegram and promoted channel mode. If you do not need promoted channels, I would -recommend you to go with direct mode: this is way more robust. +recommend you to go with direct mode: this way is more robust. -To run proxy in direct mode, all you need to do is just provide a +To run a proxy in direct mode, all you need to do is just provide a secret. If you do not provide ADTag as a second parameter, promoted channels mode won't be activated. @@ -102,95 +104,64 @@ Also, there is another project on Ansible Galaxy: https://galaxy.ansible.com/iva # Configuration -Basically, to run this tool you need to configure as less as possible. +To run this tool you need to configure as less as possible. Telegram +clients support 3 different secret types: + +* Simple - basically, it is just a flow of frames ciphered by AES-CTR stream + cipher. +* Secured - the same stream as simple but with some random noise to prevent + statistical analysis of traffic flow. +* FakeTLS - this mode envelops telegram stream in TLS so it looks (in theory) + the same as any TLS1.3 traffic from DPI point of view. + +If you do not have preferences, go with FakeTLS or at least secured. +Simple mode is a little bit naive and traffic flow can be easily +identified as Telegram one. + +Unlike the rest of implementation, mtg is quite strict about the +execution mode: if you run a proxy instance with FakeTLS secret, you +can't connect to it with simple or secured clients. You can't connect +to the proxy with secured secret with FakeTLS key. It forces one mode +of working. So, unfortunately, there is no way how to connect to the +deployed proxy with another secret (if you know how to construct and +convert them). But at the same time, old clients can't connect so they +won't expose the type of the service. First, you need to generate a secret: ```console -openssl rand -hex 16 +$ mtg generate-secret simple +52a493bdfb90eea55739eabff2d92a14 ``` -or - ```console -head -c 512 /dev/urandom | md5sum | cut -f 1 -d ' ' +$ mtg generate-secret secured +ddf05fb7acb549be047a7c585116581418 ``` -## Secure mode - -_tl;dr - use secret mode for all new installation of proxy; only clients -with dd-secrets will be able to connect. This mode abuses attempts to -DPI MTPROTO traffic._ - -Secure mode is not the best name and of course, it creates a lot of -confusion. To explain what it means, we need to tell you some bits on -dd-secrets. - -MTPROTO proxy protocol requires 16-byte secret. You usually -propagate it as a 32 characters hexadecimal string like -`282831900f371ca182feb0e4e1e1aeef` (if you decode this string -to bytes, you will get a real secret which is used in the -protocol). Everything went quite good until the moment when -developers found an evidence that [protocol is quite weak to -DPI](https://github.com/TelegramMessenger/MTProxy/issues/35) and some -enthusiasts even created simple proofs of concepts on [detecting MTPROTO -traffic](https://github.com/darkk/poormansmtproto). - -Telegram team has introduced a patch called dd-secrets. If you have -a secret `282831900f371ca182feb0e4e1e1aeef` then your dd-secret is -`dd282831900f371ca182feb0e4e1e1aeef`. That is, you just add dd prefix -to the secret, prepend it with dd. In that case, original secret -`282831900f371ca182feb0e4e1e1aeef` is used but client and server start -to act a little bit different: they start to add random noise to the -packets so they can't be detected by their length. In order to keep -backward compatibility, all proxies a quite liberal to the secrets to -use: if the client uses plain secret, without dd prefix, they fall back -to the normal behavior. If dd-secret is used (proxy can extract this -information on the handshake), then more secured, the hardened behavior -is used. - -Yes, it can look like a hack but it is as it is. - -Now going back to the secure mode: if you do not pass `-s` flag to the -mtg, then it checks what mode is requested by the client. If the client -uses plain secret, without dd prefix, then proxy falls back to the -original behavior and do not play with paddings. If dd-secret is used -and client demands this mode, then proxy start to add that random noise -to the packets. But if you pass `-s`, then only clients with dd-secrets -can connect. How to migrate existing clients then? If a client is new -enough, you can just prepend the secret with dd string in the settings. -If it is an old guy, then nothing to do, sorry. - -Why this mode matters? We do not have evidence but there is quite a big -suspicion that some ISPs start to filter MTPROTO traffic. If they detect -the IP address which acts as a proxy, they block it and no clients can -use this proxy. This is an attempt to prevent such a situation. - -General rule of thumb: with all new installation of proxies I would -advise to go with secure mode by default. But please do remember that it -means that clients, which do not pass dd-prefix to their secrets, will -not be able to connect. *Secure mode works only with dd-prefixes!* - -Oneliners to generate such secrets: - ```console -echo dd$(openssl rand -hex 16) +$ mtg generate-secret -c google.com tls +ee852380f362a09343efb4690c4e17862e676f6f676c652e636f6d ``` -or - -```console -echo dd$(head -c 512 /dev/urandom | md5sum | cut -f 1 -d ' ') -``` - - ## Antireplay cache -In order to prevent replay attacks, we have internal storage of first -frames messages for connected clients. These frames are generated -randomly by design and we have negligible possibility of duplication -(probability is 1/(2^64)) but it could be quite effective in order to -prevent replays. +To prevent replay attacks, we have internal storage of first frames +messages for connected clients. These frames are generated randomly +by design and we have the negligible possibility of duplication +(probability is 1/(2^64)) but it could be quite effective to prevent +replays. + + +## FakeTLS + +If you run this a proxy in faketls mode, this proxy will try to hide +itself cloaking a host provided as a part of the generated secret. It +means that if you cloak google.com then you can curl this proxy and +you'll get a google.com response back. + +mtg proxies L3 traffic. In other words, only TCP, without interfering in +TLS, HTTP or any other high-level protocol. ## Environment variables @@ -199,30 +170,26 @@ It is possible to configure this tool using environment variables. You can configure any flag but not secret or adtag. Here is the list of supported environment variables: -| Environment variable | Corresponding flags | Default value | Description | -|-------------------------------|-----------------------------|-----------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `MTG_DEBUG` | `-d`, `--debug` | `false` | Run in debug mode. Usually, you need to run in this mode only if you develop this tool or its maintainer is asking you to provide logs with such verbosity. | -| `MTG_VERBOSE` | `-v`, `--verbose` | `false` | Run in verbose mode. This is way less chatty than debug mode. | -| `MTG_IP` | `-b`, `--bind-ip` | `127.0.0.1` | Which IP should we bind to. As usual, `0.0.0.0` means that we want to listen on all interfaces. Also, 4 zeroes will bind to both IPv4 and IPv6. | -| `MTG_PORT` | `-p`, `--bind-port` | `3128` | Which port should we bind to (listen on). | -| `MTG_IPV4` | `-4`, `--public-ipv4` | [Autodetect](https://ifconfig.co) | IPv4 address of this proxy. This is required if you NAT your proxy or run it in a docker container. In that case, you absolutely need to specify public IPv4 address of the proxy, otherwise either URLs will be broken or proxy could not access Telegram middle proxies. | -| `MTG_IPV4_PORT` | `--public-ipv4-port` | Value of `--bind-port` | Which port should be public of IPv4 interface. This affects only generated links and should be changed only if you NAT your proxy or run it in a docker container. | -| `MTG_IPV6` | `-6`, `--public-ipv6` | [Autodetect](https://ifconfig.co) | IPv6 address of this proxy. This is required if you NAT your proxy or run it in a docker container. In that case, you absolutely need to specify public IPv6 address of the proxy, otherwise either URLs will be broken or proxy could not access Telegram middle proxies. | -| `MTG_IPV6_PORT` | `--public-ipv6-port` | Value of `--bind-port` | Which port should be public of IPv6 interface. This affects only generated links and should be changed only if you NAT your proxy or run it in a docker container. | -| `MTG_STATS_IP` | `-t`, `--stats-ip` | `127.0.0.1` | Which IP should we bind the internal statistics HTTP server. | -| `MTG_STATS_PORT` | `-q`, `--stats-port` | `3129` | Which port should we bind the internal statistics HTTP server. | -| `MTG_STATSD_IP` | `--statsd-ip` | | IP/host addresses of statsd service. No defaults, by defaults we do not send anything there. | -| `MTG_STATSD_PORT` | `--statsd-port` | `8125` | Which port should we use to work with statsd. | -| `MTG_STATSD_NETWORK` | `--statsd-network` | `udp` | Which protocol should we use to work with statsd. Possible options are `udp` and `tcp`. | -| `MTG_STATSD_PREFIX` | `--statsd-prefix` | `mtg` | Which bucket prefix we should use. For example, if you set `mtg`, then metric `traffic.ingress` would be send as `mtg.traffic.ingress`. | -| `MTG_STATSD_TAGS_FORMAT` | `--statsd-tags-format` | | Which tags format we should use. By default, we are using default vanilla statsd tags format but if you want to send directly to InfluxDB or Datadog, please specify it there. Possible options are `influxdb` and `datadog`. | -| `MTG_STATSD_TAGS` | `--statsd-tags` | | Which tags should we send to statsd with our metrics. Please specify them as `key=value` pairs. | -| `MTG_PROMETHEUS_PREFIX` | `--prometheus-prefix` | `mtg` | Which namespace should be used for prometheus metrics. | -| `MTG_BUFFER_WRITE` | `-w`, `--write-buffer` | `65536` | The size of TCP write buffer in bytes. Write buffer is the buffer for messages which are going from client to Telegram. | -| `MTG_BUFFER_READ` | `-r`, `--read-buffer` | `131072` | The size of TCP read buffer in bytes. Read buffer is the buffer for messages from Telegram to client. | -| `MTG_SECURE_ONLY` | `-s`, `--secure-only` | `false` | Support only clients with secure mode (i.e only clients with dd-secrets). | -| `MTG_ANTIREPLAY_MAXSIZE` | `anti-replay-max-size` | `128` | Max size of antireplay cache in megabytes. | -| `MTG_ANTIREPLAY_EVICTIONTIME` | `anti-replay-eviction-time` | `168h` | Eviction time for antireplay cache entries. | +| Environment variable | Corresponding flags | Default value | Description | +|-------------------------------|------------------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `MTG_DEBUG` | `-d`, `--debug` | `false` | Run in debug mode. Usually, you need to run in this mode only if you develop this tool or its maintainer is asking you to provide logs with such verbosity. | +| `MTG_VERBOSE` | `-v`, `--verbose` | `false` | Run in verbose mode. This is way less chatty than debug mode. | +| `MTG_BIND` | `-b`, `--bind` | `0.0.0.0:3128` | Which host/port pair should we bind to (listen on). | +| `MTG_IPV4` | `-4`, `--public-ipv4` | [Autodetect](https://ifconfig.co) | IPv4 address:port of this proxy. This is required if you NAT your proxy or run it in a docker container. In that case, you absolutely need to specify public IPv4 address of the proxy, otherwise either URLs will be broken or proxy could not access Telegram middle proxies. | +| `MTG_IPV6` | `-6`, `--public-ipv6` | [Autodetect](https://ifconfig.co) | IPv6 address:port of this proxy. This is required if you NAT your proxy or run it in a docker container. In that case, you absolutely need to specify public IPv6 address of the proxy, otherwise either URLs will be broken or proxy could not access Telegram middle proxies. | +| `MTG_STATS_BIND` | `-t`, `--stats-bind` | `127.0.0.1:3129` | Which hist:port should we bind the internal statistics HTTP server (Prometheus). | +| `MTG_STATS_NAMESPACE` | `--stats-namespace` | `mtg` | Which namespace should be used for prometheus metrics. | +| `MTG_STATSD_ADDR` | `--statsd-addr` | | IP:host addresses of statsd service. No defaults, by defaults we do not send anything there. | +| `MTG_STATSD_PORT` | `--statsd-port` | `8125` | Which port should we use to work with statsd. | +| `MTG_STATSD_NETWORK` | `--statsd-network` | `udp` | Which protocol should we use to work with statsd. Possible options are `udp` and `tcp`. | +| `MTG_STATSD_PREFIX` | `--statsd-prefix` | `mtg` | Which bucket prefix we should use. For example, if you set `mtg`, then metric `traffic.ingress` would be send as `mtg.traffic.ingress`. | +| `MTG_STATSD_TAGS_FORMAT` | `--statsd-tags-format` | | Which tags format we should use. By default, we are using default vanilla statsd tags format but if you want to send directly to InfluxDB or Datadog, please specify it there. Possible options are `influxdb` and `datadog`. | +| `MTG_STATSD_TAGS` | `--statsd-tags` | | Which tags should we send to statsd with our metrics. Please specify them as `key=value` pairs. | +| `MTG_BUFFER_WRITE` | `-w`, `--write-buffer` | `65536` | The size of TCP write buffer in bytes. Write buffer is the buffer for messages which are going from client to Telegram. | +| `MTG_BUFFER_READ` | `-r`, `--read-buffer` | `131072` | The size of TCP read buffer in bytes. Read buffer is the buffer for messages from Telegram to client. | +| `MTG_ANTIREPLAY_MAXSIZE` | `--anti-replay-max-size` | `128MB` | Max size of antireplay cache. | +| `MTG_CLOAK_PORT` | `--cloak-port` | `443` | Which port we should use to connect to cloaked host in FakeTLS mode. | +| `MTG_MULTIPLEX_PERCONNECTION` | `--multiplex-per-connection` | `50` | How many client connections can share a single Telegram connection in adtag mode | Usually you want to modify only read/write buffer sizes. If you feel that proxy is slow, try to increase both sizes giving more priority to @@ -237,35 +204,17 @@ userspace. Now run the tool: ```console -mtg +$ mtg run ``` How to run the tool with ADTag: ```console -mtg +$ mtg run ``` This tool will listen on port 3128 by default with the given secret. -# One-line runner - -```console -docker run --name mtg --restart=unless-stopped -p 3128:3128 -p 127.0.0.1:3129:3129 -d nineseconds/mtg:stable $(openssl rand -hex 16) -``` - -or in secret mode: - -```console -docker run --name mtg --restart=unless-stopped -p 3128:3128 -p 127.0.0.1:3129:3129 -d nineseconds/mtg:stable dd$(openssl rand -hex 16) -``` - -You will have this tool up and running on port 3128. Now curl -`localhost:3129` to get `tg://` links or do `docker logs mtg`. Also, -port 3129 will show you some statistics if you are interested in. - -Also, you can use [run-mtg.sh](https://github.com/9seconds/mtg/blob/master/run-mtg.sh) script - # statsd integration @@ -278,19 +227,20 @@ and [Datadog](https://docs.datadoghq.com/developers/dogstatsd/). All metrics are gauges. Here is the list of metrics and their meaning: -| Metric name | Unit | Description | -|---------------------------------|---------|-----------------------------------------------------------| -| `connections.abridged.ipv4` | number | The number of active abridged IPv4 connections | -| `connections.abridged.ipv6` | number | The number of active abridged IPv6 connections | -| `connections.intermediate.ipv4` | number | The number of active intermediate IPv4 connections | -| `connections.intermediate.ipv6` | number | The number of active intermediate IPv6 connections | -| `connections.secure.ipv4` | number | The number of active secure intermediate IPv4 connections | -| `connections.secure.ipv6` | number | The number of active secure intermediate IPv6 connections | -| `crashes` | number | An amount of crashes in client handlers | -| `traffic.ingress` | bytes | Ingress traffic from the start of application (incoming) | -| `traffic.egress` | bytes | Egress traffic from the start of application (outgoing) | -| `speed.ingress` | bytes/s | Ingress bandwidth of the latest second (incoming traffic) | -| `speed.egress` | bytes/s | Egress bandwidth of the latest second (outgoing traffic) | +| Metric name | Unit | Description | +|----------------------------------|---------|-----------------------------------------------------------| +| `connections.abridged.ipv4` | number | The number of active abridged IPv4 connections | +| `connections.abridged.ipv6` | number | The number of active abridged IPv6 connections | +| `connections.intermediate.ipv4` | number | The number of active intermediate IPv4 connections | +| `connections.intermediate.ipv6` | number | The number of active intermediate IPv6 connections | +| `connections.secure.ipv4` | number | The number of active secure intermediate IPv4 connections | +| `connections.secure.ipv6` | number | The number of active secure intermediate IPv6 connections | +| `telegram_connections.[dc].ipv4` | number | The number of active abridged IPv4 connections | +| `telegram_connections.[dc].ipv6` | number | The number of active abridged IPv6 connections | +| `crashes` | number | An amount of crashes in client handlers | +| `traffic.ingress` | bytes | Ingress traffic from the start of application (incoming) | +| `traffic.egress` | bytes | Egress traffic from the start of application (outgoing) | +| `replay_attacks` | number | The number of prevented replay attacks. | All metrics are prefixed with given prefix. Default prefix is `mtg`. With such prefix metric name `traffic.ingress`, for example, would be @@ -300,9 +250,22 @@ With such prefix metric name `traffic.ingress`, for example, would be # Prometheus integration [Prometheus](https://prometheus.io) integration comes out of -the box, you do not need to setup anything special. Prometheus -scrape endpoint lives on the same IP/port where generic stats -service (`http://${MTG_STATS_IP}:${MTG_STATS_PORT}`) but on -`/prometheus` path. So, if you access http stats service as `curl -http://localhost:3129/`, then your prometheus endpoint is `curl -http://localhost:3129/prometheus/`. +the box, you do not need to setup anything special. + + +# Upgrade to 1.0 + +Version 1.0 breaks compatibility with previous versions so please read +this chapter carefully: + +1. mtg now uses subcommands. Please use `mtg run` instead of just + `mtg` to run a proxy. +2. Options which set host and port separately were removed in a + favor of fused `host:port` options. +3. Own stats server was removed. Prometheus endpoint is moved to + default stats endpoint. +4. It is possible to connect to this proxy only with a secret which + was used to run it. So, no backward compatibility of clients. +5. Multiplexing involves connectivity with middle proxies and involves + the most complex code path of this proxy. To avoid potential bugs, + we still recommend using direct mode. diff --git a/antireplay/cache.go b/antireplay/cache.go index 4923d52..9eb08ab 100644 --- a/antireplay/cache.go +++ b/antireplay/cache.go @@ -1,37 +1,36 @@ package antireplay -import ( - "github.com/allegro/bigcache" - "github.com/juju/errors" +import "github.com/VictoriaMetrics/fastcache" - "github.com/9seconds/mtg/config" +var ( + prefixObfuscated2 = []byte{0x00} + prefixTLS = []byte{0x01} ) -// Cache defines storage for obfuscated2 handshake frames. -type Cache struct { - cache *bigcache.BigCache +type cache struct { + data *fastcache.Cache } -func (a Cache) Add(frame []byte) { - a.cache.Set(string(frame), nil) // nolint: errcheck +func (c *cache) AddObfuscated2(data []byte) { + c.data.Set(keyObfuscated2(data), nil) } -func (a Cache) Has(frame []byte) bool { - _, err := a.cache.Get(string(frame)) - - return err == nil +func (c *cache) AddTLS(data []byte) { + c.data.Set(keyTLS(data), nil) } -func NewCache(config *config.Config) (Cache, error) { - cache, err := bigcache.NewBigCache(bigcache.Config{ - Shards: 1024, - LifeWindow: config.AntiReplayEvictionTime, - Hasher: hasher{}, - HardMaxCacheSize: config.AntiReplayMaxSize, - }) - if err != nil { - return Cache{}, errors.Annotate(err, "Cannot make cache") - } +func (c *cache) HasObfuscated2(data []byte) bool { + return c.data.Has(keyObfuscated2(data)) +} - return Cache{cache}, nil +func (c *cache) HasTLS(data []byte) bool { + return c.data.Has(keyTLS(data)) +} + +func keyObfuscated2(data []byte) []byte { + return append(prefixObfuscated2, data...) +} + +func keyTLS(data []byte) []byte { + return append(prefixTLS, data...) } diff --git a/antireplay/hasher.go b/antireplay/hasher.go deleted file mode 100644 index acaf9b6..0000000 --- a/antireplay/hasher.go +++ /dev/null @@ -1,9 +0,0 @@ -package antireplay - -import "github.com/cespare/xxhash" - -type hasher struct{} - -func (h hasher) Sum64(value string) uint64 { - return xxhash.Sum64String(value) -} diff --git a/antireplay/init.go b/antireplay/init.go new file mode 100644 index 0000000..ccde75b --- /dev/null +++ b/antireplay/init.go @@ -0,0 +1,20 @@ +package antireplay + +import ( + "sync" + + "github.com/VictoriaMetrics/fastcache" + + "mtg/config" +) + +var ( + Cache cache + initOnce sync.Once +) + +func Init() { + initOnce.Do(func() { + Cache.data = fastcache.New(config.C.AntiReplayMaxSize) + }) +} diff --git a/cli/generate.go b/cli/generate.go new file mode 100644 index 0000000..2d92321 --- /dev/null +++ b/cli/generate.go @@ -0,0 +1,26 @@ +package cli + +import ( + "crypto/rand" + "encoding/hex" + + "mtg/config" +) + +func Generate(secretType, hostname string) { + data := make([]byte, config.SimpleSecretLength) + if _, err := rand.Read(data); err != nil { + panic(err) + } + + secret := hex.EncodeToString(data) + + switch secretType { + case "simple": + PrintStdout(secret) + case "secured": + PrintStdout("dd" + secret) + default: + PrintStdout("ee" + secret + hex.EncodeToString([]byte(hostname))) + } +} diff --git a/cli/proxy.go b/cli/proxy.go new file mode 100644 index 0000000..1cda1a3 --- /dev/null +++ b/cli/proxy.go @@ -0,0 +1,103 @@ +package cli + +import ( + "net" + "os" + "time" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "mtg/antireplay" + "mtg/config" + "mtg/faketls" + "mtg/hub" + "mtg/ntp" + "mtg/obfuscated2" + "mtg/proxy" + "mtg/stats" + "mtg/telegram" + "mtg/utils" +) + +func Proxy() error { // nolint: funlen + ctx := utils.GetSignalContext() + + atom := zap.NewAtomicLevel() + + switch { + case config.C.Debug: + atom.SetLevel(zapcore.DebugLevel) + case config.C.Verbose: + atom.SetLevel(zapcore.InfoLevel) + default: + atom.SetLevel(zapcore.ErrorLevel) + } + + encoderCfg := zap.NewProductionEncoderConfig() + logger := zap.New(zapcore.NewCore( + zapcore.NewJSONEncoder(encoderCfg), + zapcore.Lock(os.Stderr), + atom, + )) + + zap.ReplaceGlobals(logger) + defer logger.Sync() // nolint: errcheck + + if err := config.InitPublicAddress(ctx); err != nil { + Fatal(err) + } + + zap.S().Debugw("Configuration", "config", config.Printable()) + + if len(config.C.AdTag) > 0 { + zap.S().Infow("Use middle proxy connection to Telegram") + + diff, err := ntp.Fetch() + if err != nil { + Fatal("Cannot fetch time data from NTP") + } + + if diff > time.Second { + Fatal("Your local time is skewed and drift is bigger than a second. Please sync your time.") + } + + go ntp.AutoUpdate() + } else { + zap.S().Infow("Use direct connection to Telegram") + } + + PrintJSONStdout(config.GetURLs()) + + if err := stats.Init(ctx); err != nil { + Fatal(err) + } + + antireplay.Init() + telegram.Init() + hub.Init(ctx) + faketls.Init(ctx) + + proxyListener, err := net.Listen("tcp", config.C.Bind.String()) + if err != nil { + Fatal(err) + } + + go func() { + <-ctx.Done() + proxyListener.Close() + }() + + app := &proxy.Proxy{ + Logger: zap.S().Named("proxy"), + Context: ctx, + ClientProtocolMaker: obfuscated2.MakeClientProtocol, + } + if config.C.SecretMode == config.SecretModeTLS { + app.ClientProtocolMaker = faketls.MakeClientProtocol + } + + app.Serve(proxyListener) + + return nil +} diff --git a/cli/utils.go b/cli/utils.go new file mode 100644 index 0000000..f26c1b6 --- /dev/null +++ b/cli/utils.go @@ -0,0 +1,43 @@ +package cli + +import ( + "encoding/json" + "fmt" + "io" + "os" +) + +func Fatal(arg interface{}) { + if value, ok := arg.(error); ok { + arg = fmt.Errorf("fatal error: %+v", value) + } + + PrintStderr(arg) + os.Exit(1) +} + +func PrintStderr(args ...interface{}) { + fmt.Fprintln(os.Stderr, args...) +} + +func PrintStdout(args ...interface{}) { + fmt.Println(args...) +} + +func PrintJSONStderr(data interface{}) { + printJSON(os.Stderr, data) +} + +func PrintJSONStdout(data interface{}) { + printJSON(os.Stdout, data) +} + +func printJSON(writer io.Writer, data interface{}) { + encoder := json.NewEncoder(writer) + encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") + + if err := encoder.Encode(data); err != nil { + panic(err) + } +} diff --git a/client/client.go b/client/client.go deleted file mode 100644 index b8f1b93..0000000 --- a/client/client.go +++ /dev/null @@ -1,15 +0,0 @@ -package client - -import ( - "context" - "net" - - "github.com/9seconds/mtg/antireplay" - "github.com/9seconds/mtg/config" - "github.com/9seconds/mtg/mtproto" - "github.com/9seconds/mtg/wrappers" -) - -// Init defines common method for initializing client connections. -type Init func(context.Context, context.CancelFunc, net.Conn, string, - antireplay.Cache, *config.Config) (wrappers.Wrap, *mtproto.ConnectionOpts, error) diff --git a/client/direct.go b/client/direct.go deleted file mode 100644 index 5ee0a39..0000000 --- a/client/direct.go +++ /dev/null @@ -1,63 +0,0 @@ -package client - -import ( - "context" - "net" - "time" - - "github.com/juju/errors" - - "github.com/9seconds/mtg/antireplay" - "github.com/9seconds/mtg/config" - "github.com/9seconds/mtg/mtproto" - "github.com/9seconds/mtg/obfuscated2" - "github.com/9seconds/mtg/wrappers" -) - -const handshakeTimeout = 10 * time.Second - -// DirectInit initializes client connection for proxy which connects to -// Telegram directly. -func DirectInit(ctx context.Context, cancel context.CancelFunc, socket net.Conn, - connID string, antiReplayCache antireplay.Cache, - conf *config.Config) (wrappers.Wrap, *mtproto.ConnectionOpts, error) { - tcpSocket := socket.(*net.TCPConn) - if err := tcpSocket.SetNoDelay(false); err != nil { - return nil, nil, errors.Annotate(err, "Cannot disable NO_DELAY to client socket") - } - if err := tcpSocket.SetReadBuffer(conf.ReadBufferSize); err != nil { - return nil, nil, errors.Annotate(err, "Cannot set read buffer size of client socket") - } - if err := tcpSocket.SetWriteBuffer(conf.WriteBufferSize); err != nil { - return nil, nil, errors.Annotate(err, "Cannot set write buffer size of client socket") - } - - socket.SetReadDeadline(time.Now().Add(handshakeTimeout)) // nolint: errcheck, gosec - frame, err := obfuscated2.ExtractFrame(socket) - if err != nil { - return nil, nil, errors.Annotate(err, "Cannot extract frame") - } - socket.SetReadDeadline(time.Time{}) // nolint: errcheck, gosec - - conn := wrappers.NewConn(ctx, cancel, socket, connID, wrappers.ConnPurposeClient, conf.PublicIPv4, conf.PublicIPv6) - obfs2, connOpts, err := obfuscated2.ParseObfuscated2ClientFrame(conf.Secret, frame) - if err != nil { - return nil, nil, errors.Annotate(err, "Cannot parse obfuscated frame") - } - - var replayPart = []byte(frame) - - if antiReplayCache.Has(replayPart[4:60]) { - return nil, nil, errors.New("Replay attack is detected") - } - antiReplayCache.Add(replayPart[4:60]) - - connOpts.ConnectionProto = mtproto.ConnectionProtocolAny - connOpts.ClientAddr = conn.RemoteAddr() - - conn = wrappers.NewStreamCipher(conn, obfs2.Encryptor, obfs2.Decryptor) - - conn.Logger().Infow("Client connection initialized") - - return conn, connOpts, nil -} diff --git a/client/middle.go b/client/middle.go deleted file mode 100644 index ecacd8e..0000000 --- a/client/middle.go +++ /dev/null @@ -1,42 +0,0 @@ -package client - -import ( - "context" - "net" - - "github.com/9seconds/mtg/antireplay" - "github.com/9seconds/mtg/config" - "github.com/9seconds/mtg/mtproto" - "github.com/9seconds/mtg/wrappers" -) - -// MiddleInit initializes client connection for proxy which has to -// support promoted channels, connect to Telegram middle proxies etc. -func MiddleInit(ctx context.Context, cancel context.CancelFunc, socket net.Conn, - connID string, antiReplayCache antireplay.Cache, - conf *config.Config) (wrappers.Wrap, *mtproto.ConnectionOpts, error) { - conn, opts, err := DirectInit(ctx, cancel, socket, connID, antiReplayCache, conf) - if err != nil { - return nil, nil, err - } - connStream := conn.(wrappers.StreamReadWriteCloser) - - var newConn wrappers.PacketReadWriteCloser - switch opts.ConnectionType { - case mtproto.ConnectionTypeAbridged: - newConn = wrappers.NewMTProtoAbridged(connStream, opts) - case mtproto.ConnectionTypeIntermediate: - newConn = wrappers.NewMTProtoIntermediate(connStream, opts) - case mtproto.ConnectionTypeSecure: - newConn = wrappers.NewMTProtoIntermediateSecure(connStream, opts) - default: - panic("Unknown connection type") - } - - opts.ConnectionProto = mtproto.ConnectionProtocolIPv4 - if socket.LocalAddr().(*net.TCPAddr).IP.To4() == nil { - opts.ConnectionProto = mtproto.ConnectionProtocolIPv6 - } - - return newConn, opts, err -} diff --git a/config/config.go b/config/config.go index dc43864..7db0e6e 100644 --- a/config/config.go +++ b/config/config.go @@ -2,223 +2,249 @@ package config import ( "bytes" - "encoding/hex" + "context" + "encoding/json" + "errors" "fmt" "net" - "strconv" - "time" - "github.com/juju/errors" + "github.com/alecthomas/units" + "go.uber.org/zap" statsd "gopkg.in/alexcesaro/statsd.v2" ) -// Config represents common configuration of mtg. +type SecretMode uint8 + +func (s SecretMode) String() string { + switch s { + case SecretModeSimple: + return "simple" + case SecretModeSecured: + return "secured" + } + + return "tls" +} + +const ( + SecretModeSimple SecretMode = iota + SecretModeSecured + SecretModeTLS +) + +const SimpleSecretLength = 16 + +type OptionType uint8 + +const ( + OptionTypeDebug OptionType = iota + OptionTypeVerbose + + OptionTypeBind + OptionTypePublicIPv4 + OptionTypePublicIPv6 + + OptionTypeStatsBind + OptionTypeStatsNamespace + OptionTypeStatsdAddress + OptionTypeStatsdNetwork + OptionTypeStatsdTagsFormat + OptionTypeStatsdTags + + OptionTypeWriteBufferSize + OptionTypeReadBufferSize + + OptionTypeCloakPort + + OptionTypeAntiReplayMaxSize + + OptionTypeMultiplexPerConnection + + OptionTypeSecret + OptionTypeAdtag +) + type Config struct { - Debug bool - Verbose bool - SecureMode bool - SecureOnly bool + Bind *net.TCPAddr `json:"bind"` + PublicIPv4 *net.TCPAddr `json:"public_ipv4"` + PublicIPv6 *net.TCPAddr `json:"public_ipv6"` + StatsBind *net.TCPAddr `json:"stats_bind"` + StatsdAddr *net.TCPAddr `json:"stats_addr"` - ReadBufferSize int - WriteBufferSize int + StatsNamespace string `json:"stats_namespace"` + StatsdNetwork string `json:"statsd_network"` + CloakHost string `json:"cloak_host"` + StatsdTags map[string]string `json:"statsd_tags"` - BindPort uint16 - PublicIPv4Port uint16 - PublicIPv6Port uint16 - StatsPort uint16 + WriteBuffer int `json:"write_buffer"` + ReadBuffer int `json:"read_buffer"` + CloakPort int `json:"cloak_port"` - BindIP net.IP - PublicIPv4 net.IP - PublicIPv6 net.IP - StatsIP net.IP + AntiReplayMaxSize int `json:"anti_replay_max_size"` - AntiReplayMaxSize int - AntiReplayEvictionTime time.Duration + MultiplexPerConnection int `json:"multiplex_per_connection"` - StatsD struct { - Addr net.Addr - Prefix string - Tags map[string]string - TagsFormat statsd.TagFormat - Enabled bool - } - Prometheus struct { - Prefix string - } + Debug bool `json:"debug"` + Verbose bool `json:"verbose"` + StatsdTagsFormat statsd.TagFormat `json:"statsd_tags_format"` + SecretMode SecretMode `json:"secret_mode"` - Secret []byte - AdTag []byte + Secret []byte `json:"secret"` + AdTag []byte `json:"adtag"` } -// URLs contains links to the proxy (tg://, t.me) and their QR codes. -type URLs struct { - TG string `json:"tg_url"` - TMe string `json:"tme_url"` - TGQRCode string `json:"tg_qrcode"` - TMeQRCode string `json:"tme_qrcode"` +type Opt struct { + Option OptionType + Value interface{} } -// IPURLs contains links to both ipv4 and ipv6 of the proxy. -type IPURLs struct { - IPv4 URLs `json:"ipv4"` - IPv6 URLs `json:"ipv6"` - BotSecret string `json:"secret_for_mtproxybot"` -} +var C = Config{} -// BindAddr returns connection for this server to bind to. -func (c *Config) BindAddr() string { - return getAddr(c.BindIP, c.BindPort) -} - -// StatAddr returns connection string to the stats API. -func (c *Config) StatAddr() string { - return getAddr(c.StatsIP, c.StatsPort) -} - -// UseMiddleProxy defines if this proxy has to connect middle proxies -// which supports promoted channels or directly access Telegram. -func (c *Config) UseMiddleProxy() bool { - return len(c.AdTag) > 0 -} - -// BotSecretString returns secret string which should work with MTProxybot. -func (c *Config) BotSecretString() string { - return hex.EncodeToString(c.Secret) -} - -// SecretString returns a secret in a form entered on the start of the -// application. -func (c *Config) SecretString() string { - secret := c.BotSecretString() - if c.SecureMode { - return "dd" + secret - } - return secret -} - -// GetURLs returns configured IPURLs instance with links to this server. -func (c *Config) GetURLs() IPURLs { - urls := IPURLs{} - secret := c.SecretString() - if c.PublicIPv4 != nil { - urls.IPv4 = getURLs(c.PublicIPv4, c.PublicIPv4Port, secret) - } - if c.PublicIPv6 != nil { - urls.IPv6 = getURLs(c.PublicIPv6, c.PublicIPv6Port, secret) - } - urls.BotSecret = c.BotSecretString() - - return urls -} - -func getAddr(host fmt.Stringer, port uint16) string { - return net.JoinHostPort(host.String(), strconv.Itoa(int(port))) -} - -// NewConfig returns new configuration. If required, it manages and -// fetches data from external sources. Parameters passed to this -// function, should come from command line arguments. -func NewConfig(debug, verbose bool, // nolint: gocyclo - writeBufferSize, readBufferSize uint32, - bindIP, publicIPv4, publicIPv6, statsIP net.IP, - bindPort, publicIPv4Port, publicIPv6Port, statsPort, statsdPort uint16, - statsdIP, statsdNetwork, statsdPrefix, statsdTagsFormat string, - statsdTags map[string]string, prometheusPrefix string, - secureOnly bool, - antiReplayMaxSize int, antiReplayEvictionTime time.Duration, - secret, adtag []byte) (*Config, error) { - secureMode := secureOnly - if bytes.HasPrefix(secret, []byte{0xdd}) && len(secret) == 17 { - secureMode = true - secret = bytes.TrimPrefix(secret, []byte{0xdd}) - } else if len(secret) != 16 { - return nil, errors.New("Telegram demands secret of length 32") - } - - var err error - if publicIPv4 == nil { - publicIPv4, err = getGlobalIPv4() - if err != nil { - publicIPv4 = nil - } else if publicIPv4.To4() == nil { - return nil, errors.Errorf("IP %s is not IPv4", publicIPv4.String()) - } - } - if publicIPv4Port == 0 { - publicIPv4Port = bindPort - } - - if publicIPv6 == nil { - publicIPv6, err = getGlobalIPv6() - if err != nil { - publicIPv6 = nil - } else if publicIPv6.To4() != nil { - return nil, errors.Errorf("IP %s is not IPv6", publicIPv6.String()) - } - } - if publicIPv6Port == 0 { - publicIPv6Port = bindPort - } - - if statsIP == nil { - statsIP = publicIPv4 - } - - conf := &Config{ - Debug: debug, - Verbose: verbose, - SecureOnly: secureOnly, - BindIP: bindIP, - BindPort: bindPort, - PublicIPv4: publicIPv4, - PublicIPv4Port: publicIPv4Port, - PublicIPv6: publicIPv6, - PublicIPv6Port: publicIPv6Port, - StatsIP: statsIP, - StatsPort: statsPort, - Secret: secret, - AdTag: adtag, - SecureMode: secureMode, - ReadBufferSize: int(readBufferSize), - WriteBufferSize: int(writeBufferSize), - AntiReplayMaxSize: antiReplayMaxSize, - AntiReplayEvictionTime: antiReplayEvictionTime, - } - conf.Prometheus.Prefix = prometheusPrefix - - if statsdIP != "" { - conf.StatsD.Enabled = true - conf.StatsD.Prefix = statsdPrefix - conf.StatsD.Tags = statsdTags - - var ( - addr net.Addr - err error - ) - hostPort := net.JoinHostPort(statsdIP, strconv.Itoa(int(statsdPort))) - switch statsdNetwork { - case "tcp": - addr, err = net.ResolveTCPAddr("tcp", hostPort) - case "udp": - addr, err = net.ResolveUDPAddr("udp", hostPort) +func Init(options ...Opt) error { // nolint: gocyclo, funlen + for _, opt := range options { + switch opt.Option { + case OptionTypeDebug: + C.Debug = opt.Value.(bool) + case OptionTypeVerbose: + C.Verbose = opt.Value.(bool) + case OptionTypeBind: + C.Bind = opt.Value.(*net.TCPAddr) + case OptionTypePublicIPv4: + C.PublicIPv4 = opt.Value.(*net.TCPAddr) + if C.PublicIPv4 == nil { + C.PublicIPv4 = &net.TCPAddr{} + } + case OptionTypePublicIPv6: + C.PublicIPv6 = opt.Value.(*net.TCPAddr) + if C.PublicIPv6 == nil { + C.PublicIPv6 = &net.TCPAddr{} + } + case OptionTypeStatsBind: + C.StatsBind = opt.Value.(*net.TCPAddr) + case OptionTypeStatsNamespace: + C.StatsNamespace = opt.Value.(string) + case OptionTypeStatsdAddress: + C.StatsdAddr = opt.Value.(*net.TCPAddr) + case OptionTypeStatsdNetwork: + value := opt.Value.(string) + switch value { + case "udp", "tcp": + C.StatsdNetwork = value + default: + return fmt.Errorf("unknown statsd network %v", value) + } + case OptionTypeStatsdTagsFormat: + value := opt.Value.(string) + switch value { + case "datadog": + C.StatsdTagsFormat = statsd.Datadog + case "influxdb": + C.StatsdTagsFormat = statsd.InfluxDB + default: + return fmt.Errorf("incorrect statsd tag %s", value) + } + case OptionTypeStatsdTags: + C.StatsdTags = opt.Value.(map[string]string) + case OptionTypeWriteBufferSize: + C.WriteBuffer = int(opt.Value.(units.Base2Bytes)) + case OptionTypeReadBufferSize: + C.ReadBuffer = int(opt.Value.(units.Base2Bytes)) + case OptionTypeCloakPort: + C.CloakPort = int(opt.Value.(uint16)) + case OptionTypeAntiReplayMaxSize: + C.AntiReplayMaxSize = int(opt.Value.(units.Base2Bytes)) + case OptionTypeMultiplexPerConnection: + C.MultiplexPerConnection = int(opt.Value.(uint)) + case OptionTypeSecret: + C.Secret = opt.Value.([]byte) + case OptionTypeAdtag: + C.AdTag = opt.Value.([]byte) default: - err = errors.Errorf("Unknown network %s", statsdNetwork) - } - if err != nil { - return nil, errors.Annotate(err, "Cannot resolve statsd address") - } - conf.StatsD.Addr = addr - - switch statsdTagsFormat { - case "datadog": - conf.StatsD.TagsFormat = statsd.Datadog - case "influxdb": - conf.StatsD.TagsFormat = statsd.InfluxDB - case "": - default: - return nil, errors.Errorf("Unknown tags format %s", statsdTagsFormat) + return fmt.Errorf("unknown tag %v", opt.Option) } } - return conf, nil + switch { + case len(C.Secret) == 1+SimpleSecretLength && bytes.HasPrefix(C.Secret, []byte{0xdd}): + C.SecretMode = SecretModeSecured + C.Secret = bytes.TrimPrefix(C.Secret, []byte{0xdd}) + case len(C.Secret) > SimpleSecretLength && bytes.HasPrefix(C.Secret, []byte{0xee}): + C.SecretMode = SecretModeTLS + secret := bytes.TrimPrefix(C.Secret, []byte{0xee}) + C.Secret = secret[:SimpleSecretLength] + C.CloakHost = string(secret[SimpleSecretLength:]) + case len(C.Secret) == SimpleSecretLength: + C.SecretMode = SecretModeSimple + default: + return errors.New("incorrect secret") + } + + if C.MultiplexPerConnection == 0 { + return errors.New("cannot use 0 clients per connection for multiplexing") + } + + if C.CloakHost != "" { + addrs, err := net.LookupHost(C.CloakHost) + if err != nil { + return fmt.Errorf("cannot resolve address of %s host: %w", C.CloakHost, err) + } + + if len(addrs) == 0 { + return fmt.Errorf("no known ip addresses for the host %s", C.CloakHost) + } + } + + return nil +} + +func InitPublicAddress(ctx context.Context) error { + if C.PublicIPv4.Port == 0 { + C.PublicIPv4.Port = C.Bind.Port + } + + if C.PublicIPv6.Port == 0 { + C.PublicIPv6.Port = C.Bind.Port + } + + foundAddress := C.PublicIPv4.IP != nil || C.PublicIPv6.IP != nil + + if C.PublicIPv4.IP == nil { + ip, err := getGlobalIPv4(ctx) + if err != nil { + zap.S().Warnw("Cannot resolve public address", "error", err) + } else { + C.PublicIPv4.IP = ip + foundAddress = true + } + } + + if C.PublicIPv6.IP == nil { + ip, err := getGlobalIPv6(ctx) + if err != nil { + zap.S().Warnw("Cannot resolve public address", "error", err) + } else { + C.PublicIPv6.IP = ip + foundAddress = true + } + } + + if !foundAddress { + return errors.New("cannot resolve any public address") + } + + return nil +} + +func Printable() interface{} { + data, err := json.Marshal(C) + if err != nil { + panic(err) + } + + rv := map[string]interface{}{} + if err := json.Unmarshal(data, &rv); err != nil { + panic(err) + } + + return rv } diff --git a/config/global_ips.go b/config/global_ips.go index 9a144a6..e27b969 100644 --- a/config/global_ips.go +++ b/config/global_ips.go @@ -2,28 +2,43 @@ package config import ( "context" + "fmt" + "io" "io/ioutil" "net" "net/http" "strings" - - "github.com/juju/errors" + "time" ) -const ifconfigAddress = "https://ifconfig.co/ip" +const ( + ifconfigAddress = "https://ifconfig.co/ip" + ifconfigTimeout = 10 * time.Second +) -func getGlobalIPv4() (net.IP, error) { - return fetchIP("tcp4") +func getGlobalIPv4(ctx context.Context) (net.IP, error) { + ip, err := fetchIP(ctx, "tcp4") + if err != nil || ip.To4() == nil { + return nil, fmt.Errorf("cannot find public ipv4 address: %w", err) + } + + return ip, nil } -func getGlobalIPv6() (net.IP, error) { - return fetchIP("tcp6") +func getGlobalIPv6(ctx context.Context) (net.IP, error) { + ip, err := fetchIP(ctx, "tcp6") + if err != nil || ip.To4() != nil { + return nil, fmt.Errorf("cannot find public ipv6 address: %w", err) + } + + return ip, nil } -func fetchIP(network string) (net.IP, error) { +func fetchIP(ctx context.Context, network string) (net.IP, error) { dialer := &net.Dialer{FallbackDelay: -1} client := &http.Client{ - Jar: nil, + Jar: nil, + Timeout: ifconfigTimeout, Transport: &http.Transport{ DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) { return dialer.DialContext(ctx, network, addr) @@ -31,21 +46,32 @@ func fetchIP(network string) (net.IP, error) { }, } - resp, err := client.Get(ifconfigAddress) + req, err := http.NewRequest("GET", ifconfigAddress, nil) if err != nil { - return nil, err + return nil, fmt.Errorf("cannot create a request: %w", err) } + + resp, err := client.Do(req.WithContext(ctx)) + if err != nil { + if resp != nil { + io.Copy(ioutil.Discard, resp.Body) // nolint: errcheck + } + + return nil, fmt.Errorf("cannot perform a request: %w", err) + } + defer resp.Body.Close() // nolint: errcheck respDataBytes, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, err + return nil, fmt.Errorf("cannot read response body: %w", err) } + respData := strings.TrimSpace(string(respDataBytes)) ip := net.ParseIP(respData) if ip == nil { - return nil, errors.Errorf("ifconfig.co returns incorrect IP %s", respData) + return nil, fmt.Errorf("ifconfig.co returns incorrect IP %s", respData) } return ip, nil diff --git a/config/urls.go b/config/urls.go index 5f4e6d8..1e90b1d 100644 --- a/config/urls.go +++ b/config/urls.go @@ -1,15 +1,48 @@ package config import ( + "encoding/hex" "net" "net/url" "strconv" ) -func getURLs(addr net.IP, port uint16, secret string) (urls URLs) { +type URLs struct { + TG string `json:"tg_url"` + TMe string `json:"tme_url"` + TGQRCode string `json:"tg_qrcode"` + TMeQRCode string `json:"tme_qrcode"` +} + +type IPURLs struct { + IPv4 URLs `json:"ipv4"` + IPv6 URLs `json:"ipv6"` + BotSecret string `json:"secret_for_mtproxybot"` +} + +func GetURLs() (urls IPURLs) { + secret := "" + + switch C.SecretMode { + case SecretModeSimple: + secret = hex.EncodeToString(C.Secret) + case SecretModeSecured: + secret = "dd" + hex.EncodeToString(C.Secret) + case SecretModeTLS: + secret = "ee" + hex.EncodeToString(C.Secret) + hex.EncodeToString([]byte(C.CloakHost)) + } + + urls.IPv4 = makeURLs(C.PublicIPv4, secret) + urls.IPv6 = makeURLs(C.PublicIPv6, secret) + urls.BotSecret = hex.EncodeToString(C.Secret) + + return urls +} + +func makeURLs(addr *net.TCPAddr, secret string) (urls URLs) { values := url.Values{} - values.Set("server", addr.String()) - values.Set("port", strconv.Itoa(int(port))) + values.Set("server", addr.IP.String()) + values.Set("port", strconv.Itoa(addr.Port)) values.Set("secret", secret) urls.TG = makeTGURL(values) diff --git a/conntypes/acks.go b/conntypes/acks.go new file mode 100644 index 0000000..d3dbd8d --- /dev/null +++ b/conntypes/acks.go @@ -0,0 +1,6 @@ +package conntypes + +type ConnectionAcks struct { + Simple bool + Quick bool +} diff --git a/conntypes/dc.go b/conntypes/dc.go new file mode 100644 index 0000000..3cf2850 --- /dev/null +++ b/conntypes/dc.go @@ -0,0 +1,5 @@ +package conntypes + +type DC int16 + +const DCDefaultIdx DC = 1 diff --git a/conntypes/id.go b/conntypes/id.go new file mode 100644 index 0000000..1aa482c --- /dev/null +++ b/conntypes/id.go @@ -0,0 +1,24 @@ +package conntypes + +import ( + "crypto/rand" + "encoding/hex" +) + +const ConnIDLength = 8 + +type ConnID [ConnIDLength]byte + +func (c ConnID) String() string { + return hex.EncodeToString(c[:]) +} + +func NewConnID() ConnID { + var id ConnID + + if _, err := rand.Read(id[:]); err != nil { + panic(err) + } + + return id +} diff --git a/conntypes/packet.go b/conntypes/packet.go new file mode 100644 index 0000000..fa72022 --- /dev/null +++ b/conntypes/packet.go @@ -0,0 +1,3 @@ +package conntypes + +type Packet []byte diff --git a/conntypes/protocol.go b/conntypes/protocol.go new file mode 100644 index 0000000..15a1c69 --- /dev/null +++ b/conntypes/protocol.go @@ -0,0 +1,20 @@ +package conntypes + +type ConnectionProtocol uint8 + +func (c ConnectionProtocol) String() string { + switch c { + case ConnectionProtocolAny: + return "any" + case ConnectionProtocolIPv4: + return "ipv4" + } + + return "ipv6" +} + +const ( + ConnectionProtocolIPv4 ConnectionProtocol = 1 + ConnectionProtocolIPv6 = ConnectionProtocolIPv4 << 1 + ConnectionProtocolAny = ConnectionProtocolIPv4 | ConnectionProtocolIPv6 +) diff --git a/conntypes/type.go b/conntypes/type.go new file mode 100644 index 0000000..11634f2 --- /dev/null +++ b/conntypes/type.go @@ -0,0 +1,27 @@ +package conntypes + +type ConnectionType uint8 + +const ( + ConnectionTypeUnknown ConnectionType = iota + ConnectionTypeAbridged + ConnectionTypeIntermediate + ConnectionTypeSecure +) + +var ( + ConnectionTagAbridged = []byte{0xef, 0xef, 0xef, 0xef} + ConnectionTagIntermediate = []byte{0xee, 0xee, 0xee, 0xee} + ConnectionTagSecure = []byte{0xdd, 0xdd, 0xdd, 0xdd} +) + +func (t ConnectionType) Tag() []byte { + switch t { + case ConnectionTypeAbridged: + return ConnectionTagAbridged + case ConnectionTypeIntermediate: + return ConnectionTagIntermediate + default: + return ConnectionTagSecure + } +} diff --git a/conntypes/wrap_interfaces.go b/conntypes/wrap_interfaces.go new file mode 100644 index 0000000..f21eb91 --- /dev/null +++ b/conntypes/wrap_interfaces.go @@ -0,0 +1,14 @@ +package conntypes + +import ( + "net" + + "go.uber.org/zap" +) + +type Wrap interface { + Conn() net.Conn + Logger() *zap.SugaredLogger + LocalAddr() *net.TCPAddr + RemoteAddr() *net.TCPAddr +} diff --git a/conntypes/wrap_packet_ack_interfaces.go b/conntypes/wrap_packet_ack_interfaces.go new file mode 100644 index 0000000..6388f0c --- /dev/null +++ b/conntypes/wrap_packet_ack_interfaces.go @@ -0,0 +1,41 @@ +package conntypes + +import "io" + +type PacketAckReader interface { + Read(*ConnectionAcks) (Packet, error) +} + +type PacketAckWriter interface { + Write(Packet, *ConnectionAcks) error +} + +type PacketAckCloser interface { + io.Closer +} + +type PacketAckReadCloser interface { + PacketAckReader + PacketAckCloser +} + +type PacketAckWriteCloser interface { + PacketAckWriter + PacketAckCloser +} + +type PacketAckReadWriter interface { + PacketAckReader + PacketAckWriter +} + +type PacketAckReadWriteCloser interface { + PacketAckReader + PacketAckWriter + PacketAckCloser +} + +type PacketAckFullReadWriteCloser interface { + Wrap + PacketAckReadWriteCloser +} diff --git a/conntypes/wrap_packet_interfaces.go b/conntypes/wrap_packet_interfaces.go new file mode 100644 index 0000000..6c573d4 --- /dev/null +++ b/conntypes/wrap_packet_interfaces.go @@ -0,0 +1,51 @@ +package conntypes + +import "io" + +type BasePacketReader interface { + Read() (Packet, error) +} + +type BasePacketWriter interface { + Write(Packet) error +} + +type PacketReader interface { + Wrap + BasePacketReader +} + +type PacketWriter interface { + Wrap + BasePacketWriter +} + +type PacketCloser interface { + Wrap + io.Closer +} + +type PacketReadCloser interface { + Wrap + BasePacketReader + io.Closer +} + +type PacketWriteCloser interface { + Wrap + BasePacketWriter + io.Closer +} + +type PacketReadWriter interface { + Wrap + BasePacketWriter + BasePacketReader +} + +type PacketReadWriteCloser interface { + Wrap + BasePacketWriter + BasePacketReader + io.Closer +} diff --git a/conntypes/wrap_stream_interfaces.go b/conntypes/wrap_stream_interfaces.go new file mode 100644 index 0000000..5924d66 --- /dev/null +++ b/conntypes/wrap_stream_interfaces.go @@ -0,0 +1,56 @@ +package conntypes + +import ( + "io" + "time" +) + +type BaseStreamReaderWithTimeout interface { + ReadTimeout([]byte, time.Duration) (int, error) +} + +type BaseStreamWriterWithTimeout interface { + WriteTimeout([]byte, time.Duration) (int, error) +} + +type StreamReader interface { + Wrap + io.Reader + BaseStreamReaderWithTimeout +} + +type StreamWriter interface { + Wrap + io.Writer + BaseStreamWriterWithTimeout +} + +type StreamCloser interface { + Wrap + io.Closer +} + +type StreamReadCloser interface { + Wrap + io.ReadCloser + BaseStreamReaderWithTimeout +} + +type StreamWriteCloser interface { + Wrap + io.WriteCloser + BaseStreamWriterWithTimeout +} + +type StreamReadWriter interface { + Wrap + io.ReadWriter + BaseStreamReaderWithTimeout +} + +type StreamReadWriteCloser interface { + Wrap + io.ReadWriteCloser + BaseStreamReaderWithTimeout + BaseStreamWriterWithTimeout +} diff --git a/faketls/certificate_server.go b/faketls/certificate_server.go new file mode 100644 index 0000000..af0f100 --- /dev/null +++ b/faketls/certificate_server.go @@ -0,0 +1,91 @@ +package faketls + +import ( + "bytes" + "container/ring" + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "strconv" + "time" + + "go.uber.org/zap" + + "mtg/config" +) + +type connectionServer struct { + nextWriteItem *ring.Ring + nextReadItem *ring.Ring + + ctx context.Context + channelGet chan chan<- []byte +} + +func (c *connectionServer) get() ([]byte, error) { + resp := make(chan []byte) + select { + case <-c.ctx.Done(): + return nil, errors.New("context closed") + case c.channelGet <- resp: + return <-resp, nil + } +} + +func (c *connectionServer) fetch() ([]byte, error) { + addr := net.JoinHostPort(config.C.CloakHost, strconv.Itoa(config.C.CloakPort)) + conn, err := tls.Dial("tcp", addr, &tls.Config{InsecureSkipVerify: true}) // nolint: gosec + + if err != nil { + return nil, fmt.Errorf("cannot connect to the masked host: %w", err) + } + + defer conn.Close() + + if err = conn.Handshake(); err != nil { + return nil, fmt.Errorf("cannot perform tls handshake: %w", err) + } + + certificates := conn.ConnectionState().PeerCertificates + if len(certificates) == 0 { + return nil, errors.New("no certificates is found") + } + + var buf bytes.Buffer + + for _, v := range certificates { + buf.Write(v.Raw) + } + + return buf.Bytes(), nil +} + +func (c *connectionServer) run(tickEvery time.Duration) { + logger := zap.S().Named("tls-connection-server") + + ticker := time.NewTicker(tickEvery) + defer ticker.Stop() + + for { + select { + case <-c.ctx.Done(): + return + case resp := <-c.channelGet: + resp <- c.nextReadItem.Value.([]byte) + close(resp) + + c.nextReadItem = c.nextReadItem.Next() + case <-ticker.C: + cert, err := c.fetch() + switch err { + case nil: + c.nextWriteItem.Value = cert + c.nextWriteItem = c.nextWriteItem.Next() + default: + logger.Warnw("cannot fetch certificates", "error", err) + } + } + } +} diff --git a/faketls/client_protocol.go b/faketls/client_protocol.go new file mode 100644 index 0000000..718350b --- /dev/null +++ b/faketls/client_protocol.go @@ -0,0 +1,140 @@ +package faketls + +import ( + "bufio" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "strconv" + "sync" + "time" + + "mtg/antireplay" + "mtg/config" + "mtg/conntypes" + "mtg/obfuscated2" + "mtg/protocol" + "mtg/stats" + "mtg/tlstypes" + "mtg/wrappers/stream" +) + +type ClientProtocol struct { + obfuscated2.ClientProtocol +} + +func (c *ClientProtocol) Handshake(socket conntypes.StreamReadWriteCloser) (conntypes.StreamReadWriteCloser, error) { + rewinded := stream.NewRewind(socket) + bufferedReader := bufio.NewReader(rewinded) + + for _, expected := range faketlsStartBytes { + if actual, err := bufferedReader.ReadByte(); err != nil || actual != expected { + rewinded.Rewind() + c.cloakHost(rewinded) + + return nil, errors.New("failed first bytes of tls handshake") + } + } + + rewinded.Rewind() + rewinded = stream.NewRewind(rewinded) + + if err := c.tlsHandshake(rewinded); err != nil { + rewinded.Rewind() + c.cloakHost(rewinded) + + return nil, fmt.Errorf("failed tls handshake: %w", err) + } + + conn := stream.NewFakeTLS(socket) + conn, err := c.ClientProtocol.Handshake(conn) + + if err != nil { + return nil, err + } + + return conn, err +} + +func (c *ClientProtocol) tlsHandshake(conn io.ReadWriter) error { + helloRecord, err := tlstypes.ReadRecord(conn) + if err != nil { + return fmt.Errorf("cannot read initial record: %w", err) + } + + clientHello, err := tlstypes.ParseClientHello(helloRecord.Data.Bytes()) + if err != nil { + return fmt.Errorf("cannot parse client hello: %w", err) + } + + digest := clientHello.Digest() + for i := 0; i < len(digest)-4; i++ { + if digest[i] != 0 { + return errBadDigest + } + } + + timestamp := int64(binary.LittleEndian.Uint32(digest[len(digest)-4:])) + createdAt := time.Unix(timestamp, 0) + timeDiff := time.Since(createdAt) + + if (timeDiff > TimeSkew || timeDiff < -TimeSkew) && timestamp > TimeFromBoot { + return errBadTime + } + + if antireplay.Cache.HasTLS(clientHello.Random[:]) { + stats.Stats.ReplayDetected() + return errors.New("replay attack is detected") + } + + antireplay.Cache.AddTLS(clientHello.Random[:]) + + hostCert, err := connectionServerInstance.get() + if err != nil { + return fmt.Errorf("cannot get host certificate: %w", err) + } + + serverHello := tlstypes.NewServerHello(clientHello) + serverHelloPacket := serverHello.WelcomePacket(hostCert) + + if _, err := conn.Write(serverHelloPacket); err != nil { + return fmt.Errorf("cannot send welcome packet: %w", err) + } + + return nil +} + +func (c *ClientProtocol) cloakHost(clientConn io.ReadWriteCloser) { + addr := net.JoinHostPort(config.C.CloakHost, strconv.Itoa(config.C.CloakPort)) + hostConn, err := net.Dial("tcp", addr) + + if err != nil { + return + } + + defer hostConn.Close() + + wg := &sync.WaitGroup{} + wg.Add(2) + + go c.pipe(hostConn, clientConn, wg) + + go c.pipe(clientConn, hostConn, wg) + + wg.Wait() +} + +func (c *ClientProtocol) pipe(dst io.WriteCloser, src io.Reader, wg *sync.WaitGroup) { + defer func() { + wg.Done() + dst.Close() + }() + + io.Copy(dst, src) // nolint: errcheck +} + +func MakeClientProtocol() protocol.ClientProtocol { + return &ClientProtocol{} +} diff --git a/faketls/consts.go b/faketls/consts.go new file mode 100644 index 0000000..9bd2353 --- /dev/null +++ b/faketls/consts.go @@ -0,0 +1,30 @@ +package faketls + +import ( + "errors" + "time" +) + +const ( + TimeSkew = 5 * time.Second + TimeFromBoot = 24 * 60 * 60 +) + +var ( + errBadDigest = errors.New("bad digest") + errBadTime = errors.New("bad time") + + faketlsStartBytes = [...]byte{ + 0x16, + 0x03, + 0x01, + 0x02, + 0x00, + 0x01, + 0x00, + 0x01, + 0xfc, + 0x03, + 0x03, + } +) diff --git a/faketls/init.go b/faketls/init.go new file mode 100644 index 0000000..d2c8369 --- /dev/null +++ b/faketls/init.go @@ -0,0 +1,50 @@ +package faketls + +import ( + "container/ring" + "context" + "sync" + "time" + + "mtg/config" +) + +var ( + connectionServerInstance connectionServer + connectionServerInitOnce sync.Once +) + +const ( + connectionServerKeepCertificates = 5 + connectionServerUpdateEvery = 10 * time.Minute +) + +func Init(ctx context.Context) { + connectionServerInitOnce.Do(func() { + if config.C.CloakHost == "" { + return + } + + connectionServerInstance = connectionServer{ + channelGet: make(chan chan<- []byte), + ctx: ctx, + } + + cert, err := connectionServerInstance.fetch() + if err != nil { + panic(err) + } + + r := ring.New(connectionServerKeepCertificates) + + for i := 0; i < connectionServerKeepCertificates; i++ { + r.Value = cert + r = r.Next() + } + + connectionServerInstance.nextWriteItem = r + connectionServerInstance.nextReadItem = r + + go connectionServerInstance.run(connectionServerUpdateEvery) + }) +} diff --git a/go.mod b/go.mod index f3e79de..901f250 100644 --- a/go.mod +++ b/go.mod @@ -1,34 +1,18 @@ -module github.com/9seconds/mtg +module mtg -replace github.com/golang/lint => github.com/golang/lint v0.0.0-20190227174305-8f45f776aaf1 +go 1.13 require ( - github.com/OneOfOne/xxhash v1.2.5 // indirect - github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect - github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 // indirect - github.com/allegro/bigcache v1.2.1 + github.com/VictoriaMetrics/fastcache v1.5.2 + github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d github.com/beevik/ntp v0.2.0 - github.com/cespare/xxhash v1.1.0 - github.com/dustin/go-humanize v1.0.0 - github.com/gofrs/uuid v3.2.0+incompatible - github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d - github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 // indirect - github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2 // indirect - github.com/kr/pretty v0.1.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.1 // indirect - github.com/pkg/errors v0.8.1 // indirect - github.com/prometheus/client_golang v1.1.0 - github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 // indirect - github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/stretchr/testify v1.4.0 - go.uber.org/atomic v1.4.0 // indirect - go.uber.org/multierr v1.1.0 // indirect - go.uber.org/zap v1.10.0 - golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect - golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a + github.com/prometheus/client_golang v1.2.1 + go.uber.org/multierr v1.4.0 // indirect + go.uber.org/zap v1.12.0 + golang.org/x/crypto v0.0.0-20191108234033-bd318be0434a + golang.org/x/net v0.0.0-20191109021931-daa7c04131f5 // indirect + golang.org/x/sys v0.0.0-20191110163157-d32e6e3b99c4 + golang.org/x/tools v0.0.0-20191109212701-97ad0ed33101 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/alexcesaro/statsd.v2 v2.0.0 - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect - gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect ) diff --git a/go.sum b/go.sum index 5e49b8c..3b89cb0 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,9 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/OneOfOne/xxhash v1.2.5 h1:zl/OfRA6nftbBK9qTohYBJ5xvw6C/oNKizR7cZGl3cI= github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/VictoriaMetrics/fastcache v1.5.2 h1:Erd8iIuBAL9kke8JzM4+WxkKuFkHh3ktwLanJvDgR44= +github.com/VictoriaMetrics/fastcache v1.5.2/go.mod h1:+jv9Ckb+za/P1ZRg/sulP5Ni1v49daAVERr0H3CuscE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= @@ -9,8 +12,10 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZq github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc= -github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/beevik/ntp v0.2.0 h1:sGsd+kAXzT0bfVfzJfce04g+dSRfrs+tbQW8lweuYgw= github.com/beevik/ntp v0.2.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= @@ -21,17 +26,17 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.0.1-0.20190104013014-3767db7a7e18/go.mod h1:HD5P3vAIAh+Y2GAxg0PrPN1P8WkepXGpjbUPDHJqqKM= +github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA= +github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= -github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= @@ -40,17 +45,16 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d h1:hJXjZMxj0SWlMoQkzeZDLi2cmeiWKa7y1B8Rg+qaoEc= -github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= -github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 h1:UUHMLvzt/31azWTN/ifGWef4WUqvXk0iRqdhdy/2uzI= -github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= -github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2 h1:Pp8RxiF4rSoXP9SED26WCfNB28/dwTDpPXS8XMJR8rc= -github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -73,8 +77,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI= +github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= @@ -83,18 +87,19 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCb github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d h1:GoAlyOgbOEIFdaDqxJVlbOQ1DtGmZWs/Qau0hIlk+WQ= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= -github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.0.1-0.20190317074736-539464a789e9/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= @@ -103,28 +108,53 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.4.0 h1:f3WCSC2KzAcBXGATIxAB1E2XuCpNU255wNKZ505qi3E= +go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.12.0 h1:dySoUQPFBGj6xwjmBzageVL8jGi8uxc6bEmJQjA06bw= +go.uber.org/zap v1.12.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191108234033-bd318be0434a h1:R/qVym5WAxsZWQqZCwDY/8sdVKV1m1WgU4/S5IRQAzc= +golang.org/x/crypto v0.0.0-20191108234033-bd318be0434a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191109021931-daa7c04131f5 h1:bHNaocaoJxYBo5cw41UyTMLjYlb8wPY7+WFrnklbHOM= +golang.org/x/net v0.0.0-20191109021931-daa7c04131f5/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191110163157-d32e6e3b99c4 h1:Hynbrlo6LbYI3H1IqXpkVDOcX/3HiPdhVEuyj5a59RM= +golang.org/x/sys v0.0.0-20191110163157-d32e6e3b99c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191109212701-97ad0ed33101 h1:LCmXVkvpQCDj724eX6irUTPCJP5GelFHxqGSWL2D1R0= +golang.org/x/tools v0.0.0-20191109212701-97ad0ed33101/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= @@ -132,9 +162,10 @@ gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= -gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/hub/connection.go b/hub/connection.go new file mode 100644 index 0000000..1bb67c7 --- /dev/null +++ b/hub/connection.go @@ -0,0 +1,163 @@ +package hub + +import ( + "fmt" + "math/rand" + "sync" + + "go.uber.org/zap" + + "mtg/conntypes" + "mtg/mtproto" + "mtg/mtproto/rpc" + "mtg/protocol" +) + +type connection struct { + conn conntypes.PacketReadWriteCloser + proxyConns map[string]*ProxyConn + closeOnce sync.Once + proxyConnsMutex sync.RWMutex + id int + logger *zap.SugaredLogger + + channelDone chan struct{} + channelWrite chan conntypes.Packet + channelRead chan *rpc.ProxyResponse + channelConnAttach chan *ProxyConn + channelConnDetach chan conntypes.ConnID +} + +func (c *connection) run() { + defer c.Close() + + for { + select { + case <-c.channelDone: + for _, v := range c.proxyConns { + v.Close() + } + + return + case resp := <-c.channelRead: + if channel, ok := c.proxyConns[string(resp.ConnID[:])]; ok { + if resp.Type == rpc.ProxyResponseTypeCloseExt { + channel.Close() + } else { + channel.put(resp) + } + } + case packet := <-c.channelWrite: + if err := c.conn.Write(packet); err != nil { + c.logger.Debugw("Cannot write packet", "error", err) + c.Close() + } + case conn := <-c.channelConnAttach: + c.proxyConnsMutex.Lock() + c.proxyConns[string(conn.req.ConnID[:])] = conn + c.proxyConnsMutex.Unlock() + conn.channelWrite = c.channelWrite + case connID := <-c.channelConnDetach: + if conn, ok := c.proxyConns[string(connID[:])]; ok { + c.proxyConnsMutex.Lock() + delete(c.proxyConns, string(connID[:])) + c.proxyConnsMutex.Unlock() + conn.Close() + } + } + } +} + +func (c *connection) readLoop() { + for { + packet, err := c.conn.Read() + if err != nil { + c.logger.Debugw("Cannot read packet", "error", err) + c.Close() + + return + } + + response, err := rpc.ParseProxyResponse(packet) + if err != nil { + c.logger.Debugw("Failed response", "error", err) + continue + } + + select { + case <-c.channelDone: + return + case c.channelRead <- response: + } + } +} + +func (c *connection) Close() { + c.closeOnce.Do(func() { + c.logger.Debugw("Closing connection") + + close(c.channelDone) + c.conn.Close() + }) +} + +func (c *connection) Done() bool { + select { + case <-c.channelDone: + return true + default: + return c.Len() == 0 + } +} + +func (c *connection) Len() int { + c.proxyConnsMutex.RLock() + defer c.proxyConnsMutex.RUnlock() + + return len(c.proxyConns) +} + +func (c *connection) Attach(conn *ProxyConn) error { + select { + case <-c.channelDone: + return ErrClosed + case c.channelConnAttach <- conn: + return nil + } +} + +func (c *connection) Detach(connID conntypes.ConnID) { + select { + case <-c.channelDone: + case c.channelConnDetach <- connID: + } +} + +func newConnection(req *protocol.TelegramRequest) (*connection, error) { + conn, err := mtproto.TelegramProtocol(req) + if err != nil { + return nil, fmt.Errorf("cannot create a new connection: %w", err) + } + + id := rand.Int() // nolint: gosec + rv := &connection{ + conn: conn, + id: id, + logger: zap.S().Named("hub-connection").With("id", id, + "dc", req.ClientProtocol.DC(), + "protocol", req.ClientProtocol.ConnectionProtocol()), + proxyConns: make(map[string]*ProxyConn), + + channelRead: make(chan *rpc.ProxyResponse, 1), + channelDone: make(chan struct{}), + channelWrite: make(chan conntypes.Packet), + channelConnAttach: make(chan *ProxyConn), + channelConnDetach: make(chan conntypes.ConnID), + } + + go rv.readLoop() + + go rv.run() + + return rv, nil +} diff --git a/hub/connection_list.go b/hub/connection_list.go new file mode 100644 index 0000000..7acdde1 --- /dev/null +++ b/hub/connection_list.go @@ -0,0 +1,70 @@ +package hub + +import ( + "fmt" + "sort" + + "mtg/config" +) + +type connectionList struct { + connections []*connection +} + +func (c *connectionList) Get(conn *ProxyConn) (*connection, error) { + if len(c.connections) > 0 { + c.gc() + } + + if len(c.connections) > 0 && c.connections[0].Len() < config.C.MultiplexPerConnection { + if err := c.connections[0].Attach(conn); err == nil { + return c.connections[0], nil + } + } + + newConn, err := newConnection(conn.req) + if err != nil { + return nil, fmt.Errorf("cannot allocate a new connection: %w", err) + } + + if err = newConn.Attach(conn); err != nil { + newConn.Close() + return nil, fmt.Errorf("cannot attach to the newly created connection: %w", err) + } + + c.connections = append(c.connections, newConn) + lastIndex := len(c.connections) - 1 + c.connections[0], c.connections[lastIndex] = c.connections[lastIndex], c.connections[0] + + return newConn, nil +} + +func (c *connectionList) gc() { + prevLen := len(c.connections) + + for i := len(c.connections) - 1; i >= 0; i-- { + lastIndex := len(c.connections) - 1 + + if c.connections[i].Done() { + c.connections[i].Close() + + if len(c.connections)-1 == i { + c.connections = c.connections[:lastIndex] + } else { + c.connections[i], c.connections[lastIndex] = c.connections[lastIndex], c.connections[i] + } + } + } + + if prevLen != len(c.connections) { + c.sort() + } +} + +func (c *connectionList) sort() { + if len(c.connections) > 1 { + sort.Slice(c.connections, func(i, j int) bool { + return c.connections[i].Len() < c.connections[j].Len() + }) + } +} diff --git a/hub/hub.go b/hub/hub.go new file mode 100644 index 0000000..fd291ec --- /dev/null +++ b/hub/hub.go @@ -0,0 +1,40 @@ +package hub + +import ( + "context" + "sync" + + "mtg/protocol" +) + +type hub struct { + muxes map[int32]*mux + mutex sync.RWMutex + ctx context.Context +} + +func (h *hub) Register(req *protocol.TelegramRequest) (*ProxyConn, error) { + return h.getMux(req).Get(req) +} + +func (h *hub) getMux(req *protocol.TelegramRequest) *mux { + var key int32 = 32767 + int32(req.ClientProtocol.DC()) + 100000*int32(req.ClientProtocol.ConnectionProtocol()) + + h.mutex.RLock() + m, ok := h.muxes[key] + h.mutex.RUnlock() + + if !ok { + h.mutex.Lock() + m, ok = h.muxes[key] + + if !ok { + m = newMux(h.ctx) + h.muxes[key] = m + } + + h.mutex.Unlock() + } + + return m +} diff --git a/hub/init.go b/hub/init.go new file mode 100644 index 0000000..19f5289 --- /dev/null +++ b/hub/init.go @@ -0,0 +1,24 @@ +package hub + +import ( + "context" + "errors" + "sync" +) + +var ( + ErrTimeout = errors.New("timeout") + ErrClosed = errors.New("context is closed") + + Hub Interface + initOnce sync.Once +) + +func Init(ctx context.Context) { + initOnce.Do(func() { + Hub = &hub{ + muxes: make(map[int32]*mux), + ctx: ctx, + } + }) +} diff --git a/hub/interface.go b/hub/interface.go new file mode 100644 index 0000000..883c91e --- /dev/null +++ b/hub/interface.go @@ -0,0 +1,7 @@ +package hub + +import "mtg/protocol" + +type Interface interface { + Register(*protocol.TelegramRequest) (*ProxyConn, error) +} diff --git a/hub/mux.go b/hub/mux.go new file mode 100644 index 0000000..7263826 --- /dev/null +++ b/hub/mux.go @@ -0,0 +1,81 @@ +package hub + +import ( + "context" + + "mtg/conntypes" + "mtg/protocol" +) + +type muxNewRequest struct { + req *protocol.TelegramRequest + resp chan<- muxNewResponse +} + +type muxNewResponse struct { + conn *ProxyConn + err error +} + +type mux struct { + connections connectionList + clients map[string]*connection + ctx context.Context + channelClosed chan conntypes.ConnID + channelNew chan muxNewRequest +} + +func (m *mux) run() { + for { + select { + case <-m.ctx.Done(): + for _, v := range m.clients { + v.Close() + } + + return + case req := <-m.channelNew: + proxyConn := newProxyConn(req.req, m.channelClosed) + conn, err := m.connections.Get(proxyConn) + + if err == nil { + m.clients[string(req.req.ConnID[:])] = conn + } + + req.resp <- muxNewResponse{ + conn: proxyConn, + err: err, + } + close(req.resp) + case connID := <-m.channelClosed: + if conn, ok := m.clients[string(connID[:])]; ok { + conn.Detach(connID) + delete(m.clients, string(connID[:])) + } + } + } +} + +func (m *mux) Get(req *protocol.TelegramRequest) (*ProxyConn, error) { + resp := make(chan muxNewResponse) + m.channelNew <- muxNewRequest{ + req: req, + resp: resp, + } + + rv := <-resp + + return rv.conn, rv.err +} + +func newMux(ctx context.Context) *mux { + m := &mux{ + ctx: ctx, + clients: make(map[string]*connection), + channelClosed: make(chan conntypes.ConnID, 1), + channelNew: make(chan muxNewRequest), + } + go m.run() + + return m +} diff --git a/hub/proxy_conn.go b/hub/proxy_conn.go new file mode 100644 index 0000000..bbfa428 --- /dev/null +++ b/hub/proxy_conn.go @@ -0,0 +1,77 @@ +package hub + +import ( + "sync" + "time" + + "mtg/conntypes" + "mtg/mtproto/rpc" + "mtg/protocol" +) + +const ( + proxyConnWriteTimeout = 2 * time.Minute + proxyConnReadTimeout = 2 * time.Minute +) + +type ProxyConn struct { + closeOnce sync.Once + req *protocol.TelegramRequest + channelResponse chan *rpc.ProxyResponse + channelClosed chan<- conntypes.ConnID + channelWrite chan<- conntypes.Packet + channelDone chan struct{} +} + +func (p *ProxyConn) Read() (*rpc.ProxyResponse, error) { + timer := time.NewTimer(proxyConnReadTimeout) + defer timer.Stop() + + select { + case <-timer.C: + return nil, ErrTimeout + case <-p.channelDone: + return nil, ErrClosed + case packet := <-p.channelResponse: + return packet, nil + } +} + +func (p *ProxyConn) Write(packet conntypes.Packet) error { + timer := time.NewTimer(proxyConnWriteTimeout) + defer timer.Stop() + + select { + case <-timer.C: + return ErrTimeout + case <-p.channelDone: + return ErrClosed + case p.channelWrite <- packet: + return nil + } +} + +func (p *ProxyConn) put(response *rpc.ProxyResponse) { + select { + case <-p.channelDone: + case p.channelResponse <- response: + } +} + +func (p *ProxyConn) Close() { + p.closeOnce.Do(func() { + close(p.channelDone) + go func() { + p.channelClosed <- p.req.ConnID + }() + }) +} + +func newProxyConn(req *protocol.TelegramRequest, channelClosed chan<- conntypes.ConnID) *ProxyConn { + return &ProxyConn{ + channelResponse: make(chan *rpc.ProxyResponse), + channelDone: make(chan struct{}), + channelClosed: channelClosed, + req: req, + } +} diff --git a/main.go b/main.go index 921544a..6864153 100644 --- a/main.go +++ b/main.go @@ -1,22 +1,15 @@ package main import ( - "encoding/json" - "fmt" - "io" "math/rand" "os" "time" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "gopkg.in/alecthomas/kingpin.v2" + kingpin "gopkg.in/alecthomas/kingpin.v2" - "github.com/9seconds/mtg/config" - "github.com/9seconds/mtg/ntp" - "github.com/9seconds/mtg/proxy" - "github.com/9seconds/mtg/rlimit" - "github.com/9seconds/mtg/stats" + "mtg/cli" + "mtg/config" + "mtg/utils" ) var version = "dev" // this has to be set by build ld flags @@ -24,216 +17,145 @@ var version = "dev" // this has to be set by build ld flags var ( app = kingpin.New("mtg", "Simple MTPROTO proxy.") - debug = app.Flag("debug", + generateSecretCommand = app.Command("generate-secret", + "Generate new secret") + generateCloakHost = generateSecretCommand.Flag("cloak-host", + "A host to use for TLS cloaking."). + Short('c'). + Default("storage.googleapis.com"). + String() + generateSecretType = generateSecretCommand.Arg("type", + "A type of secret to generate. Valid options are 'simple', 'secured' and 'tls'"). + Required(). + Enum("simple", "secured", "tls") + + runCommand = app.Command("run", + "Run new proxy instance") + runDebug = runCommand.Flag("debug", "Run in debug mode."). Short('d'). Envar("MTG_DEBUG"). Bool() - verbose = app.Flag("verbose", + runVerbose = runCommand.Flag("verbose", "Run in verbose mode."). Short('v'). Envar("MTG_VERBOSE"). Bool() - - bindIP = app.Flag("bind-ip", - "Which IP to bind to."). + runBind = runCommand.Flag("bind", + "Host:Port to bind proxy to."). Short('b'). - Envar("MTG_IP"). - Default("127.0.0.1"). - IP() - bindPort = app.Flag("bind-port", - "Which port to bind to."). - Short('p'). - Envar("MTG_PORT"). - Default("3128"). - Uint16() - - publicIPv4 = app.Flag("public-ipv4", - "Which IPv4 address is public."). + Envar("MTG_BIND"). + Default("0.0.0.0:3128"). + TCP() + runPublicIPv4 = runCommand.Flag("public-ipv4", + "Which IPv4 host:port to use."). Short('4'). Envar("MTG_IPV4"). - IP() - publicIPv4Port = app.Flag("public-ipv4-port", - "Which IPv4 port is public. Default is 'bind-port' value."). - Envar("MTG_IPV4_PORT"). - Uint16() - - publicIPv6 = app.Flag("public-ipv6", - "Which IPv6 address is public."). + TCP() + runPublicIPv6 = runCommand.Flag("public-ipv6", + "Which IPv6 host:port to use."). Short('6'). Envar("MTG_IPV6"). - IP() - publicIPv6Port = app.Flag("public-ipv6-port", - "Which IPv6 port is public. Default is 'bind-port' value."). - Envar("MTG_IPV6_PORT"). - Uint16() - - statsIP = app.Flag("stats-ip", - "Which IP bind stats server to."). + TCP() + runStatsBind = runCommand.Flag("stats-bind", + "Which Host:Port to bind stats server to."). Short('t'). - Envar("MTG_STATS_IP"). - Default("127.0.0.1"). - IP() - statsPort = app.Flag("stats-port", - "Which port bind stats to."). - Short('q'). - Envar("MTG_STATS_PORT"). - Default("3129"). - Uint16() - - statsdIP = app.Flag("statsd-ip", - "Which IP should we use for working with statsd."). - Envar("MTG_STATSD_IP"). + Envar("MTG_STATS_BIND"). + Default("127.0.0.1:3129"). + TCP() + runStatsNamespace = runCommand.Flag("stats-namespace", + "Which namespace to use for Prometheus."). + Envar("MTG_STATS_NAMESPACE"). + Default("mtg"). String() - statsdPort = app.Flag("statsd-port", - "Which port should we use for working with statsd."). - Envar("MTG_STATSD_PORT"). - Default("8125"). - Uint16() - statsdNetwork = app.Flag("statsd-network", + runStatsdAddress = runCommand.Flag("statsd-addr", + "Host:port of statsd server"). + Envar("MTG_STATSD_ADDR"). + TCP() + runStatsdNetwork = runCommand.Flag("statsd-network", "Which network is used to work with statsd. Only 'tcp' and 'udp' are supported."). Envar("MTG_STATSD_NETWORK"). Default("udp"). - String() - statsdPrefix = app.Flag("statsd-prefix", - "Which bucket prefix should we use for sending stats to statsd."). - Envar("MTG_STATSD_PREFIX"). - Default("mtg"). - String() - statsdTagsFormat = app.Flag("statsd-tags-format", + Enum("udp", "tcp") + runStatsdTagsFormat = runCommand.Flag("statsd-tags-format", "Which tag format should we use to send stats metrics. Valid options are 'datadog' and 'influxdb'."). Envar("MTG_STATSD_TAGS_FORMAT"). - String() - statsdTags = app.Flag("statsd-tags", + Default("influxdb"). + Enum("datadog", "influxdb") + runStatsdTags = runCommand.Flag("statsd-tags", "Tags to use for working with statsd (specified as 'key=value')."). Envar("MTG_STATSD_TAGS"). StringMap() - - prometheusPrefix = app.Flag("prometheus-prefix", - "Which namespace to use to send stats to Prometheus."). - Envar("MTG_PROMETHEUS_PREFIX"). - Default("mtg"). - String() - - writeBufferSize = app.Flag("write-buffer", + runWriteBufferSize = runCommand.Flag("write-buffer", "Write buffer size in bytes. You can think about it as a buffer from client to Telegram."). Short('w'). Envar("MTG_BUFFER_WRITE"). - Default("65536"). - Uint32() - readBufferSize = app.Flag("read-buffer", + Default("65536KB"). + Bytes() + runReadBufferSize = runCommand.Flag("read-buffer", "Read buffer size in bytes. You can think about it as a buffer from Telegram to client."). Short('r'). Envar("MTG_BUFFER_READ"). - Default("131072"). - Uint32() - secureOnly = app.Flag("secure-only", - "Support clients with dd-secrets only."). - Short('s'). - Envar("MTG_SECURE_ONLY"). - Bool() - - antiReplayMaxSize = app.Flag("anti-replay-max-size", - "Max size of antireplay cache in megabytes."). + Default("131072KB"). + Bytes() + runTLSCloakPort = runCommand.Flag("cloak-port", + "Port which should be used for host cloaking."). + Envar("MTG_CLOAK_PORT"). + Default("443"). + Uint16() + runAntiReplayMaxSize = runCommand.Flag("anti-replay-max-size", + "Max size of antireplay cache."). Envar("MTG_ANTIREPLAY_MAXSIZE"). - Default("128"). - Int() - antiReplayEvictionTime = app.Flag("anti-replay-eviction-time", - "Eviction time period for obfuscated2 handshakes"). - Envar("MTG_ANTIREPLAY_EVICTIONTIME"). - Default("168h"). - Duration() - - secret = app.Arg("secret", "Secret of this proxy.").Required().HexBytes() - adtag = app.Arg("adtag", "ADTag of the proxy.").HexBytes() + Default("128MB"). + Bytes() + runMultiplexPerConnection = runCommand.Flag("multiplex-per-connection", + "How many clients can share a single connection to Telegram."). + Envar("MTG_MULTIPLEX_PERCONNECTION"). + Default("50"). + Uint() + runSecret = runCommand.Arg("secret", "Secret of this proxy.").Required().HexBytes() + runAdtag = runCommand.Arg("adtag", "ADTag of the proxy.").HexBytes() ) -func main() { // nolint: gocyclo +func main() { rand.Seed(time.Now().UTC().UnixNano()) app.Version(version) app.HelpFlag.Short('h') - kingpin.MustParse(app.Parse(os.Args[1:])) - - err := rlimit.Set() - if err != nil { - usage(err.Error()) + if err := utils.SetLimits(); err != nil { + cli.Fatal(err) } - conf, err := config.NewConfig(*debug, *verbose, - *writeBufferSize, *readBufferSize, - *bindIP, *publicIPv4, *publicIPv6, *statsIP, - *bindPort, *publicIPv4Port, *publicIPv6Port, *statsPort, *statsdPort, - *statsdIP, *statsdNetwork, *statsdPrefix, *statsdTagsFormat, - *statsdTags, *prometheusPrefix, *secureOnly, - *antiReplayMaxSize, *antiReplayEvictionTime, - *secret, *adtag, - ) - if err != nil { - usage(err.Error()) - } - - atom := zap.NewAtomicLevel() - switch { - case conf.Debug: - atom.SetLevel(zapcore.DebugLevel) - case conf.Verbose: - atom.SetLevel(zapcore.InfoLevel) - default: - atom.SetLevel(zapcore.ErrorLevel) - } - encoderCfg := zap.NewProductionEncoderConfig() - logger := zap.New(zapcore.NewCore( - zapcore.NewJSONEncoder(encoderCfg), - zapcore.Lock(os.Stderr), - atom, - )) - zap.ReplaceGlobals(logger) - defer logger.Sync() // nolint: errcheck - - printURLs(conf.GetURLs()) - zap.S().Debugw("Configuration", "config", conf) - - if conf.UseMiddleProxy() { - zap.S().Infow("Use middle proxy connection to Telegram") - if diff, err := ntp.Fetch(); err != nil { - zap.S().Warnw("Could not fetch time data from NTP") - } else { - if diff >= time.Second { - usage(fmt.Sprintf("You choose to use middle proxy but your clock drift (%s) "+ - "is bigger than 1 second. Please, sync your time", diff)) - } - go ntp.AutoUpdate() + switch kingpin.MustParse(app.Parse(os.Args[1:])) { + case generateSecretCommand.FullCommand(): + cli.Generate(*generateSecretType, *generateCloakHost) + case runCommand.FullCommand(): + err := config.Init( + config.Opt{Option: config.OptionTypeDebug, Value: *runDebug}, + config.Opt{Option: config.OptionTypeVerbose, Value: *runVerbose}, + config.Opt{Option: config.OptionTypeBind, Value: *runBind}, + config.Opt{Option: config.OptionTypePublicIPv4, Value: *runPublicIPv4}, + config.Opt{Option: config.OptionTypePublicIPv6, Value: *runPublicIPv6}, + config.Opt{Option: config.OptionTypeStatsBind, Value: *runStatsBind}, + config.Opt{Option: config.OptionTypeStatsNamespace, Value: *runStatsNamespace}, + config.Opt{Option: config.OptionTypeStatsdAddress, Value: *runStatsdAddress}, + config.Opt{Option: config.OptionTypeStatsdNetwork, Value: *runStatsdNetwork}, + config.Opt{Option: config.OptionTypeStatsdTagsFormat, Value: *runStatsdTagsFormat}, + config.Opt{Option: config.OptionTypeStatsdTags, Value: *runStatsdTags}, + config.Opt{Option: config.OptionTypeWriteBufferSize, Value: *runWriteBufferSize}, + config.Opt{Option: config.OptionTypeReadBufferSize, Value: *runReadBufferSize}, + config.Opt{Option: config.OptionTypeCloakPort, Value: *runTLSCloakPort}, + config.Opt{Option: config.OptionTypeAntiReplayMaxSize, Value: *runAntiReplayMaxSize}, + config.Opt{Option: config.OptionTypeMultiplexPerConnection, Value: *runMultiplexPerConnection}, + config.Opt{Option: config.OptionTypeSecret, Value: *runSecret}, + config.Opt{Option: config.OptionTypeAdtag, Value: *runAdtag}, + ) + if err != nil { + cli.Fatal(err) } - } else { - zap.S().Infow("Use direct connection to Telegram") - } - if err := stats.Init(conf); err != nil { - panic(err) - } - - server, err := proxy.NewProxy(conf) - if err != nil { - panic(err) - } - if err := server.Serve(); err != nil { - zap.S().Fatalw("Server stopped", "error", err) + if err := cli.Proxy(); err != nil { + cli.Fatal(err) + } } } - -func printURLs(data interface{}) { - encoder := json.NewEncoder(os.Stdout) - encoder.SetEscapeHTML(false) - encoder.SetIndent("", " ") - - err := encoder.Encode(data) - if err != nil { - panic(err) - } -} - -func usage(msg string) { - io.WriteString(os.Stderr, msg+"\n") // nolint: errcheck, gosec - os.Exit(1) -} diff --git a/mtproto/connection_options.go b/mtproto/connection_options.go deleted file mode 100644 index 0230f5f..0000000 --- a/mtproto/connection_options.go +++ /dev/null @@ -1,87 +0,0 @@ -package mtproto - -import ( - "bytes" - "net" - - "github.com/juju/errors" -) - -// ConnectionType is a type of obfuscated2/mtproto connection requested -// by the user. -type ConnectionType uint8 - -// ConnectionProtocol is a type of IP protocol to use. -type ConnectionProtocol uint8 - -// Hacks is a simple structure to store flags for packet transmission. -type Hacks struct { - SimpleAck bool - QuickAck bool -} - -// ConnectionOpts presents an options, metadata on connection requested -// by the user on handshake. -type ConnectionOpts struct { - DC int16 - ConnectionType ConnectionType - ConnectionProto ConnectionProtocol - // Read and Write means direction related to the client. - // ReadHacks are meant to be flushed on client read - // WriteHacks are meant to be flushed on client write. - ReadHacks Hacks - WriteHacks Hacks - ClientAddr *net.TCPAddr -} - -// Different connection types which user requests from Telegram. -const ( - ConnectionTypeUnknown ConnectionType = iota - ConnectionTypeAbridged - ConnectionTypeIntermediate - ConnectionTypeSecure -) - -// ConnectionProtocol* define which connection protocols to use. -// ConnectionProtocolAny means that any is suitable. -const ( - ConnectionProtocolIPv4 ConnectionProtocol = 1 - ConnectionProtocolIPv6 = ConnectionProtocolIPv4 << 1 - ConnectionProtocolAny = ConnectionProtocolIPv4 | ConnectionProtocolIPv6 -) - -// Connection tags for mtproto handshakes. -var ( - ConnectionTagAbridged = []byte{0xef, 0xef, 0xef, 0xef} - ConnectionTagIntermediate = []byte{0xee, 0xee, 0xee, 0xee} - ConnectionTagSecure = []byte{0xdd, 0xdd, 0xdd, 0xdd} -) - -// Tag maps connection type to the corresponding handshake tag. -func (t ConnectionType) Tag() ([]byte, error) { - switch t { - case ConnectionTypeAbridged: - return ConnectionTagAbridged, nil - case ConnectionTypeIntermediate: - return ConnectionTagIntermediate, nil - case ConnectionTypeSecure: - return ConnectionTagSecure, nil - default: - return nil, errors.Errorf("Unknown connection type %d", t) - } -} - -// ConnectionTagFromHandshake maps magic bytes to the connection type. -func ConnectionTagFromHandshake(magic []byte) (ConnectionType, error) { - if bytes.Equal(magic, ConnectionTagIntermediate) { - return ConnectionTypeIntermediate, nil - } - if bytes.Equal(magic, ConnectionTagAbridged) { - return ConnectionTypeAbridged, nil - } - if bytes.Equal(magic, ConnectionTagSecure) { - return ConnectionTypeSecure, nil - } - - return ConnectionTypeUnknown, errors.New("Unknown handshake protocol") -} diff --git a/mtproto/protocol.go b/mtproto/protocol.go new file mode 100644 index 0000000..3dfdda4 --- /dev/null +++ b/mtproto/protocol.go @@ -0,0 +1,102 @@ +package mtproto + +import ( + "fmt" + + "mtg/conntypes" + "mtg/mtproto/rpc" + "mtg/protocol" + "mtg/telegram" + "mtg/wrappers/packet" + "mtg/wrappers/stream" +) + +func TelegramProtocol(req *protocol.TelegramRequest) (conntypes.PacketReadWriteCloser, error) { + conn, err := telegram.Middle.Dial(req.ClientProtocol.DC(), + req.ClientProtocol.ConnectionProtocol()) + if err != nil { + return nil, fmt.Errorf("cannot connect to telegram: %w", err) + } + + rpcNonceConn := packet.NewMtprotoFrame(conn, rpc.SeqNoNonce) + rpcNonceReq, err := doRPCNonceRequest(rpcNonceConn) + + if err != nil { + return nil, fmt.Errorf("cannot do nonce request: %w", err) + } + + rpcNonceResp, err := getRPCNonceResponse(rpcNonceConn, rpcNonceReq) + if err != nil { + return nil, fmt.Errorf("cannot get nonce response: %w", err) + } + + secureConn := stream.NewMiddleProxyCipher(conn, rpcNonceReq, rpcNonceResp, telegram.Middle.Secret()) + frameConn := packet.NewMtprotoFrame(secureConn, rpc.SeqNoHandshake) + + if err := doRPCHandshakeRequest(frameConn); err != nil { + return nil, fmt.Errorf("cannot do handshake request: %w", err) + } + + if err := getRPCHandshakeResponse(frameConn); err != nil { + return nil, fmt.Errorf("cannot get handshake response: %w", err) + } + + return frameConn, nil +} + +func doRPCNonceRequest(conn conntypes.BasePacketWriter) (*rpc.NonceRequest, error) { + rpcNonceReq, err := rpc.NewNonceRequest(telegram.Middle.Secret()) + if err != nil { + panic(err) + } + + if err := conn.Write(rpcNonceReq.Bytes()); err != nil { + return nil, err + } + + return rpcNonceReq, nil +} + +func getRPCNonceResponse(conn conntypes.BasePacketReader, req *rpc.NonceRequest) (*rpc.NonceResponse, error) { + packet, err := conn.Read() + if err != nil { + return nil, fmt.Errorf("cannot read from connection: %w", err) + } + + resp, err := rpc.NewNonceResponse(packet) + if err != nil { + return nil, fmt.Errorf("cannot build rpc nonce response: %w", err) + } + + if err = resp.Valid(req); err != nil { + return nil, fmt.Errorf("invalid nonce response: %w", err) + } + + return resp, nil +} + +func doRPCHandshakeRequest(conn conntypes.BasePacketWriter) error { + if err := conn.Write(rpc.HandshakeRequest); err != nil { + return fmt.Errorf("cannot make a request: %w", err) + } + + return nil +} + +func getRPCHandshakeResponse(conn conntypes.BasePacketReader) error { + packet, err := conn.Read() + if err != nil { + return fmt.Errorf("cannot read a response: %w", err) + } + + resp, err := rpc.NewHandshakeResponse(packet) + if err != nil { + return fmt.Errorf("cannot build a handshake response: %w", err) + } + + if err := resp.Valid(); err != nil { + return fmt.Errorf("invalid handshake response: %w", err) + } + + return nil +} diff --git a/mtproto/rpc/rpc.go b/mtproto/rpc/consts.go similarity index 100% rename from mtproto/rpc/rpc.go rename to mtproto/rpc/consts.go diff --git a/mtproto/rpc/handshake_request.go b/mtproto/rpc/handshake_request.go index dcd2290..e0971cc 100644 --- a/mtproto/rpc/handshake_request.go +++ b/mtproto/rpc/handshake_request.go @@ -1,26 +1,5 @@ package rpc -import "bytes" - -// HandshakeRequest is the data type which is responsible for -// constructing of correct handshake request. -type HandshakeRequest struct { -} - -// Bytes returns serialized handshake request. -func (r *HandshakeRequest) Bytes() []byte { - buf := &bytes.Buffer{} - buf.Grow(len(TagHandshake) + len(HandshakeFlags) + len(HandshakeSenderPID) + len(HandshakePeerPID)) - - buf.Write(TagHandshake) // nolint: gosec - buf.Write(HandshakeFlags) // nolint: gosec - buf.Write(HandshakeSenderPID) // nolint: gosec - buf.Write(HandshakePeerPID) // nolint: gosec - - return buf.Bytes() -} - -// NewHandshakeRequest creates new HandshakeRequest instance. -func NewHandshakeRequest() *HandshakeRequest { - return &HandshakeRequest{} -} +var HandshakeRequest = append(TagHandshake, + append(HandshakeFlags, + append(HandshakeSenderPID, HandshakePeerPID...)...)...) diff --git a/mtproto/rpc/handshake_response.go b/mtproto/rpc/handshake_response.go index 962e949..1f1d509 100644 --- a/mtproto/rpc/handshake_response.go +++ b/mtproto/rpc/handshake_response.go @@ -2,12 +2,10 @@ package rpc import ( "bytes" - - "github.com/juju/errors" + "errors" + "fmt" ) -// HandshakeResponse defines data structure which is used for storage of -// handshake response. type HandshakeResponse struct { Type []byte Flags []byte @@ -28,12 +26,13 @@ func (r *HandshakeResponse) Bytes() []byte { } // Valid checks that handshake response compliments request. -func (r *HandshakeResponse) Valid(req *HandshakeRequest) error { +func (r *HandshakeResponse) Valid() error { if !bytes.Equal(r.Type, TagHandshake) { - return errors.New("Unexpected handshake tag") + return errors.New("unexpected handshake tag") } + if !bytes.Equal(r.PeerPID, HandshakeSenderPID) { - return errors.New("Incorrect sender PID") + return errors.New("incorrect sender PID") } return nil @@ -43,7 +42,7 @@ func (r *HandshakeResponse) Valid(req *HandshakeRequest) error { // data. func NewHandshakeResponse(data []byte) (*HandshakeResponse, error) { if len(data) != 32 { - return nil, errors.New("Incorrect handshake response length") + return nil, fmt.Errorf("incorrect handshake response length %d", len(data)) } return &HandshakeResponse{ diff --git a/mtproto/rpc/nonce_request.go b/mtproto/rpc/nonce_request.go index 60cf218..8a06c26 100644 --- a/mtproto/rpc/nonce_request.go +++ b/mtproto/rpc/nonce_request.go @@ -4,13 +4,10 @@ import ( "bytes" "crypto/rand" "encoding/binary" + "fmt" "time" - - "github.com/juju/errors" ) -// NonceRequest is the data type which contains all the data for correct -// nonce request. type NonceRequest struct { KeySelector []byte CryptoTS []byte @@ -21,11 +18,11 @@ type NonceRequest struct { func (r *NonceRequest) Bytes() []byte { buf := &bytes.Buffer{} - buf.Write(TagNonce) // nolint: gosec - buf.Write(r.KeySelector) // nolint: gosec - buf.Write(NonceCryptoAES) // nolint: gosec - buf.Write(r.CryptoTS) // nolint: gosec - buf.Write(r.Nonce) // nolint: gosec + buf.Write(TagNonce) + buf.Write(r.KeySelector) + buf.Write(NonceCryptoAES) + buf.Write(r.CryptoTS) + buf.Write(r.Nonce) return buf.Bytes() } @@ -37,8 +34,9 @@ func NewNonceRequest(proxySecret []byte) (*NonceRequest, error) { cryptoTS := make([]byte, 4) if _, err := rand.Read(nonce); err != nil { - return nil, errors.Annotate(err, "Cannot generate nonce") + return nil, fmt.Errorf("cannot generate nonce: %w", err) } + copy(keySelector, proxySecret) timestamp := time.Now().Truncate(time.Second).Unix() % 4294967296 // 256 ^ 4 - do not know how to name diff --git a/mtproto/rpc/nonce_response.go b/mtproto/rpc/nonce_response.go index f7c592e..b214eb5 100644 --- a/mtproto/rpc/nonce_response.go +++ b/mtproto/rpc/nonce_response.go @@ -2,11 +2,10 @@ package rpc import ( "bytes" - - "github.com/juju/errors" + "errors" + "fmt" ) -// NonceResponse is the data type which contains data of nonce response. type NonceResponse struct { NonceRequest @@ -27,16 +26,17 @@ func (r *NonceResponse) Bytes() []byte { return buf.Bytes() } -// Valid checks that nonce response compliments nonce request. func (r *NonceResponse) Valid(req *NonceRequest) error { if !bytes.Equal(r.Type, TagNonce) { - return errors.New("Unexpected RPC type") + return errors.New("unexpected RPC type") } + if !bytes.Equal(r.Crypto, NonceCryptoAES) { - return errors.New("Unexpected crypto type") + return errors.New("unexpected crypto type") } + if !bytes.Equal(r.KeySelector, req.KeySelector) { - return errors.New("Unexpected key selector") + return errors.New("unexpected key selector") } return nil @@ -45,7 +45,7 @@ func (r *NonceResponse) Valid(req *NonceRequest) error { // NewNonceResponse build new nonce response based on the given data. func NewNonceResponse(data []byte) (*NonceResponse, error) { if len(data) != 32 { - return nil, errors.New("Unexpected message length") + return nil, fmt.Errorf("unexpected message length %d", len(data)) } return &NonceResponse{ diff --git a/mtproto/rpc/proxy_flags.go b/mtproto/rpc/proxy_flags.go index c59d33f..2179c66 100644 --- a/mtproto/rpc/proxy_flags.go +++ b/mtproto/rpc/proxy_flags.go @@ -5,53 +5,60 @@ import ( "strings" ) -type proxyRequestFlags uint32 +type ProxyRequestFlags uint32 const ( - proxyRequestFlagsHasAdTag proxyRequestFlags = 0x8 - proxyRequestFlagsEncrypted proxyRequestFlags = 0x2 - proxyRequestFlagsMagic proxyRequestFlags = 0x1000 - proxyRequestFlagsExtMode2 proxyRequestFlags = 0x20000 - proxyRequestFlagsIntermediate proxyRequestFlags = 0x20000000 - proxyRequestFlagsAbdridged proxyRequestFlags = 0x40000000 - proxyRequestFlagsQuickAck proxyRequestFlags = 0x80000000 - proxyRequestFlagsPad proxyRequestFlags = 0x8000000 + ProxyRequestFlagsHasAdTag ProxyRequestFlags = 0x8 + ProxyRequestFlagsEncrypted ProxyRequestFlags = 0x2 + ProxyRequestFlagsMagic ProxyRequestFlags = 0x1000 + ProxyRequestFlagsExtMode2 ProxyRequestFlags = 0x20000 + ProxyRequestFlagsIntermediate ProxyRequestFlags = 0x20000000 + ProxyRequestFlagsAbdridged ProxyRequestFlags = 0x40000000 + ProxyRequestFlagsQuickAck ProxyRequestFlags = 0x80000000 + ProxyRequestFlagsPad ProxyRequestFlags = 0x8000000 ) -var proxyRequestFlagsEncryptedPrefix [8]byte +var ProxyRequestFlagsEncryptedPrefix [8]byte -func (r proxyRequestFlags) Bytes() []byte { +func (r ProxyRequestFlags) Bytes() []byte { converted := make([]byte, 4) binary.LittleEndian.PutUint32(converted, uint32(r)) return converted } -func (r proxyRequestFlags) String() string { +func (r ProxyRequestFlags) String() string { flags := make([]string, 0, 7) - if r&proxyRequestFlagsHasAdTag != 0 { + if r&ProxyRequestFlagsHasAdTag != 0 { flags = append(flags, "HAS_AD_TAG") } - if r&proxyRequestFlagsEncrypted != 0 { + + if r&ProxyRequestFlagsEncrypted != 0 { flags = append(flags, "ENCRYPTED") } - if r&proxyRequestFlagsMagic != 0 { + + if r&ProxyRequestFlagsMagic != 0 { flags = append(flags, "MAGIC") } - if r&proxyRequestFlagsExtMode2 != 0 { + + if r&ProxyRequestFlagsExtMode2 != 0 { flags = append(flags, "EXT_MODE_2") } - if r&proxyRequestFlagsIntermediate != 0 { + + if r&ProxyRequestFlagsIntermediate != 0 { flags = append(flags, "INTERMEDIATE") } - if r&proxyRequestFlagsAbdridged != 0 { + + if r&ProxyRequestFlagsAbdridged != 0 { flags = append(flags, "ABRIDGED") } - if r&proxyRequestFlagsQuickAck != 0 { + + if r&ProxyRequestFlagsQuickAck != 0 { flags = append(flags, "QUICK_ACK") } - if r&proxyRequestFlagsPad != 0 { + + if r&ProxyRequestFlagsPad != 0 { flags = append(flags, "PAD") } diff --git a/mtproto/rpc/proxy_request.go b/mtproto/rpc/proxy_request.go deleted file mode 100644 index bb9e68f..0000000 --- a/mtproto/rpc/proxy_request.go +++ /dev/null @@ -1,105 +0,0 @@ -package rpc - -import ( - "bytes" - "crypto/rand" - "encoding/binary" - "fmt" - "net" - - "github.com/juju/errors" - - "github.com/9seconds/mtg/mtproto" -) - -// ProxyRequest is the data type for storing data required to compose -// RPC_PROXY_REQ request. -type ProxyRequest struct { - Flags proxyRequestFlags - ConnectionID []byte - OurIPPort []byte - ClientIPPort []byte - ADTag []byte - Options *mtproto.ConnectionOpts -} - -// MakeHeader makes RPC_PROXY_REQ header. We need only to append the -// data for it. -func (r *ProxyRequest) MakeHeader(message []byte) (*bytes.Buffer, fmt.Stringer) { - bufferLength := len(TagProxyRequest) + - 4 + // len(flags) - len(r.ConnectionID) + - len(r.ClientIPPort) + - len(r.OurIPPort) + - len(ProxyRequestExtraSize) + - len(ProxyRequestProxyTag) + - 1 + // len(AdTag) - len(r.ADTag) - bufferLength += bufferLength % 4 - - buf := &bytes.Buffer{} - buf.Grow(bufferLength + len(message)) - - flags := r.Flags - if r.Options.ReadHacks.QuickAck { - flags |= proxyRequestFlagsQuickAck - } - - if bytes.HasPrefix(message, proxyRequestFlagsEncryptedPrefix[:]) { - flags |= proxyRequestFlagsEncrypted - } - - buf.Write(TagProxyRequest) // nolint: gosec - buf.Write(flags.Bytes()) // nolint: gosec - buf.Write(r.ConnectionID) // nolint: gosec - buf.Write(r.ClientIPPort) // nolint: gosec - buf.Write(r.OurIPPort) // nolint: gosec - buf.Write(ProxyRequestExtraSize) // nolint: gosec - buf.Write(ProxyRequestProxyTag) // nolint: gosec - buf.WriteByte(byte(len(r.ADTag))) // nolint: gosec - buf.Write(r.ADTag) // nolint: gosec - buf.Write(make([]byte, (4-buf.Len()%4)%4)) // nolint: gosec - - return buf, flags -} - -// NewProxyRequest build new ProxyRequest data structure. -func NewProxyRequest(clientAddr, ownAddr *net.TCPAddr, - opts *mtproto.ConnectionOpts, adTag []byte) (*ProxyRequest, error) { - flags := proxyRequestFlagsHasAdTag | proxyRequestFlagsMagic | proxyRequestFlagsExtMode2 - - switch opts.ConnectionType { - case mtproto.ConnectionTypeAbridged: - flags |= proxyRequestFlagsAbdridged - case mtproto.ConnectionTypeIntermediate: - flags |= proxyRequestFlagsIntermediate - case mtproto.ConnectionTypeSecure: - flags |= proxyRequestFlagsIntermediate | proxyRequestFlagsPad - default: - panic("Unknown connection type") - } - - request := &ProxyRequest{ - Flags: flags, - ADTag: adTag, - Options: opts, - ConnectionID: make([]byte, 8), - ClientIPPort: make([]byte, 16+4), - OurIPPort: make([]byte, 16+4), - } - - if _, err := rand.Read(request.ConnectionID); err != nil { - return nil, errors.Annotate(err, "Cannot generate connection ID") - } - - port := [4]byte{} - copy(request.ClientIPPort[:16], clientAddr.IP.To16()) - binary.LittleEndian.PutUint32(port[:], uint32(clientAddr.Port)) - copy(request.ClientIPPort[16:], port[:]) - - copy(request.OurIPPort[:16], ownAddr.IP.To16()) - binary.LittleEndian.PutUint32(port[:], uint32(ownAddr.Port)) - copy(request.OurIPPort[16:], port[:]) - - return request, nil -} diff --git a/mtproto/rpc/proxy_response.go b/mtproto/rpc/proxy_response.go new file mode 100644 index 0000000..8132d67 --- /dev/null +++ b/mtproto/rpc/proxy_response.go @@ -0,0 +1,53 @@ +package rpc + +import ( + "bytes" + "fmt" + + "mtg/conntypes" +) + +type ProxyResponseType uint8 + +const ( + ProxyResponseTypeAns ProxyResponseType = iota + ProxyResponseTypeSimpleAck + ProxyResponseTypeCloseExt +) + +type ProxyResponse struct { + Type ProxyResponseType + ConnID conntypes.ConnID + Payload conntypes.Packet +} + +func ParseProxyResponse(packet conntypes.Packet) (*ProxyResponse, error) { + var response ProxyResponse + + if len(packet) < 4 { + return nil, fmt.Errorf("incorrect packet length: %d", len(packet)) + } + + tag := packet[:4] + + switch { + case bytes.Equal(tag, TagProxyAns): + response.Type = ProxyResponseTypeAns + copy(response.ConnID[:], packet[8:16]) + response.Payload = packet[16:] + + return &response, nil + case bytes.Equal(tag, TagSimpleAck): + response.Type = ProxyResponseTypeSimpleAck + copy(response.ConnID[:], packet[4:12]) + response.Payload = packet[12:] + + return &response, nil + case bytes.Equal(tag, TagCloseExt): + response.Type = ProxyResponseTypeCloseExt + + return &response, nil + } + + return nil, fmt.Errorf("unknown response type %x", tag) +} diff --git a/ntp/ntp.go b/ntp/ntp.go index 5aee499..0b4fa1f 100644 --- a/ntp/ntp.go +++ b/ntp/ntp.go @@ -1,17 +1,17 @@ package ntp import ( + "fmt" "math/rand" "time" "github.com/beevik/ntp" - "github.com/juju/errors" "go.uber.org/zap" ) const autoUpdatePeriod = time.Minute -var ntpEndpoints = []string{ +var ntpEndpoints = [...]string{ "0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org", @@ -21,15 +21,17 @@ var ntpEndpoints = []string{ // Fetch fetches the data on time drift. func Fetch() (time.Duration, error) { url := ntpEndpoints[rand.Intn(len(ntpEndpoints))] + resp, err := ntp.Query(url) if err != nil { - return 0, errors.Annotatef(err, "Cannot fetch NTP server %s", url) + return 0, fmt.Errorf("cannot fetch NTP server %s: %w", url, err) } offsetInt := int64(resp.ClockOffset) if offsetInt < 0 { offsetInt = -offsetInt } + offset := time.Duration(offsetInt) return offset, nil diff --git a/obfuscated2/client_protocol.go b/obfuscated2/client_protocol.go new file mode 100644 index 0000000..3d8b6f9 --- /dev/null +++ b/obfuscated2/client_protocol.go @@ -0,0 +1,113 @@ +package obfuscated2 + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "errors" + "fmt" + "io" + "time" + + "mtg/antireplay" + "mtg/config" + "mtg/conntypes" + "mtg/protocol" + "mtg/stats" + "mtg/utils" + "mtg/wrappers/stream" +) + +const clientProtocolHandshakeTimeout = 10 * time.Second + +type ClientProtocol struct { + connectionType conntypes.ConnectionType + connectionProtocol conntypes.ConnectionProtocol + dc conntypes.DC +} + +func (c *ClientProtocol) ConnectionType() conntypes.ConnectionType { + return c.connectionType +} + +func (c *ClientProtocol) ConnectionProtocol() conntypes.ConnectionProtocol { + return c.connectionProtocol +} + +func (c *ClientProtocol) DC() conntypes.DC { + return c.dc +} + +func (c *ClientProtocol) Handshake(socket conntypes.StreamReadWriteCloser) (conntypes.StreamReadWriteCloser, error) { + fm, err := c.ReadFrame(socket) + if err != nil { + return nil, fmt.Errorf("cannot make a client handshake: %w", err) + } + + decHasher := sha256.New() + decHasher.Write(fm.Key()) // nolint: errcheck + decHasher.Write(config.C.Secret) // nolint: errcheck + decryptor := utils.MakeStreamCipher(decHasher.Sum(nil), fm.IV()) + + invertedFrame := fm.Invert() + encHasher := sha256.New() + encHasher.Write(invertedFrame.Key()) // nolint: errcheck + encHasher.Write(config.C.Secret) // nolint: errcheck + encryptor := utils.MakeStreamCipher(encHasher.Sum(nil), invertedFrame.IV()) + + decryptedFrame := Frame{} + decryptor.XORKeyStream(decryptedFrame.Bytes(), fm.Bytes()) + + magic := decryptedFrame.Magic() + + switch { + case bytes.Equal(magic, conntypes.ConnectionTagAbridged): + c.connectionType = conntypes.ConnectionTypeAbridged + case bytes.Equal(magic, conntypes.ConnectionTagIntermediate): + c.connectionType = conntypes.ConnectionTypeIntermediate + case bytes.Equal(magic, conntypes.ConnectionTagSecure): + c.connectionType = conntypes.ConnectionTypeSecure + default: + return nil, errors.New("unknown connection type") + } + + c.connectionProtocol = conntypes.ConnectionProtocolIPv4 + if socket.LocalAddr().IP.To4() == nil { + c.connectionProtocol = conntypes.ConnectionProtocolIPv6 + } + + buf := bytes.NewReader(decryptedFrame.DC()) + if err := binary.Read(buf, binary.LittleEndian, &c.dc); err != nil { + c.dc = conntypes.DCDefaultIdx + } + + replayKey := decryptedFrame.Unique() + if antireplay.Cache.HasObfuscated2(replayKey) { + stats.Stats.ReplayDetected() + return nil, errors.New("replay attack is detected") + } + + antireplay.Cache.AddObfuscated2(replayKey) + + return stream.NewObfuscated2(socket, encryptor, decryptor), nil +} + +func (c *ClientProtocol) ReadFrame(socket conntypes.StreamReader) (fm Frame, err error) { + if _, err = io.ReadFull(handshakeReader{socket}, fm.Bytes()); err != nil { + err = fmt.Errorf("cannot extract obfuscated2 frame: %w", err) + } + + return +} + +type handshakeReader struct { + parent conntypes.StreamReader +} + +func (h handshakeReader) Read(p []byte) (int, error) { + return h.parent.ReadTimeout(p, clientProtocolHandshakeTimeout) +} + +func MakeClientProtocol() protocol.ClientProtocol { + return &ClientProtocol{} +} diff --git a/obfuscated2/frame.go b/obfuscated2/frame.go index 33a8dd4..6df81c8 100644 --- a/obfuscated2/frame.go +++ b/obfuscated2/frame.go @@ -1,17 +1,5 @@ package obfuscated2 -import ( - "bytes" - "crypto/rand" - "encoding/binary" - "io" - - "github.com/juju/errors" - - "github.com/9seconds/mtg/mtproto" -) - -// [frameOffsetFirst:frameOffsetKey:frameOffsetIV:frameOffsetMagic:frameOffsetDC:frameOffsetEnd] const ( frameLenKey = 32 frameLenIV = 16 @@ -24,98 +12,43 @@ const ( frameOffsetMagic = frameOffsetIV + frameLenMagic frameOffsetDC = frameOffsetMagic + frameLenDC - FrameLen = 64 + frameLen = 64 ) -// Frame represents handshake frame. Telegram sends 64 bytes of obfuscated2 -// initialization data first. -// https://blog.susanka.eu/how-telegram-obfuscates-its-mtproto-traffic/ -type Frame []byte - -// Key returns AES encryption key. -func (f Frame) Key() []byte { - return f[frameOffsetFirst:frameOffsetKey] +// [frameOffsetFirst:frameOffsetKey:frameOffsetIV:frameOffsetMagic:frameOffsetDC:frameOffsetEnd] +type Frame struct { + data [frameLen]byte } -// IV returns AES encryption initialization vector -func (f Frame) IV() []byte { - return f[frameOffsetKey:frameOffsetIV] +func (f *Frame) Bytes() []byte { + return f.data[:] } -// Magic returns magic bytes from last 8 bytes of frame. Telegram checks -// for values there. If after decryption magic is not as expected, -// connection considered as failed. -func (f Frame) Magic() []byte { - return f[frameOffsetIV:frameOffsetMagic] +func (f *Frame) Key() []byte { + return f.data[frameOffsetFirst:frameOffsetKey] } -// DC returns number of datacenter IP client wants to use. -func (f Frame) DC() (n int16) { - buf := bytes.NewReader(f[frameOffsetMagic:frameOffsetDC]) - if err := binary.Read(buf, binary.LittleEndian, &n); err != nil { - n = 1 +func (f *Frame) IV() []byte { + return f.data[frameOffsetKey:frameOffsetIV] +} + +func (f *Frame) Magic() []byte { + return f.data[frameOffsetIV:frameOffsetMagic] +} + +func (f *Frame) DC() []byte { + return f.data[frameOffsetMagic:frameOffsetDC] +} + +func (f *Frame) Unique() []byte { + return f.data[frameOffsetFirst:frameOffsetDC] +} + +func (f *Frame) Invert() (nf Frame) { + nf = *f + for i := 0; i < frameLenKey+frameLenIV; i++ { + nf.data[frameOffsetFirst+i] = f.data[frameOffsetIV-1-i] } return } - -// ConnectionType identifies connection type of the handshake frame. -func (f Frame) ConnectionType() (mtproto.ConnectionType, error) { - return mtproto.ConnectionTagFromHandshake(f.Magic()) -} - -// Invert inverts frame for extracting encryption keys. Pkease check that link: -// https://blog.susanka.eu/how-telegram-obfuscates-its-mtproto-traffic/ -func (f Frame) Invert() Frame { - reversed := make(Frame, FrameLen) - copy(reversed, f) - - for i := 0; i < frameLenKey+frameLenIV; i++ { - reversed[frameOffsetFirst+i] = f[frameOffsetIV-1-i] - } - - return reversed -} - -// ExtractFrame extracts exact obfuscated2 handshake frame from given reader. -func ExtractFrame(conn io.Reader) (Frame, error) { - frame := make(Frame, FrameLen) - buf := bytes.NewBuffer(frame) - buf.Reset() - - if _, err := io.CopyN(buf, conn, FrameLen); err != nil { - return nil, errors.Annotate(err, "Cannot extract obfuscated header") - } - copy(frame, buf.Bytes()) - - return frame, nil -} - -func generateFrame(connectionType mtproto.ConnectionType) Frame { - frame := make(Frame, FrameLen) - - for { - if _, err := rand.Read(frame); err != nil { - continue - } - if frame[0] == 0xef { - continue - } - - val := (uint32(frame[3]) << 24) | (uint32(frame[2]) << 16) | (uint32(frame[1]) << 8) | uint32(frame[0]) - if val == 0x44414548 || val == 0x54534f50 || val == 0x20544547 || val == 0x4954504f || val == 0xeeeeeeee { - continue - } - - val = (uint32(frame[7]) << 24) | (uint32(frame[6]) << 16) | (uint32(frame[5]) << 8) | uint32(frame[4]) - if val == 0x00000000 { - continue - } - - // error has to be checked before calling this function - tag, _ := connectionType.Tag() // nolint: errcheck, gosec - copy(frame.Magic(), tag) - - return frame - } -} diff --git a/obfuscated2/frame_test.go b/obfuscated2/frame_test.go deleted file mode 100644 index 0983958..0000000 --- a/obfuscated2/frame_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package obfuscated2 - -import ( - "bytes" - "strconv" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/9seconds/mtg/mtproto" -) - -func TestFrameKey(t *testing.T) { - toCompare := make([]byte, 32) - for i := 0; i < 32; i++ { - toCompare[i] = byte(1) - } - - assert.Equal(t, toCompare, makeFrame().Key()) -} - -func TestFrameIV(t *testing.T) { - toCompare := make([]byte, 16) - for i := 0; i < 16; i++ { - toCompare[i] = byte(2) - } - - assert.Equal(t, toCompare, makeFrame().IV()) -} - -func TestFrameMagic(t *testing.T) { - toCompare := make([]byte, 4) - for i := 0; i < 4; i++ { - toCompare[i] = 0xee - } - - assert.Equal(t, toCompare, makeFrame().Magic()) -} - -func TestFrameDC(t *testing.T) { - assert.Equal(t, int16(771), makeFrame().DC()) -} - -func TestFrameValid(t *testing.T) { - frame := makeFrame() - connType, err := frame.ConnectionType() - assert.Nil(t, err) - assert.Equal(t, connType, mtproto.ConnectionTypeIntermediate) - - frame[8+32+16+2] = byte(3) - _, err = frame.ConnectionType() - assert.NotNil(t, err) -} - -func TestFrameDoubleInvert(t *testing.T) { - frame := makeFrame() - assert.True(t, bytes.Equal(frame, frame.Invert().Invert())) -} - -func TestFrameInvert(t *testing.T) { - frame := makeFrame() - reversed := frame.Invert() - - assert.Exactly(t, frame[:8], reversed[:8]) - assert.Exactly(t, frame[56:], reversed[56:]) - - toCompare := make([]byte, 48) - for i := 0; i < 48; i++ { - toCompare[i] = frame[55-i] - } - assert.Equal(t, []byte(reversed[8:56]), toCompare) -} - -func TestFrameGenerateValid(t *testing.T) { - validTests := []mtproto.ConnectionType{ - mtproto.ConnectionTypeIntermediate, - mtproto.ConnectionTypeAbridged, - } - for _, test := range validTests { - t.Run(strconv.Itoa(int(test)), func(tt *testing.T) { - frame := generateFrame(test) // nolint: scopelint - conType, err := frame.ConnectionType() - assert.Nil(tt, err) - assert.Equal(tt, conType, test) // nolint: scopelint - }) - } -} - -func makeFrame() Frame { - f := make(Frame, FrameLen) - - for i := 8; i < (8 + 32); i++ { - f[i] = byte(1) - } - for i := (8 + 32); i < (8 + 32 + 16); i++ { - f[i] = byte(2) - } - for i := (8 + 32 + 16); i < (8 + 32 + 16 + 4); i++ { - f[i] = 0xee - } - for i := (8 + 32 + 16 + 4); i < (8 + 32 + 16 + 4 + 2); i++ { - f[i] = byte(3) - } - - return f -} diff --git a/obfuscated2/obfuscated2.go b/obfuscated2/obfuscated2.go deleted file mode 100644 index 5fce5dc..0000000 --- a/obfuscated2/obfuscated2.go +++ /dev/null @@ -1,81 +0,0 @@ -package obfuscated2 - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/sha256" - - "github.com/juju/errors" - - "github.com/9seconds/mtg/mtproto" -) - -// Obfuscated2 contains AES CTR encryption and decryption streams -// for telegram connection. -type Obfuscated2 struct { - Decryptor cipher.Stream - Encryptor cipher.Stream -} - -// ParseObfuscated2ClientFrame parses client frame. Please check this link for -// details: http://telegra.ph/telegram-blocks-wtf-05-26 -// -// Beware, link above is in russian. -func ParseObfuscated2ClientFrame(secret []byte, frame Frame) (*Obfuscated2, *mtproto.ConnectionOpts, error) { - decHasher := sha256.New() - decHasher.Write(frame.Key()) // nolint: errcheck, gosec - decHasher.Write(secret) // nolint: errcheck, gosec - decryptor := makeStreamCipher(decHasher.Sum(nil), frame.IV()) - - invertedFrame := frame.Invert() - encHasher := sha256.New() - encHasher.Write(invertedFrame.Key()) // nolint: errcheck, gosec - encHasher.Write(secret) // nolint: errcheck, gosec - encryptor := makeStreamCipher(encHasher.Sum(nil), invertedFrame.IV()) - - decryptedFrame := make(Frame, FrameLen) - decryptor.XORKeyStream(decryptedFrame, frame) - connType, err := decryptedFrame.ConnectionType() - if err != nil { - return nil, nil, errors.Annotate(err, "Unknown protocol") - } - - obfs := &Obfuscated2{ - Decryptor: decryptor, - Encryptor: encryptor, - } - connOpts := &mtproto.ConnectionOpts{ - DC: decryptedFrame.DC(), - ConnectionType: connType, - } - - return obfs, connOpts, nil -} - -// MakeTelegramObfuscated2Frame creates new handshake frame to send to -// Telegram. -// https://blog.susanka.eu/how-telegram-obfuscates-its-mtproto-traffic/ -func MakeTelegramObfuscated2Frame(opts *mtproto.ConnectionOpts) (*Obfuscated2, Frame) { - frame := generateFrame(opts.ConnectionType) - - encryptor := makeStreamCipher(frame.Key(), frame.IV()) - decryptorFrame := frame.Invert() - decryptor := makeStreamCipher(decryptorFrame.Key(), decryptorFrame.IV()) - - copyFrame := make(Frame, FrameLen) - copy(copyFrame[:frameOffsetIV], frame[:frameOffsetIV]) - encryptor.XORKeyStream(frame, frame) - copy(frame[:frameOffsetIV], copyFrame[:frameOffsetIV]) - - obfs := &Obfuscated2{ - Decryptor: decryptor, - Encryptor: encryptor, - } - - return obfs, frame -} - -func makeStreamCipher(key, iv []byte) cipher.Stream { - block, _ := aes.NewCipher(key) // nolint: gosec - return cipher.NewCTR(block, iv) -} diff --git a/obfuscated2/obfuscated2_test.go b/obfuscated2/obfuscated2_test.go deleted file mode 100644 index 7ebe92f..0000000 --- a/obfuscated2/obfuscated2_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package obfuscated2 - -import ( - "crypto/sha256" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/9seconds/mtg/mtproto" -) - -func TestObfs2TelegramFrameDecrypt(t *testing.T) { - connOpts := &mtproto.ConnectionOpts{ - DC: 1, - ConnectionType: mtproto.ConnectionTypeIntermediate, - } - _, frame := MakeTelegramObfuscated2Frame(connOpts) - decryptor := makeStreamCipher(frame.Key(), frame.IV()) - - decrypted := make(Frame, FrameLen) - decryptor.XORKeyStream(decrypted, frame) - - _, err := decrypted.ConnectionType() - assert.Nil(t, err) -} - -func TestObfs2TelegramDecryptEncryptDecrypt(t *testing.T) { - connOpts := &mtproto.ConnectionOpts{ - DC: 1, - ConnectionType: mtproto.ConnectionTypeIntermediate, - } - obfs2, frame := MakeTelegramObfuscated2Frame(connOpts) - inverted := frame.Invert() - encryptor := makeStreamCipher(inverted.Key(), inverted.IV()) - - data := []byte{1, 2, 3} - encrypted := make([]byte, 3) - encryptor.XORKeyStream(encrypted, data) - decrypted := make([]byte, 3) - obfs2.Decryptor.XORKeyStream(decrypted, encrypted) - - assert.Equal(t, data, decrypted) -} - -func TestObfs2Full(t *testing.T) { - secret := []byte{1, 2, 3, 4, 5} - - clientFrame := generateFrame(mtproto.ConnectionTypeIntermediate) - clientHasher := sha256.New() - clientHasher.Write(clientFrame.Key()) // nolint: errcheck, gosec - clientHasher.Write(secret) // nolint: errcheck, gosec - clientKey := clientHasher.Sum(nil) - - encryptor := makeStreamCipher(clientKey, clientFrame.IV()) - encrypted := make(Frame, FrameLen) - encryptor.XORKeyStream(encrypted, clientFrame) - copy(encrypted[:56], clientFrame[:56]) - - invertedClientFrame := clientFrame.Invert() - clientHasher = sha256.New() - clientHasher.Write(invertedClientFrame.Key()) // nolint: errcheck, gosec - clientHasher.Write(secret) // nolint: errcheck, gosec - invertedClientKey := clientHasher.Sum(nil) - clientDecryptor := makeStreamCipher(invertedClientKey, invertedClientFrame.IV()) - - clientObfs, _, err := ParseObfuscated2ClientFrame(secret, encrypted) - assert.Nil(t, err) - - connOpts := &mtproto.ConnectionOpts{ - DC: 1, - ConnectionType: mtproto.ConnectionTypeIntermediate, - } - tgObfs, tgFrame := MakeTelegramObfuscated2Frame(connOpts) - tgDecryptor := makeStreamCipher(tgFrame.Key(), tgFrame.IV()) - decrypted := make(Frame, FrameLen) - tgDecryptor.XORKeyStream(decrypted, tgFrame) - _, err = decrypted.ConnectionType() - assert.Nil(t, err) - - tgInvertedFrame := tgFrame.Invert() - tgEncryptor := makeStreamCipher(tgInvertedFrame.Key(), tgInvertedFrame.IV()) - - message := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9} - tgEncryptedMessage := make([]byte, len(message)) - tgEncryptor.XORKeyStream(tgEncryptedMessage, message) - - tgEncDecryptedMessage := make([]byte, len(tgEncryptedMessage)) - tgObfs.Decryptor.XORKeyStream(tgEncDecryptedMessage, tgEncryptedMessage) - assert.Equal(t, message, tgEncDecryptedMessage) - - clientEncryptedMessage := make([]byte, len(tgEncDecryptedMessage)) - clientObfs.Encryptor.XORKeyStream(clientEncryptedMessage, tgEncDecryptedMessage) - finalMessage := make([]byte, len(clientEncryptedMessage)) - clientDecryptor.XORKeyStream(finalMessage, clientEncryptedMessage) - - assert.Equal(t, finalMessage, message) -} diff --git a/obfuscated2/telegram_protocol.go b/obfuscated2/telegram_protocol.go new file mode 100644 index 0000000..34ec469 --- /dev/null +++ b/obfuscated2/telegram_protocol.go @@ -0,0 +1,68 @@ +package obfuscated2 + +import ( + "crypto/rand" + "fmt" + + "mtg/conntypes" + "mtg/protocol" + "mtg/telegram" + "mtg/utils" + "mtg/wrappers/stream" +) + +func TelegramProtocol(req *protocol.TelegramRequest) (conntypes.StreamReadWriteCloser, error) { + conn, err := telegram.Direct.Dial(req.ClientProtocol.DC(), + req.ClientProtocol.ConnectionProtocol()) + if err != nil { + return nil, fmt.Errorf("cannot dial to telegram: %w", err) + } + + conn = stream.NewTimeout(conn) + conn = stream.NewCtx(req.Ctx, req.Cancel, conn) + fm := generateFrame(req.ClientProtocol) + data := fm.Bytes() + + encryptor := utils.MakeStreamCipher(fm.Key(), fm.IV()) + decryptedFrame := fm.Invert() + decryptor := utils.MakeStreamCipher(decryptedFrame.Key(), decryptedFrame.IV()) + + copyFrame := make([]byte, frameLen) + copy(copyFrame[:frameOffsetIV], data[:frameOffsetIV]) + encryptor.XORKeyStream(data, data) + copy(data[:frameOffsetIV], copyFrame[:frameOffsetIV]) + + if _, err := conn.Write(data); err != nil { + return nil, fmt.Errorf("cannot write handshake frame to telegram: %w", err) + } + + return stream.NewObfuscated2(conn, encryptor, decryptor), nil +} + +func generateFrame(cp protocol.ClientProtocol) (fm Frame) { + data := fm.Bytes() + + for { + if _, err := rand.Read(data); err != nil { + continue + } + + if data[0] == 0xef { + continue + } + + val := (uint32(data[3]) << 24) | (uint32(data[2]) << 16) | (uint32(data[1]) << 8) | uint32(data[0]) + if val == 0x44414548 || val == 0x54534f50 || val == 0x20544547 || val == 0x4954504f || val == 0xeeeeeeee { + continue + } + + val = (uint32(data[7]) << 24) | (uint32(data[6]) << 16) | (uint32(data[5]) << 8) | uint32(data[4]) + if val == 0x00000000 { + continue + } + + copy(fm.Magic(), cp.ConnectionType().Tag()) + + return + } +} diff --git a/protocol/interfaces.go b/protocol/interfaces.go new file mode 100644 index 0000000..bd44ce8 --- /dev/null +++ b/protocol/interfaces.go @@ -0,0 +1,12 @@ +package protocol + +import "mtg/conntypes" + +type ClientProtocol interface { + Handshake(conntypes.StreamReadWriteCloser) (conntypes.StreamReadWriteCloser, error) + ConnectionType() conntypes.ConnectionType + ConnectionProtocol() conntypes.ConnectionProtocol + DC() conntypes.DC +} + +type ClientProtocolMaker func() ClientProtocol diff --git a/protocol/request.go b/protocol/request.go new file mode 100644 index 0000000..8db368a --- /dev/null +++ b/protocol/request.go @@ -0,0 +1,18 @@ +package protocol + +import ( + "context" + + "go.uber.org/zap" + + "mtg/conntypes" +) + +type TelegramRequest struct { + Logger *zap.SugaredLogger + ClientConn conntypes.StreamReadWriteCloser + ConnID conntypes.ConnID + Ctx context.Context + Cancel context.CancelFunc + ClientProtocol ClientProtocol +} diff --git a/proxy/direct.go b/proxy/direct.go new file mode 100644 index 0000000..e4b5683 --- /dev/null +++ b/proxy/direct.go @@ -0,0 +1,49 @@ +package proxy + +import ( + "io" + "sync" + + "go.uber.org/zap" + + "mtg/conntypes" + "mtg/obfuscated2" + "mtg/protocol" +) + +const directPipeBufferSize = 1024 * 1024 + +func directConnection(request *protocol.TelegramRequest) error { + telegramConnRaw, err := obfuscated2.TelegramProtocol(request) + if err != nil { + return err + } + + telegramConn := telegramConnRaw.(conntypes.StreamReadWriteCloser) + + defer telegramConn.Close() + + wg := &sync.WaitGroup{} + wg.Add(2) + + go directPipe(telegramConn, request.ClientConn, wg, request.Logger) + + go directPipe(request.ClientConn, telegramConn, wg, request.Logger) + + wg.Wait() + + return nil +} + +func directPipe(dst io.WriteCloser, src io.ReadCloser, wg *sync.WaitGroup, logger *zap.SugaredLogger) { + defer func() { + dst.Close() + src.Close() + wg.Done() + }() + + buf := make([]byte, directPipeBufferSize) + if _, err := io.CopyBuffer(dst, src, buf); err != nil { + logger.Debugw("Cannot pump sockets", "error", err) + } +} diff --git a/proxy/middle.go b/proxy/middle.go new file mode 100644 index 0000000..97262ec --- /dev/null +++ b/proxy/middle.go @@ -0,0 +1,68 @@ +package proxy + +import ( + "sync" + + "go.uber.org/zap" + + "mtg/conntypes" + "mtg/protocol" + "mtg/wrappers/packetack" +) + +func middleConnection(request *protocol.TelegramRequest) { + telegramConn, err := packetack.NewProxy(request) + if err != nil { + request.Logger.Debugw("Cannot dial to Telegram", "error", err) + return + } + defer telegramConn.Close() + + var clientConn conntypes.PacketAckFullReadWriteCloser + + switch request.ClientProtocol.ConnectionType() { + case conntypes.ConnectionTypeAbridged: + clientConn = packetack.NewClientAbridged(request.ClientConn) + case conntypes.ConnectionTypeIntermediate: + clientConn = packetack.NewClientIntermediate(request.ClientConn) + case conntypes.ConnectionTypeSecure: + clientConn = packetack.NewClientIntermediateSecure(request.ClientConn) + default: + panic("unknown connection type") + } + + wg := &sync.WaitGroup{} + wg.Add(2) + + go middlePipe(telegramConn, clientConn, wg, request.Logger) + + go middlePipe(clientConn, telegramConn, wg, request.Logger) + + wg.Wait() +} + +func middlePipe(dst conntypes.PacketAckWriteCloser, + src conntypes.PacketAckReadCloser, + wg *sync.WaitGroup, + logger *zap.SugaredLogger) { + defer func() { + dst.Close() + src.Close() + wg.Done() + }() + + for { + acks := conntypes.ConnectionAcks{} + packet, err := src.Read(&acks) + + if err != nil { + logger.Debugw("Cannot read packet", "error", err) + return + } + + if err = dst.Write(packet, &acks); err != nil { + logger.Debugw("Cannot send packet", "error", err) + return + } + } +} diff --git a/proxy/proxy.go b/proxy/proxy.go index ecda26a..4c800dd 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -2,177 +2,97 @@ package proxy import ( "context" - "io" "net" - "sync" - "github.com/gofrs/uuid" - "github.com/juju/errors" "go.uber.org/zap" - "github.com/9seconds/mtg/antireplay" - "github.com/9seconds/mtg/client" - "github.com/9seconds/mtg/config" - "github.com/9seconds/mtg/mtproto" - "github.com/9seconds/mtg/stats" - "github.com/9seconds/mtg/telegram" - "github.com/9seconds/mtg/wrappers" + "mtg/config" + "mtg/conntypes" + "mtg/protocol" + "mtg/stats" + "mtg/utils" + "mtg/wrappers/stream" ) -// Proxy is a core of this program. type Proxy struct { - antiReplayCache antireplay.Cache - clientInit client.Init - tg telegram.Telegram - conf *config.Config + Logger *zap.SugaredLogger + Context context.Context + ClientProtocolMaker protocol.ClientProtocolMaker } -// Serve runs TCP proxy server. -func (p *Proxy) Serve() error { - lsock, err := net.Listen("tcp", p.conf.BindAddr()) - if err != nil { - return errors.Annotate(err, "Cannot create listen socket") - } +func (p *Proxy) Serve(listener net.Listener) { + doneChan := p.Context.Done() for { - if conn, err := lsock.Accept(); err != nil { - zap.S().Errorw("Cannot allocate incoming connection", "error", err) - } else { - go p.accept(conn) + conn, err := listener.Accept() + if err != nil { + select { + case <-doneChan: + return + default: + p.Logger.Errorw("Cannot allocate incoming connection", "error", err) + continue + } } + + go p.accept(conn) } } func (p *Proxy) accept(conn net.Conn) { - connID := uuid.Must(uuid.NewV4()).String() - log := zap.S().With("connection_id", connID).Named("main") - ctx, cancel := context.WithCancel(context.Background()) - defer func() { - cancel() - conn.Close() // nolint: errcheck, gosec - + conn.Close() if err := recover(); err != nil { - stats.NewCrash() - log.Errorw("Crash of accept handler", "error", err) + stats.Stats.Crash() + p.Logger.Errorw("Crash of accept handler", "error", err) } }() - log.Infow("Client connected", "addr", conn.RemoteAddr()) + connID := conntypes.NewConnID() + logger := p.Logger.With("connection_id", connID) + + if err := utils.InitTCP(conn); err != nil { + logger.Errorw("Cannot initialize client TCP connection", "error", err) + return + } + + ctx, cancel := context.WithCancel(p.Context) + defer cancel() + + clientConn := stream.NewClientConn(conn, connID) + clientConn = stream.NewCtx(ctx, cancel, clientConn) + clientConn = stream.NewTimeout(clientConn) + + defer clientConn.Close() + + clientProtocol := p.ClientProtocolMaker() + clientConn, err := clientProtocol.Handshake(clientConn) - clientConn, opts, err := p.clientInit(ctx, cancel, conn, connID, p.antiReplayCache, p.conf) if err != nil { - log.Errorw("Cannot initialize client connection", "error", err) - return - } - defer clientConn.(io.Closer).Close() // nolint: errcheck - - if p.conf.SecureOnly && opts.ConnectionType != mtproto.ConnectionTypeSecure { - log.Errorw("Proxy supports only secure connections", "connection_type", opts.ConnectionType) + logger.Warnw("Cannot perform client handshake", "error", err) return } - stats.ClientConnected(opts.ConnectionType, clientConn.RemoteAddr()) - defer stats.ClientDisconnected(opts.ConnectionType, clientConn.RemoteAddr()) + stats.Stats.ClientConnected(clientProtocol.ConnectionType(), clientConn.RemoteAddr()) + defer stats.Stats.ClientDisconnected(clientProtocol.ConnectionType(), clientConn.RemoteAddr()) + logger.Infow("Client connected", "addr", conn.RemoteAddr()) - serverConn, err := p.getTelegramConn(ctx, cancel, opts, connID) - if err != nil { - log.Errorw("Cannot initialize server connection", "error", err) - return + req := &protocol.TelegramRequest{ + Logger: logger, + ClientConn: clientConn, + ConnID: connID, + Ctx: ctx, + Cancel: cancel, + ClientProtocol: clientProtocol, } - defer serverConn.(io.Closer).Close() // nolint: errcheck - go func() { - <-ctx.Done() - serverConn.(io.Closer).Close() // nolint: gosec - clientConn.(io.Closer).Close() // nolint: gosec - }() + err = nil - wait := &sync.WaitGroup{} - wait.Add(2) - - if p.conf.UseMiddleProxy() { - clientPacket := clientConn.(wrappers.PacketReadWriteCloser) - serverPacket := serverConn.(wrappers.PacketReadWriteCloser) - go p.middlePipe(clientPacket, serverPacket, wait, &opts.ReadHacks) - p.middlePipe(serverPacket, clientPacket, wait, &opts.WriteHacks) + if len(config.C.AdTag) > 0 { + middleConnection(req) } else { - clientStream := clientConn.(wrappers.StreamReadWriteCloser) - serverStream := serverConn.(wrappers.StreamReadWriteCloser) - go p.directPipe(clientStream, serverStream, wait, p.conf.ReadBufferSize) - p.directPipe(serverStream, clientStream, wait, p.conf.WriteBufferSize) + err = directConnection(req) } - wait.Wait() - - log.Infow("Client disconnected", "addr", conn.RemoteAddr()) -} - -func (p *Proxy) getTelegramConn(ctx context.Context, cancel context.CancelFunc, - opts *mtproto.ConnectionOpts, connID string) (wrappers.Wrap, error) { - streamConn, err := p.tg.Dial(ctx, cancel, connID, opts) - if err != nil { - return nil, errors.Annotate(err, "Cannot dial to Telegram") - } - - packetConn, err := p.tg.Init(opts, streamConn) - if err != nil { - return nil, errors.Annotate(err, "Cannot handshake telegram") - } - - return packetConn, nil -} - -func (p *Proxy) middlePipe(src wrappers.PacketReadCloser, dst io.Writer, wait *sync.WaitGroup, hacks *mtproto.Hacks) { - defer wait.Done() - - for { - hacks.SimpleAck = false - hacks.QuickAck = false - - packet, err := src.Read() - if err != nil { - src.Logger().Warnw("Cannot read packet", "error", err) - return - } - if _, err = dst.Write(packet); err != nil { - src.Logger().Warnw("Cannot write packet", "error", err) - return - } - } -} - -func (p *Proxy) directPipe(src wrappers.StreamReadCloser, dst io.Writer, wait *sync.WaitGroup, bufferSize int) { - defer wait.Done() - - buffer := make([]byte, bufferSize) - if _, err := io.CopyBuffer(dst, src, buffer); err != nil { - src.Logger().Warnw("Cannot pump sockets", "error", err) - } -} - -// NewProxy returns new proxy instance. -func NewProxy(conf *config.Config) (*Proxy, error) { - var clientInit client.Init - var tg telegram.Telegram - - cache, err := antireplay.NewCache(conf) - if err != nil { - return nil, errors.Annotate(err, "Cannot make proxy") - } - - if conf.UseMiddleProxy() { - clientInit = client.MiddleInit - tg = telegram.NewMiddleTelegram(conf) - } else { - clientInit = client.DirectInit - tg = telegram.NewDirectTelegram(conf) - } - - return &Proxy{ - antiReplayCache: cache, - conf: conf, - clientInit: clientInit, - tg: tg, - }, nil + logger.Infow("Client disconnected", "error", err, "addr", conn.RemoteAddr()) } diff --git a/rlimit/rlimit.go b/rlimit/rlimit.go deleted file mode 100644 index d2d3e07..0000000 --- a/rlimit/rlimit.go +++ /dev/null @@ -1,25 +0,0 @@ -//+build !windows - -package rlimit - -import ( - "github.com/juju/errors" - "golang.org/x/sys/unix" -) - -func Set() (err error) { - rLimit := unix.Rlimit{} - err = unix.Getrlimit(unix.RLIMIT_NOFILE, &rLimit) - if err != nil { - err = errors.Annotate(err, "Cannot get rlimit") - return - } - rLimit.Cur = rLimit.Max - - err = unix.Setrlimit(unix.RLIMIT_NOFILE, &rLimit) - if err != nil { - err = errors.Annotate(err, "Cannot set rlimit") - } - - return -} diff --git a/rlimit/rlimit_windows.go b/rlimit/rlimit_windows.go deleted file mode 100644 index fc938a5..0000000 --- a/rlimit/rlimit_windows.go +++ /dev/null @@ -1,7 +0,0 @@ -// +build windows - -package rlimit - -func Set() (err error) { - return -} diff --git a/run-mtg.sh b/run-mtg.sh deleted file mode 100755 index 60459c0..0000000 --- a/run-mtg.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -set -eu -o pipefail - -IMAGE_NAME="nineseconds/mtg" -CONTAINER_NAME="mtg" -SECRET_PATH="$HOME/.mtg.secret" -PROXY_PORT=444 -STAT_PORT=3129 - -[[ -e "$SECRET_PATH" ]] || ( - openssl rand -hex 16 > "$SECRET_PATH" - chmod 0400 "$SECRET_PATH" -) - -docker pull "$IMAGE_NAME" -docker ps --filter "Name=$CONTAINER_NAME" -aq | xargs -r docker rm -fv -docker run \ - -d \ - --name "$CONTAINER_NAME" \ - --sysctl 'net.ipv4.ip_local_port_range=10000 65000' \ - --sysctl net.ipv4.tcp_congestion_control=bbr \ - --sysctl net.ipv4.tcp_fastopen=3 \ - --sysctl net.ipv4.tcp_fin_timeout=30 \ - --sysctl net.ipv4.tcp_max_syn_backlog=4096 \ - --sysctl net.ipv4.tcp_max_tw_buckets=5000 \ - --sysctl net.ipv4.tcp_mtu_probing=1 \ - --sysctl 'net.ipv4.tcp_rmem=4096 87380 67108864' \ - --sysctl net.ipv4.tcp_syncookies=1 \ - --sysctl net.ipv4.tcp_tw_reuse=1 \ - --sysctl 'net.ipv4.tcp_wmem=4096 65536 67108864' \ - --ulimit nofile=51200:51200 \ - --restart=unless-stopped \ - -p $PROXY_PORT:3128 \ - -p $STAT_PORT:3129 \ - "$IMAGE_NAME" "$(cat "$SECRET_PATH")" diff --git a/stats/channels.go b/stats/channels.go deleted file mode 100644 index f7317db..0000000 --- a/stats/channels.go +++ /dev/null @@ -1,76 +0,0 @@ -package stats - -import ( - "net" - - "github.com/9seconds/mtg/mtproto" -) - -const ( - connectionsChanLength = 10 - trafficChanLength = 10 -) - -var ( - crashesChan = make(chan struct{}) - statsChan = make(chan chan<- Stats) - connectionsChan = make(chan connectionData, connectionsChanLength) - trafficChan = make(chan trafficData, trafficChanLength) -) - -type connectionData struct { - connectionType mtproto.ConnectionType - connected bool - addr *net.TCPAddr -} - -type trafficData struct { - traffic int - ingress bool -} - -// NewCrash indicates new crash. -func NewCrash() { - crashesChan <- struct{}{} -} - -// ClientConnected indicates that new client was connected. -func ClientConnected(connectionType mtproto.ConnectionType, addr *net.TCPAddr) { - connectionsChan <- connectionData{ - connectionType: connectionType, - addr: addr, - connected: true, - } -} - -// ClientDisconnected indicates that client was disconnected. -func ClientDisconnected(connectionType mtproto.ConnectionType, addr *net.TCPAddr) { - connectionsChan <- connectionData{ - connectionType: connectionType, - addr: addr, - connected: false, - } -} - -// IngressTraffic accounts new ingress traffic. -func IngressTraffic(traffic int) { - trafficChan <- trafficData{ - traffic: traffic, - ingress: true, - } -} - -// EgressTraffic accounts new ingress traffic. -func EgressTraffic(traffic int) { - trafficChan <- trafficData{ - traffic: traffic, - ingress: false, - } -} - -// GetStats returns a snapshot of Stats instance. -func GetStats() Stats { - rpcChan := make(chan Stats) - statsChan <- rpcChan - return <-rpcChan -} diff --git a/stats/init.go b/stats/init.go deleted file mode 100644 index ba3640f..0000000 --- a/stats/init.go +++ /dev/null @@ -1,28 +0,0 @@ -package stats - -import ( - "github.com/juju/errors" - - "github.com/9seconds/mtg/config" -) - -// Init initializes stats subsystem. -func Init(conf *config.Config) error { - if conf.StatsD.Enabled { - client, err := newStatsd(conf) - if err != nil { - return errors.Annotate(err, "Cannot initialize statsd client") - } - go client.run() - } - prometheus, err := newPrometheus(conf) - if err != nil { - return errors.Annotate(err, "Cannot initialize prometheus client") - } - go prometheus.run() - - go NewStats(conf).start() - go startServer(conf, prometheus.getHTTPHandler()) - - return nil -} diff --git a/stats/interfaces.go b/stats/interfaces.go new file mode 100644 index 0000000..345bdaa --- /dev/null +++ b/stats/interfaces.go @@ -0,0 +1,50 @@ +package stats + +import ( + "net" + + "mtg/conntypes" +) + +type IngressTrafficInterface interface { + IngressTraffic(int) +} + +type EgressTrafficInterface interface { + EgressTraffic(int) +} + +type ClientConnectedInterface interface { + ClientConnected(conntypes.ConnectionType, *net.TCPAddr) +} + +type ClientDisconnectedInterface interface { + ClientDisconnected(conntypes.ConnectionType, *net.TCPAddr) +} + +type TelegramConnectedInterface interface { + TelegramConnected(conntypes.DC, *net.TCPAddr) +} + +type TelegramDisconnectedInterface interface { + TelegramDisconnected(conntypes.DC, *net.TCPAddr) +} + +type CrashInterface interface { + Crash() +} + +type ReplayDetectedInterface interface { + ReplayDetected() +} + +type Interface interface { + IngressTrafficInterface + EgressTrafficInterface + ClientConnectedInterface + ClientDisconnectedInterface + TelegramConnectedInterface + TelegramDisconnectedInterface + CrashInterface + ReplayDetectedInterface +} diff --git a/stats/multi_stats.go b/stats/multi_stats.go new file mode 100644 index 0000000..e7728d3 --- /dev/null +++ b/stats/multi_stats.go @@ -0,0 +1,57 @@ +package stats + +import ( + "net" + + "mtg/conntypes" +) + +type multiStats []Interface + +func (m multiStats) IngressTraffic(traffic int) { + for i := range m { + go m[i].IngressTraffic(traffic) + } +} + +func (m multiStats) EgressTraffic(traffic int) { + for i := range m { + go m[i].EgressTraffic(traffic) + } +} + +func (m multiStats) ClientConnected(connectionType conntypes.ConnectionType, addr *net.TCPAddr) { + for i := range m { + go m[i].ClientConnected(connectionType, addr) + } +} + +func (m multiStats) ClientDisconnected(connectionType conntypes.ConnectionType, addr *net.TCPAddr) { + for i := range m { + go m[i].ClientDisconnected(connectionType, addr) + } +} + +func (m multiStats) TelegramConnected(dc conntypes.DC, addr *net.TCPAddr) { + for i := range m { + go m[i].TelegramConnected(dc, addr) + } +} + +func (m multiStats) TelegramDisconnected(dc conntypes.DC, addr *net.TCPAddr) { + for i := range m { + go m[i].TelegramDisconnected(dc, addr) + } +} + +func (m multiStats) Crash() { + for i := range m { + go m[i].Crash() + } +} + +func (m multiStats) ReplayDetected() { + for i := range m { + go m[i].ReplayDetected() + } +} diff --git a/stats/prometheus.go b/stats/prometheus.go deleted file mode 100644 index 00257fa..0000000 --- a/stats/prometheus.go +++ /dev/null @@ -1,91 +0,0 @@ -package stats - -import ( - "net/http" - "time" - - "github.com/juju/errors" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - - "github.com/9seconds/mtg/config" -) - -const prometheusPollTime = time.Second - -type prometheusExporter struct { - registry prometheus.Gatherer - - connections *prometheus.GaugeVec - traffic *prometheus.GaugeVec - speed *prometheus.GaugeVec - crashes prometheus.Gauge -} - -func (p *prometheusExporter) run() { - for range time.Tick(prometheusPollTime) { - instance := GetStats() - - p.connections.WithLabelValues("abridged", "v4").Set(float64(instance.Connections.Abridged.IPv4)) - p.connections.WithLabelValues("abridged", "v6").Set(float64(instance.Connections.Abridged.IPv6)) - p.connections.WithLabelValues("intermediate", "v4").Set(float64(instance.Connections.Intermediate.IPv4)) - p.connections.WithLabelValues("intermediate", "v6").Set(float64(instance.Connections.Intermediate.IPv6)) - p.connections.WithLabelValues("secure", "v4").Set(float64(instance.Connections.Secure.IPv4)) - p.connections.WithLabelValues("secure", "v6").Set(float64(instance.Connections.Secure.IPv6)) - p.traffic.WithLabelValues("ingress").Set(float64(instance.Traffic.ingress)) - p.traffic.WithLabelValues("egress").Set(float64(instance.Traffic.egress)) - p.speed.WithLabelValues("ingress").Set(float64(instance.Speed.ingress)) - p.speed.WithLabelValues("egress").Set(float64(instance.Speed.egress)) - p.crashes.Set(float64(instance.Crashes)) - } -} - -func (p *prometheusExporter) getHTTPHandler() http.Handler { - return promhttp.HandlerFor(p.registry, promhttp.HandlerOpts{}) -} - -func newPrometheus(conf *config.Config) (*prometheusExporter, error) { - registry := prometheus.NewRegistry() - - connections := prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: conf.Prometheus.Prefix, - Name: "connections", - Help: "Current number of connections to the proxy.", - }, []string{"type", "protocol"}) - traffic := prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: conf.Prometheus.Prefix, - Name: "traffic", - Help: "Traffic passed through the proxy in bytes.", - }, []string{"direction"}) - speed := prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: conf.Prometheus.Prefix, - Name: "speed", - Help: "Current throughput in bytes per second.", - }, []string{"direction"}) - crashes := prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: conf.Prometheus.Prefix, - Name: "crashes", - Help: "How many crashes happened.", - }) - - if err := registry.Register(connections); err != nil { - return nil, errors.Annotate(err, "Cannot register connections collector") - } - if err := registry.Register(traffic); err != nil { - return nil, errors.Annotate(err, "cannot register traffic collector") - } - if err := registry.Register(speed); err != nil { - return nil, errors.Annotate(err, "cannot register speed collector") - } - if err := registry.Register(crashes); err != nil { - return nil, errors.Annotate(err, "cannot register crashes collector") - } - - return &prometheusExporter{ - registry: registry, - connections: connections, - traffic: traffic, - speed: speed, - crashes: crashes, - }, nil -} diff --git a/stats/server.go b/stats/server.go deleted file mode 100644 index cc5f659..0000000 --- a/stats/server.go +++ /dev/null @@ -1,40 +0,0 @@ -package stats - -import ( - "encoding/json" - "net/http" - - "go.uber.org/zap" - - "github.com/9seconds/mtg/config" -) - -func startServer(conf *config.Config, prometheusHandler http.Handler) { - log := zap.S().Named("stats") - - http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - - first, err := json.Marshal(GetStats()) - if err != nil { - log.Errorw("Cannot encode json", "error", err) - http.Error(w, "Internal server error", 500) - return - } - - interim := map[string]interface{}{} - json.Unmarshal(first, &interim) // nolint: errcheck, gosec - - encoder := json.NewEncoder(w) - encoder.SetEscapeHTML(false) - encoder.SetIndent("", " ") - if err = encoder.Encode(interim); err != nil { - log.Errorw("Cannot encode json", "error", err) - } - }) - http.Handle("/prometheus/", prometheusHandler) - - if err := http.ListenAndServe(conf.StatAddr(), nil); err != nil { - log.Fatalw("Stats server has been stopped", "error", err) - } -} diff --git a/stats/stats.go b/stats/stats.go index 686163d..b4ccad7 100644 --- a/stats/stats.go +++ b/stats/stats.go @@ -1,175 +1,52 @@ package stats import ( - "encoding/json" + "context" "fmt" - "strconv" - "time" + "net" + "net/http" - humanize "github.com/dustin/go-humanize" - - "github.com/9seconds/mtg/config" - "github.com/9seconds/mtg/mtproto" + "mtg/config" ) -type uptime time.Time +var Stats Interface -func (u uptime) MarshalJSON() ([]byte, error) { - duration := time.Since(time.Time(u)) - value := map[string]string{ - "seconds": strconv.Itoa(int(duration.Seconds())), - "human": humanize.Time(time.Time(u)), +func Init(ctx context.Context) error { + mux := http.NewServeMux() + + instancePrometheus, err := newStatsPrometheus(mux) + if err != nil { + return fmt.Errorf("cannot initialize prometheus: %w", err) } - return json.Marshal(value) -} + stats := []Interface{instancePrometheus} -type connectionType struct { - IPv6 uint32 `json:"ipv6"` - IPv4 uint32 `json:"ipv4"` -} - -type baseConnections struct { - All connectionType `json:"all"` - Abridged connectionType `json:"abridged"` - Intermediate connectionType `json:"intermediate"` - Secure connectionType `json:"secure"` -} - -type connections struct { - baseConnections -} - -func (c connections) MarshalJSON() ([]byte, error) { - c.All.IPv4 = c.Abridged.IPv4 + c.Intermediate.IPv4 + c.Secure.IPv4 - c.All.IPv6 = c.Abridged.IPv6 + c.Intermediate.IPv6 + c.Secure.IPv6 - - return json.Marshal(c.baseConnections) -} - -type traffic struct { - ingress uint64 - egress uint64 -} - -func (t *traffic) dumpValue(value uint64) map[string]interface{} { - return map[string]interface{}{ - "bytes": value, - "human": humanize.Bytes(value), - } -} - -func (t traffic) MarshalJSON() ([]byte, error) { - value := map[string]map[string]interface{}{ - "ingress": t.dumpValue(t.ingress), - "egress": t.dumpValue(t.egress), - } - - return json.Marshal(value) -} - -type speed struct { - ingress uint64 - egress uint64 -} - -func (s *speed) dumpValue(value uint64) map[string]interface{} { - return map[string]interface{}{ - "bytes/s": value, - "human": fmt.Sprintf("%s/s", humanize.Bytes(value)), - } -} - -func (s speed) MarshalJSON() ([]byte, error) { - value := map[string]map[string]interface{}{ - "ingress": s.dumpValue(s.ingress), - "egress": s.dumpValue(s.egress), - } - - return json.Marshal(value) -} - -// Stats represents a statistics of the proxy. -type Stats struct { - URLs config.IPURLs `json:"urls"` - Connections connections `json:"connections"` - Traffic traffic `json:"traffic"` - Speed speed `json:"speed"` - Uptime uptime `json:"uptime"` - Crashes uint32 `json:"crashes"` - - previousTraffic traffic -} - -func (s *Stats) start() { - speedChan := time.Tick(time.Second) - - for { - select { - case <-speedChan: - s.handleSpeed() - case event := <-trafficChan: - s.handleTraffic(event) - case event := <-connectionsChan: - s.handleConnection(event) - case getStatsChan := <-statsChan: - s.handleGetStats(getStatsChan) - case <-crashesChan: - s.handleCrash() + if config.C.StatsdAddr != nil { + instanceStatsd, err := newStatsStatsd() + if err != nil { + return fmt.Errorf("cannot inialize statsd: %w", err) } - } -} -func (s *Stats) handleTraffic(evt trafficData) { - if evt.ingress { - s.Traffic.ingress += uint64(evt.traffic) - } else { - s.Traffic.egress += uint64(evt.traffic) - } -} - -func (s *Stats) handleSpeed() { - s.Speed.ingress = s.Traffic.ingress - s.previousTraffic.ingress - s.Speed.egress = s.Traffic.egress - s.previousTraffic.egress - s.previousTraffic.ingress = s.Traffic.ingress - s.previousTraffic.egress = s.Traffic.egress -} - -func (s *Stats) handleConnection(evt connectionData) { - var inc uint32 = 1 - if !evt.connected { - inc = ^uint32(0) + stats = append(stats, instanceStatsd) } - var conn *connectionType - switch evt.connectionType { - case mtproto.ConnectionTypeAbridged: - conn = &s.Connections.Abridged - case mtproto.ConnectionTypeSecure: - conn = &s.Connections.Secure - default: - conn = &s.Connections.Intermediate + listener, err := net.Listen("tcp", config.C.StatsBind.String()) + if err != nil { + return fmt.Errorf("cannot initialize stats server: %w", err) } - if evt.addr.IP.To4() != nil { - conn.IPv4 += inc - } else { - conn.IPv6 += inc + srv := http.Server{ + Handler: mux, } -} -func (s *Stats) handleGetStats(getStatsChan chan<- Stats) { - getStatsChan <- *s -} + go srv.Serve(listener) // nolint: errcheck -func (s *Stats) handleCrash() { - s.Crashes++ -} + go func() { + <-ctx.Done() + srv.Shutdown(context.Background()) // nolint: errcheck + }() -// NewStats creates a new instance of Stats structure. -func NewStats(conf *config.Config) *Stats { - return &Stats{ - URLs: conf.GetURLs(), - Uptime: uptime(time.Now()), - } + Stats = multiStats(stats) + + return nil } diff --git a/stats/stats_prometheus.go b/stats/stats_prometheus.go new file mode 100644 index 0000000..509f864 --- /dev/null +++ b/stats/stats_prometheus.go @@ -0,0 +1,146 @@ +package stats + +import ( + "fmt" + "net" + "net/http" + "strconv" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + + "mtg/config" + "mtg/conntypes" +) + +type statsPrometheus struct { + connections *prometheus.GaugeVec + telegramConnections *prometheus.GaugeVec + traffic *prometheus.GaugeVec + crashes prometheus.Gauge + replayAttacks prometheus.Counter +} + +func (s *statsPrometheus) IngressTraffic(traffic int) { + s.traffic.WithLabelValues("ingress").Add(float64(traffic)) +} + +func (s *statsPrometheus) EgressTraffic(traffic int) { + s.traffic.WithLabelValues("egress").Add(float64(traffic)) +} + +func (s *statsPrometheus) ClientConnected(connectionType conntypes.ConnectionType, addr *net.TCPAddr) { + s.changeConnections(connectionType, addr, 1.0) +} + +func (s *statsPrometheus) ClientDisconnected(connectionType conntypes.ConnectionType, addr *net.TCPAddr) { + s.changeConnections(connectionType, addr, -1.0) +} + +func (s *statsPrometheus) changeConnections(connectionType conntypes.ConnectionType, + addr *net.TCPAddr, + increment float64) { + labels := [...]string{ + "intermediate", + "ipv4", + } + + switch connectionType { + case conntypes.ConnectionTypeAbridged: + labels[0] = "abridged" + case conntypes.ConnectionTypeSecure: + labels[0] = "secured" + } + + if addr.IP.To4() == nil { + labels[1] = "ipv6" // nolint: goconst + } + + s.connections.WithLabelValues(labels[:]...).Add(increment) +} + +func (s *statsPrometheus) TelegramConnected(dc conntypes.DC, addr *net.TCPAddr) { + s.changeTelegramConnections(dc, addr, 1.0) +} + +func (s *statsPrometheus) TelegramDisconnected(dc conntypes.DC, addr *net.TCPAddr) { + s.changeTelegramConnections(dc, addr, -1.0) +} + +func (s *statsPrometheus) changeTelegramConnections(dc conntypes.DC, addr *net.TCPAddr, increment float64) { + labels := [...]string{ + strconv.Itoa(int(dc)), + "ipv4", + } + + if addr.IP.To4() == nil { + labels[1] = "ipv6" + } + + s.telegramConnections.WithLabelValues(labels[:]...).Add(increment) +} + +func (s *statsPrometheus) Crash() { + s.crashes.Inc() +} + +func (s *statsPrometheus) ReplayDetected() { + s.replayAttacks.Inc() +} + +func newStatsPrometheus(mux *http.ServeMux) (Interface, error) { + registry := prometheus.NewPedanticRegistry() + + instance := &statsPrometheus{ + connections: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: config.C.StatsNamespace, + Name: "connections", + Help: "Current number of client connections to the proxy.", + }, []string{"type", "protocol"}), + telegramConnections: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: config.C.StatsNamespace, + Name: "telegram_connections", + Help: "Current number of telegram connections established by this proxy.", + }, []string{"dc", "protocol"}), + traffic: prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: config.C.StatsNamespace, + Name: "traffic", + Help: "Traffic passed through the proxy in bytes.", + }, []string{"direction"}), + crashes: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: config.C.StatsNamespace, + Name: "crashes", + Help: "How many crashes happened.", + }), + replayAttacks: prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: config.C.StatsNamespace, + Name: "replay_attacks", + Help: "How many replay attacks were prevented.", + }), + } + + if err := registry.Register(instance.connections); err != nil { + return nil, fmt.Errorf("cannot register metrics for connections: %w", err) + } + + if err := registry.Register(instance.telegramConnections); err != nil { + return nil, fmt.Errorf("cannot register metrics for telegram connections: %w", err) + } + + if err := registry.Register(instance.traffic); err != nil { + return nil, fmt.Errorf("cannot register metrics for traffic: %w", err) + } + + if err := registry.Register(instance.crashes); err != nil { + return nil, fmt.Errorf("cannot register metrics for crashes: %w", err) + } + + if err := registry.Register(instance.replayAttacks); err != nil { + return nil, fmt.Errorf("cannot register metrics for replays: %w", err) + } + + handler := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) + mux.Handle("/", handler) + + return instance, nil +} diff --git a/stats/stats_statsd.go b/stats/stats_statsd.go new file mode 100644 index 0000000..0261f0b --- /dev/null +++ b/stats/stats_statsd.go @@ -0,0 +1,111 @@ +package stats + +import ( + "fmt" + "net" + "strconv" + "strings" + + "gopkg.in/alexcesaro/statsd.v2" + + "mtg/config" + "mtg/conntypes" +) + +type statsStatsd struct { + client *statsd.Client +} + +func (s *statsStatsd) IngressTraffic(traffic int) { + s.client.Count("traffic.ingress", traffic) +} + +func (s *statsStatsd) EgressTraffic(traffic int) { + s.client.Count("traffic.egress", traffic) +} + +func (s *statsStatsd) ClientConnected(connectionType conntypes.ConnectionType, addr *net.TCPAddr) { + s.changeConnections(connectionType, addr, 1) +} + +func (s *statsStatsd) ClientDisconnected(connectionType conntypes.ConnectionType, addr *net.TCPAddr) { + s.changeConnections(connectionType, addr, -1) +} + +func (s *statsStatsd) changeConnections(connectionType conntypes.ConnectionType, addr *net.TCPAddr, value int) { + labels := [...]string{ + "connections", + "intermediate", + "ipv4", + } + + switch connectionType { + case conntypes.ConnectionTypeAbridged: + labels[1] = "abridged" + case conntypes.ConnectionTypeSecure: + labels[1] = "secured" + } + + if addr.IP.To4() == nil { + labels[2] = "ipv6" + } + + s.client.Count(strings.Join(labels[:], "."), value) +} + +func (s *statsStatsd) TelegramConnected(dc conntypes.DC, addr *net.TCPAddr) { + s.changeTelegramConnections(dc, addr, 1) +} + +func (s *statsStatsd) TelegramDisconnected(dc conntypes.DC, addr *net.TCPAddr) { + s.changeTelegramConnections(dc, addr, -1) +} + +func (s *statsStatsd) changeTelegramConnections(dc conntypes.DC, addr *net.TCPAddr, value int) { + labels := [...]string{ + "telegram_connections", + strconv.Itoa(int(dc)), + "ipv4", + } + + if addr.IP.To4() == nil { + labels[2] = "ipv6" + } + + s.client.Count(strings.Join(labels[:], "."), value) +} + +func (s *statsStatsd) Crash() { + s.client.Increment("crashes") +} + +func (s *statsStatsd) ReplayDetected() { + s.client.Increment("replay_attacks") +} + +func newStatsStatsd() (Interface, error) { + options := []statsd.Option{ + statsd.Prefix(config.C.StatsNamespace), + statsd.Network(config.C.StatsdNetwork), + statsd.Address(config.C.StatsBind.String()), + statsd.TagsFormat(config.C.StatsdTagsFormat), + } + + if len(config.C.StatsdTags) > 0 { + tags := make([]string, len(config.C.StatsdTags)*2) + for k, v := range config.C.StatsdTags { + tags = append(tags, k, v) + } + + options = append(options, statsd.Tags(tags...)) + } + + client, err := statsd.New(options...) + if err != nil { + return nil, fmt.Errorf("cannot initialize a client: %w", err) + } + + return &statsStatsd{ + client: client, + }, nil +} diff --git a/stats/statsd.go b/stats/statsd.go deleted file mode 100644 index 5745889..0000000 --- a/stats/statsd.go +++ /dev/null @@ -1,77 +0,0 @@ -package stats - -import ( - "time" - - "github.com/juju/errors" - statsd "gopkg.in/alexcesaro/statsd.v2" - - "github.com/9seconds/mtg/config" -) - -const ( - statsdConnectionsAbridgedV4 = "connections.abridged.ipv4" - statsdConnectionsAbridgedV6 = "connections.abridged.ipv6" - - statsdConnectionsIntermediateV4 = "connections.intermediate.ipv4" - statsdConnectionsIntermediateV6 = "connections.intermediate.ipv6" - - statsdConnectionsSecureV4 = "connections.secure.ipv4" - statsdConnectionsSecureV6 = "connections.secure.ipv6" - - statsdTrafficIngress = "traffic.ingress" - statsdTrafficEgress = "traffic.egress" - - statsdSpeedIngress = "speed.ingress" - statsdSpeedEgress = "speed.egress" - - statsdCrashes = "crashes" -) - -const statsdPollTime = time.Second - -type statsdExporter struct { - client *statsd.Client -} - -func (s *statsdExporter) run() { - for range time.Tick(statsdPollTime) { - instance := GetStats() - - s.client.Gauge(statsdConnectionsAbridgedV4, instance.Connections.Abridged.IPv4) - s.client.Gauge(statsdConnectionsAbridgedV6, instance.Connections.Abridged.IPv6) - s.client.Gauge(statsdConnectionsIntermediateV4, instance.Connections.Intermediate.IPv4) - s.client.Gauge(statsdConnectionsIntermediateV6, instance.Connections.Intermediate.IPv6) - s.client.Gauge(statsdConnectionsSecureV4, instance.Connections.Secure.IPv4) - s.client.Gauge(statsdConnectionsSecureV6, instance.Connections.Secure.IPv6) - s.client.Gauge(statsdTrafficIngress, instance.Traffic.ingress) - s.client.Gauge(statsdTrafficEgress, instance.Traffic.egress) - s.client.Gauge(statsdSpeedIngress, instance.Speed.ingress) - s.client.Gauge(statsdSpeedEgress, instance.Speed.egress) - s.client.Gauge(statsdCrashes, instance.Crashes) - } -} - -func newStatsd(conf *config.Config) (*statsdExporter, error) { - options := []statsd.Option{ - statsd.Network(conf.StatsD.Addr.Network()), - statsd.Address(conf.StatsD.Addr.String()), - statsd.Prefix(conf.StatsD.Prefix), - } - - if conf.StatsD.TagsFormat > 0 { - options = append(options, statsd.TagsFormat(conf.StatsD.TagsFormat)) - tags := make([]string, len(conf.StatsD.Tags)*2) - for k, v := range conf.StatsD.Tags { - tags = append(tags, k, v) - } - options = append(options, statsd.Tags(tags...)) - } - - client, err := statsd.New(options...) - if err != nil { - return nil, errors.Annotate(err, "Cannot create statsd client") - } - - return &statsdExporter{client: client}, nil -} diff --git a/telegram/api/addresses.go b/telegram/api/addresses.go new file mode 100644 index 0000000..bbb2116 --- /dev/null +++ b/telegram/api/addresses.go @@ -0,0 +1,109 @@ +package api + +import ( + "bufio" + "fmt" + "net" + "regexp" + "strconv" + "strings" + + "mtg/conntypes" +) + +const ( + addressesURLV4 = "https://core.telegram.org/getProxyConfig" // nolint: gas + addressesURLV6 = "https://core.telegram.org/getProxyConfigV6" // nolint: gas +) + +var addressesProxyForSplitter = regexp.MustCompile(`\s+`) + +func AddressesV4() (map[conntypes.DC][]string, conntypes.DC, error) { + return getAddresses(addressesURLV4) +} + +func AddressesV6() (map[conntypes.DC][]string, conntypes.DC, error) { + return getAddresses(addressesURLV6) +} + +func getAddresses(url string) (map[conntypes.DC][]string, conntypes.DC, error) { + resp, err := request(url) + if err != nil { + return nil, 0, fmt.Errorf("cannot get http response: %w", err) + } + + defer resp.Close() + + scanner := bufio.NewScanner(resp) + data := map[conntypes.DC][]string{} + defaultDC := conntypes.DCDefaultIdx + + for scanner.Scan() { + text := strings.TrimSpace(scanner.Text()) + + switch { + case strings.HasPrefix(text, "#"): + continue + case strings.HasPrefix(text, "proxy_for"): + addr, idx, err := addressesParseProxyFor(text) + if err != nil { + return nil, 0, fmt.Errorf("cannot parse 'proxy_for' section: %w", err) + } + + if addresses, ok := data[idx]; ok { + data[idx] = append(addresses, addr) + } else { + data[idx] = []string{addr} + } + case strings.HasPrefix(text, "default"): + idx, err := addressesParseDefault(text) + if err != nil { + return nil, 0, fmt.Errorf("cannot parse 'default' section: %w", err) + } + + defaultDC = idx + } + } + + err = scanner.Err() + if err != nil { + return nil, 0, fmt.Errorf("cannot parse http response: %w", err) + } + + return data, defaultDC, nil +} + +func addressesParseProxyFor(text string) (string, conntypes.DC, error) { + chunks := addressesProxyForSplitter.Split(text, 3) + if len(chunks) != 3 || chunks[0] != "proxy_for" { + return "", 0, fmt.Errorf("incorrect config %s", text) + } + + dc, err := strconv.ParseInt(chunks[1], 10, 16) + if err != nil { + return "", 0, fmt.Errorf("incorrect config '%s': %w", text, err) + } + + addr := strings.TrimRight(chunks[2], ";") + if _, _, err = net.SplitHostPort(addr); err != nil { + return "", 0, fmt.Errorf("incorrect config '%s': %w", text, err) + } + + return addr, conntypes.DC(dc), nil +} + +func addressesParseDefault(text string) (conntypes.DC, error) { + chunks := addressesProxyForSplitter.Split(text, 2) + if len(chunks) != 2 || chunks[0] != "default" { + return 0, fmt.Errorf("incorrect config '%s'", text) + } + + dcString := strings.TrimRight(chunks[1], ";") + + dc, err := strconv.ParseInt(dcString, 10, 16) + if err != nil { + return 0, fmt.Errorf("incorrect config '%s': %w", text, err) + } + + return conntypes.DC(dc), nil +} diff --git a/telegram/api/api.go b/telegram/api/api.go new file mode 100644 index 0000000..91a21c1 --- /dev/null +++ b/telegram/api/api.go @@ -0,0 +1,40 @@ +package api + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "time" +) + +const ( + apiUserAgent = "mtg" + apiHTTPTimeout = 30 * time.Second +) + +var httpClient = http.Client{ + Timeout: apiHTTPTimeout, +} + +func request(url string) (io.ReadCloser, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + panic(err) + } + + req.Header.Set("Accept", "text/plan") + req.Header.Set("User-Agent", apiUserAgent) + + resp, err := httpClient.Do(req) + if err != nil { + if resp != nil { + io.Copy(ioutil.Discard, resp.Body) // nolint: errcheck + resp.Body.Close() + } + + return nil, fmt.Errorf("cannot perform a request: %w", err) + } + + return resp.Body, err +} diff --git a/telegram/api/secret.go b/telegram/api/secret.go new file mode 100644 index 0000000..c5a1137 --- /dev/null +++ b/telegram/api/secret.go @@ -0,0 +1,24 @@ +package api + +import ( + "fmt" + "io/ioutil" +) + +const secretURL = "https://core.telegram.org/getProxySecret" // nolint: gas + +func Secret() ([]byte, error) { + resp, err := request(secretURL) + if err != nil { + return nil, fmt.Errorf("cannot access telegram server: %w", err) + } + + defer resp.Close() + + secret, err := ioutil.ReadAll(resp) + if err != nil { + return nil, fmt.Errorf("cannot read response: %w", err) + } + + return secret, nil +} diff --git a/telegram/base.go b/telegram/base.go new file mode 100644 index 0000000..f4cf3ff --- /dev/null +++ b/telegram/base.go @@ -0,0 +1,65 @@ +package telegram + +import ( + "fmt" + "math/rand" + "net" + + "mtg/conntypes" + "mtg/utils" + "mtg/wrappers/stream" +) + +type baseTelegram struct { + dialer net.Dialer + + secret []byte + v4DefaultDC conntypes.DC + V6DefaultDC conntypes.DC + v4Addresses map[conntypes.DC][]string + v6Addresses map[conntypes.DC][]string +} + +func (b *baseTelegram) Secret() []byte { + return b.secret +} + +func (b *baseTelegram) dial(dc conntypes.DC, + protocol conntypes.ConnectionProtocol) (conntypes.StreamReadWriteCloser, error) { + addr := "" + + switch protocol { + case conntypes.ConnectionProtocolIPv4: + addr = b.chooseAddress(b.v4Addresses, dc, b.v4DefaultDC) + default: + addr = b.chooseAddress(b.v6Addresses, dc, b.V6DefaultDC) + } + + conn, err := b.dialer.Dial("tcp", addr) + if err != nil { + return nil, fmt.Errorf("dial has failed: %w", err) + } + + if err := utils.InitTCP(conn); err != nil { + return nil, fmt.Errorf("cannot initialize tcp socket: %w", err) + } + + return stream.NewTelegramConn(dc, conn), nil +} + +func (b *baseTelegram) chooseAddress(addresses map[conntypes.DC][]string, + dc, defaultDC conntypes.DC) string { + addrs, ok := addresses[dc] + if !ok { + addrs = addresses[defaultDC] + } + + switch { + case len(addrs) == 1: + return addrs[0] + case len(addrs) > 1: + return addrs[rand.Intn(len(addrs))] + } + + return "" +} diff --git a/telegram/dialer.go b/telegram/dialer.go deleted file mode 100644 index 8d192d6..0000000 --- a/telegram/dialer.go +++ /dev/null @@ -1,52 +0,0 @@ -package telegram - -import ( - "context" - "net" - "time" - - "github.com/juju/errors" - - "github.com/9seconds/mtg/config" - "github.com/9seconds/mtg/wrappers" -) - -const telegramDialTimeout = 10 * time.Second - -type tgDialer struct { - net.Dialer - - conf *config.Config -} - -func (t *tgDialer) dial(addr string) (net.Conn, error) { - conn, err := t.Dialer.Dial("tcp", addr) - if err != nil { - return nil, errors.Annotate(err, "Cannot connect to Telegram") - } - - tcpSocket := conn.(*net.TCPConn) - if err = tcpSocket.SetNoDelay(true); err != nil { - return nil, errors.Annotate(err, "Cannot set NO_DELAY to Telegram") - } - if err = tcpSocket.SetReadBuffer(t.conf.WriteBufferSize); err != nil { - return nil, errors.Annotate(err, "Cannot set read buffer size on telegram socket") - } - if err = tcpSocket.SetWriteBuffer(t.conf.ReadBufferSize); err != nil { - return nil, errors.Annotate(err, "Cannot set write buffer size on telegram socket") - } - - return conn, nil -} - -func (t *tgDialer) dialRWC(ctx context.Context, cancel context.CancelFunc, - addr, connID string) (wrappers.StreamReadWriteCloser, error) { - conn, err := t.dial(addr) - if err != nil { - return nil, err - } - tgConn := wrappers.NewConn(ctx, cancel, conn, connID, - wrappers.ConnPurposeTelegram, t.conf.PublicIPv4, t.conf.PublicIPv6) - - return tgConn, nil -} diff --git a/telegram/direct.go b/telegram/direct.go index f8a083a..8fc0b48 100644 --- a/telegram/direct.go +++ b/telegram/direct.go @@ -1,31 +1,21 @@ package telegram -import ( - "context" - "net" - - "github.com/juju/errors" - - "github.com/9seconds/mtg/config" - "github.com/9seconds/mtg/mtproto" - "github.com/9seconds/mtg/obfuscated2" - "github.com/9seconds/mtg/wrappers" -) +import "mtg/conntypes" const ( - directV4DefaultIdx = 1 - directV6DefaultIdx = 1 + directV4DefaultIdx conntypes.DC = 1 + directV6DefaultIdx conntypes.DC = 1 ) var ( - directV4Addresses = map[int16][]string{ + directV4Addresses = map[conntypes.DC][]string{ 0: {"149.154.175.50:443"}, 1: {"149.154.167.51:443"}, 2: {"149.154.175.100:443"}, 3: {"149.154.167.91:443"}, 4: {"149.154.171.5:443"}, } - directV6Addresses = map[int16][]string{ + directV6Addresses = map[conntypes.DC][]string{ 0: {"[2001:b28:f23d:f001::a]:443"}, 1: {"[2001:67c:04e8:f002::a]:443"}, 2: {"[2001:b28:f23d:f003::a]:443"}, @@ -38,42 +28,14 @@ type directTelegram struct { baseTelegram } -func (t *directTelegram) Dial(ctx context.Context, cancel context.CancelFunc, - connID string, connOpts *mtproto.ConnectionOpts) (wrappers.StreamReadWriteCloser, error) { - dc := connOpts.DC - if dc < 0 { +func (d *directTelegram) Dial(dc conntypes.DC, + protocol conntypes.ConnectionProtocol) (conntypes.StreamReadWriteCloser, error) { + switch { + case dc < 0: dc = -dc - } else if dc == 0 { - dc = 1 + case dc == 0: + dc = conntypes.DCDefaultIdx } - return t.baseTelegram.dial(ctx, cancel, dc-1, connID, connOpts.ConnectionProto) -} - -func (t *directTelegram) Init(connOpts *mtproto.ConnectionOpts, - conn wrappers.StreamReadWriteCloser) (wrappers.Wrap, error) { - obfs2, frame := obfuscated2.MakeTelegramObfuscated2Frame(connOpts) - - if _, err := conn.Write(frame); err != nil { - return nil, errors.Annotate(err, "Cannot write hadnshake frame") - } - - return wrappers.NewStreamCipher(conn, obfs2.Encryptor, obfs2.Decryptor), nil -} - -// NewDirectTelegram returns Telegram instance which connects directly -// to Telegram bypassing middleproxies. -func NewDirectTelegram(conf *config.Config) Telegram { - return &directTelegram{ - baseTelegram: baseTelegram{ - dialer: tgDialer{ - Dialer: net.Dialer{Timeout: telegramDialTimeout}, - conf: conf, - }, - v4DefaultIdx: directV4DefaultIdx, - v6DefaultIdx: directV6DefaultIdx, - v4Addresses: directV4Addresses, - v6Addresses: directV6Addresses, - }, - } + return d.baseTelegram.dial(dc-1, protocol) } diff --git a/telegram/init.go b/telegram/init.go new file mode 100644 index 0000000..b4a0c19 --- /dev/null +++ b/telegram/init.go @@ -0,0 +1,42 @@ +package telegram + +import ( + "net" + "sync" + "time" +) + +const telegramDialTimeout = 10 * time.Second + +var ( + Direct Telegram + Middle Telegram + + initOnce sync.Once +) + +func Init() { + initOnce.Do(func() { + Direct = &directTelegram{ + baseTelegram: baseTelegram{ + dialer: net.Dialer{Timeout: telegramDialTimeout}, + v4DefaultDC: directV4DefaultIdx, + V6DefaultDC: directV6DefaultIdx, + v4Addresses: directV4Addresses, + v6Addresses: directV6Addresses, + }, + } + + tg := &middleTelegram{ + baseTelegram: baseTelegram{ + dialer: net.Dialer{Timeout: telegramDialTimeout}, + }, + } + if err := tg.update(); err != nil { + panic(err) + } + go tg.backgroundUpdate() + + Middle = tg + }) +} diff --git a/telegram/interfaces.go b/telegram/interfaces.go new file mode 100644 index 0000000..f6008c2 --- /dev/null +++ b/telegram/interfaces.go @@ -0,0 +1,8 @@ +package telegram + +import "mtg/conntypes" + +type Telegram interface { + Dial(conntypes.DC, conntypes.ConnectionProtocol) (conntypes.StreamReadWriteCloser, error) + Secret() []byte +} diff --git a/telegram/middle.go b/telegram/middle.go index 62ff4ba..3464489 100644 --- a/telegram/middle.go +++ b/telegram/middle.go @@ -1,139 +1,76 @@ package telegram import ( - "io" - "net" - "net/http" + "fmt" "sync" + "time" - "github.com/juju/errors" + "go.uber.org/zap" - "github.com/9seconds/mtg/config" - "github.com/9seconds/mtg/mtproto" - "github.com/9seconds/mtg/mtproto/rpc" - "github.com/9seconds/mtg/wrappers" + "mtg/conntypes" + "mtg/telegram/api" ) +const middleTelegramBackgroundUpdateEvery = time.Hour + type middleTelegram struct { - middleTelegramCaller + baseTelegram - conf *config.Config + mutex sync.RWMutex } -func (t *middleTelegram) Init(connOpts *mtproto.ConnectionOpts, - conn wrappers.StreamReadWriteCloser) (wrappers.Wrap, error) { - rpcNonceConn := wrappers.NewMTProtoFrame(conn, rpc.SeqNoNonce) +func (m *middleTelegram) Secret() []byte { + m.mutex.RLock() + defer m.mutex.RUnlock() - rpcNonceReq, err := t.sendRPCNonceRequest(rpcNonceConn) - if err != nil { - return nil, err - } - rpcNonceResp, err := t.receiveRPCNonceResponse(rpcNonceConn, rpcNonceReq) - if err != nil { - return nil, err - } - - secureConn := wrappers.NewMiddleProxyCipher(conn, rpcNonceReq, rpcNonceResp, t.proxySecret) - frameConn := wrappers.NewMTProtoFrame(secureConn, rpc.SeqNoHandshake) - - rpcHandshakeReq, err := t.sendRPCHandshakeRequest(frameConn) - if err != nil { - return nil, err - } - _, err = t.receiveRPCHandshakeResponse(frameConn, rpcHandshakeReq) - if err != nil { - return nil, err - } - - proxyConn, err := wrappers.NewMTProtoProxy(frameConn, connOpts, t.conf.AdTag) - if err != nil { - return nil, err - } - proxyConn.Logger().Infow("Telegram connection initialized") - - return proxyConn, nil + return m.baseTelegram.Secret() } -func (t *middleTelegram) sendRPCNonceRequest(conn io.Writer) (*rpc.NonceRequest, error) { - rpcNonceReq, err := rpc.NewNonceRequest(t.proxySecret) +func (m *middleTelegram) update() error { + secret, err := api.Secret() if err != nil { - return nil, errors.Annotate(err, "Cannot create RPC nonce request") - } - if _, err = conn.Write(rpcNonceReq.Bytes()); err != nil { - return nil, errors.Annotate(err, "Cannot send RPC nonce request") + return fmt.Errorf("cannot fetch secret: %w", err) } - return rpcNonceReq, nil + v4Addresses, v4DefaultDC, err := api.AddressesV4() + if err != nil { + return fmt.Errorf("cannot fetch addresses for ipv4: %w", err) + } + + v6Addresses, v6DefaultDC, err := api.AddressesV6() + if err != nil { + return fmt.Errorf("cannot fetch addresses for ipv6: %w", err) + } + + m.mutex.Lock() + m.secret = secret + m.v4DefaultDC = v4DefaultDC + m.V6DefaultDC = v6DefaultDC + m.v4Addresses = v4Addresses + m.v6Addresses = v6Addresses + m.mutex.Unlock() + + return nil } -func (t *middleTelegram) receiveRPCNonceResponse(conn wrappers.PacketReader, - req *rpc.NonceRequest) (*rpc.NonceResponse, error) { - packet, err := conn.Read() - if err != nil { - return nil, errors.Annotate(err, "Cannot read RPC nonce response") - } +func (m *middleTelegram) backgroundUpdate() { + logger := zap.S().Named("telegram") - rpcNonceResp, err := rpc.NewNonceResponse(packet) - if err != nil { - return nil, errors.Annotate(err, "Cannot initialize RPC nonce response") + for range time.Tick(middleTelegramBackgroundUpdateEvery) { + if err := m.update(); err != nil { + logger.Warnw("Cannot update Telegram proxies", "error", err) + } } - if err = rpcNonceResp.Valid(req); err != nil { - return nil, errors.Annotate(err, "Invalid RPC nonce response") - } - - return rpcNonceResp, nil } -func (t *middleTelegram) sendRPCHandshakeRequest(conn io.Writer) (*rpc.HandshakeRequest, error) { - req := rpc.NewHandshakeRequest() - if _, err := conn.Write(req.Bytes()); err != nil { - return nil, errors.Annotate(err, "Cannot send RPC handshake request") +func (m *middleTelegram) Dial(dc conntypes.DC, + protocol conntypes.ConnectionProtocol) (conntypes.StreamReadWriteCloser, error) { + if dc == 0 { + dc = conntypes.DCDefaultIdx } - return req, nil -} + m.mutex.RLock() + defer m.mutex.RUnlock() -func (t *middleTelegram) receiveRPCHandshakeResponse(conn wrappers.PacketReader, - req *rpc.HandshakeRequest) (*rpc.HandshakeResponse, error) { - packet, err := conn.Read() - if err != nil { - return nil, errors.Annotate(err, "Cannot read RPC handshake response") - } - - rpcHandshakeResp, err := rpc.NewHandshakeResponse(packet) - if err != nil { - return nil, errors.Annotate(err, "Cannot initialize RPC handshake response") - } - if err = rpcHandshakeResp.Valid(req); err != nil { - return nil, errors.Annotate(err, "Invalid RPC handshake response") - } - - return rpcHandshakeResp, nil -} - -// NewMiddleTelegram creates new instance of Telegram which works with -// middle proxies. -func NewMiddleTelegram(conf *config.Config) Telegram { - tg := &middleTelegram{ - middleTelegramCaller: middleTelegramCaller{ - baseTelegram: baseTelegram{ - dialer: tgDialer{ - Dialer: net.Dialer{Timeout: telegramDialTimeout}, - conf: conf, - }, - }, - httpClient: &http.Client{ - Timeout: middleTelegramHTTPClientTimeout, - }, - dialerMutex: &sync.RWMutex{}, - }, - conf: conf, - } - - if err := tg.update(); err != nil { - panic(err) - } - go tg.autoUpdate() - - return tg + return m.baseTelegram.dial(dc, protocol) } diff --git a/telegram/middle_caller.go b/telegram/middle_caller.go deleted file mode 100644 index 47f0780..0000000 --- a/telegram/middle_caller.go +++ /dev/null @@ -1,191 +0,0 @@ -package telegram - -import ( - "bufio" - "context" - "io/ioutil" - "net" - "net/http" - "regexp" - "strconv" - "strings" - "sync" - "time" - - "github.com/juju/errors" - "go.uber.org/zap" - - "github.com/9seconds/mtg/mtproto" - "github.com/9seconds/mtg/wrappers" -) - -const ( - middleTelegramAutoUpdateInterval = 6 * time.Hour - middleTelegramHTTPClientTimeout = 30 * time.Second - - tgAddrProxySecret = "https://core.telegram.org/getProxySecret" // nolint: gas - tgAddrProxyV4 = "https://core.telegram.org/getProxyConfig" // nolint: gas - tgAddrProxyV6 = "https://core.telegram.org/getProxyConfigV6" // nolint: gas - tgUserAgent = "mtg" -) - -var middleTelegramProxyConfigSplitter = regexp.MustCompile(`\s+`) - -type middleTelegramCaller struct { - baseTelegram - - proxySecret []byte - dialerMutex *sync.RWMutex - httpClient *http.Client -} - -func (t *middleTelegramCaller) Dial(ctx context.Context, cancel context.CancelFunc, connID string, - connOpts *mtproto.ConnectionOpts) (wrappers.StreamReadWriteCloser, error) { - dc := connOpts.DC - if dc == 0 { - dc = 1 - } - t.dialerMutex.RLock() - defer t.dialerMutex.RUnlock() - - return t.baseTelegram.dial(ctx, cancel, dc, connID, connOpts.ConnectionProto) -} - -func (t *middleTelegramCaller) autoUpdate() { - for range time.Tick(middleTelegramAutoUpdateInterval) { - if err := t.update(); err != nil { - zap.S().Warnw("Cannot update from Telegram", "error", err) - } - } -} - -func (t *middleTelegramCaller) update() error { - secret, err := t.getTelegramProxySecret() - if err != nil { - return errors.Annotate(err, "Cannot get proxy secret") - } - - v4Addresses, v4DefaultIdx, err := t.getTelegramAddresses(tgAddrProxyV4) - if err != nil { - return errors.Annotate(err, "Cannot get ipv4 addresses") - } - - v6Addresses, v6DefaultIdx, err := t.getTelegramAddresses(tgAddrProxyV6) - if err != nil { - return errors.Annotate(err, "Cannot get ipv6 addresses") - } - - t.dialerMutex.Lock() - t.proxySecret = secret - t.v4DefaultIdx = v4DefaultIdx - t.v6DefaultIdx = v6DefaultIdx - t.v4Addresses = v4Addresses - t.v6Addresses = v6Addresses - t.dialerMutex.Unlock() - - zap.S().Infow("Telegram middle proxy data has been updated") - - return nil -} - -func (t *middleTelegramCaller) getTelegramProxySecret() ([]byte, error) { - resp, err := t.call(tgAddrProxySecret) - if err != nil { - return nil, errors.Annotate(err, "Cannot access telegram server") - } - defer resp.Body.Close() // nolint: errcheck - - secret, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, errors.Annotate(err, "Cannot read response") - } - - return secret, nil -} - -func (t *middleTelegramCaller) getTelegramAddresses(url string) (map[int16][]string, int16, error) { // nolint: gocyclo - resp, err := t.call(url) - if err != nil { - return nil, 0, errors.Annotate(err, "Cannot access telegram server") - } - defer resp.Body.Close() // nolint: errcheck - - scanner := bufio.NewScanner(resp.Body) - data := map[int16][]string{} - - var defaultIdx int16 = 1 - for scanner.Scan() { - text := strings.TrimSpace(scanner.Text()) - switch { - case strings.HasPrefix(text, "#"): - continue - case strings.HasPrefix(text, "proxy_for"): - addr, idx, err2 := t.parseProxyFor(text) - if err2 != nil { - return nil, 0, errors.Annotate(err2, "Cannot parse 'proxy_for' section") - } - if addresses, ok := data[idx]; ok { - data[idx] = append(addresses, addr) - } else { - data[idx] = []string{addr} - } - case strings.HasPrefix(text, "default"): - idx, err2 := t.parseDefault(text) - if err2 != nil { - return nil, 0, errors.Annotate(err2, "Cannot parse 'default' section") - } - defaultIdx = idx - default: - return nil, 0, errors.Errorf("Unknown config string '%s'", text) - } - } - - err = scanner.Err() - if err != nil { - return nil, 0, errors.Annotate(err, "Cannot read response from the telegram") - } - - return data, defaultIdx, nil -} - -func (t *middleTelegramCaller) parseProxyFor(text string) (string, int16, error) { - chunks := middleTelegramProxyConfigSplitter.Split(text, 3) - if len(chunks) != 3 || chunks[0] != "proxy_for" { - return "", 0, errors.Errorf("Incorrect config '%s'", text) - } - - dcIdx, err := strconv.ParseInt(chunks[1], 10, 16) - if err != nil { - return "", 0, errors.Annotatef(err, "Incorrect config '%s'", text) - } - - addr := strings.TrimRight(chunks[2], ";") - if _, _, err = net.SplitHostPort(addr); err != nil { - return "", 0, errors.Annotatef(err, "Incorrect config '%s'", text) - } - - return addr, int16(dcIdx), nil -} - -func (t *middleTelegramCaller) parseDefault(text string) (int16, error) { - chunks := middleTelegramProxyConfigSplitter.Split(text, 2) - if len(chunks) != 2 || chunks[0] != "default" { - return 0, errors.Errorf("Incorrect config '%s'", text) - } - - dcIdxString := strings.TrimRight(chunks[1], ";") - dcIdx, err := strconv.ParseInt(dcIdxString, 10, 16) - if err != nil { - return 0, errors.Annotatef(err, "Incorrect config '%s'", text) - } - - return int16(dcIdx), nil -} - -func (t *middleTelegramCaller) call(url string) (*http.Response, error) { - req, _ := http.NewRequest("GET", url, nil) // nolint: gosec - req.Header.Set("Accept", "text/plain") - req.Header.Set("User-Agent", tgUserAgent) - - return t.httpClient.Do(req) -} diff --git a/telegram/telegram.go b/telegram/telegram.go deleted file mode 100644 index c7fd41e..0000000 --- a/telegram/telegram.go +++ /dev/null @@ -1,68 +0,0 @@ -package telegram - -import ( - "context" - "math/rand" - - "github.com/juju/errors" - - "github.com/9seconds/mtg/mtproto" - "github.com/9seconds/mtg/wrappers" -) - -// Telegram is an interface for different Telegram work modes. -type Telegram interface { - Dial(context.Context, context.CancelFunc, string, *mtproto.ConnectionOpts) (wrappers.StreamReadWriteCloser, error) - Init(*mtproto.ConnectionOpts, wrappers.StreamReadWriteCloser) (wrappers.Wrap, error) -} - -type baseTelegram struct { - dialer tgDialer - - v4DefaultIdx int16 - v6DefaultIdx int16 - v4Addresses map[int16][]string - v6Addresses map[int16][]string -} - -func (b *baseTelegram) dial(ctx context.Context, cancel context.CancelFunc, dcIdx int16, connID string, - proto mtproto.ConnectionProtocol) (wrappers.StreamReadWriteCloser, error) { - addrs := make([]string, 2) - - if proto&mtproto.ConnectionProtocolIPv6 != 0 { - if addr := b.chooseAddress(b.v6Addresses, dcIdx, b.v6DefaultIdx); addr != "" { - addrs = append(addrs, addr) - } - } - if proto&mtproto.ConnectionProtocolIPv4 != 0 { - if addr := b.chooseAddress(b.v4Addresses, dcIdx, b.v4DefaultIdx); addr != "" { - addrs = append(addrs, addr) - } - } - - for _, addr := range addrs { - if conn, err := b.dialer.dialRWC(ctx, cancel, addr, connID); err == nil { - return conn, err - } - } - - return nil, errors.New("Cannot connect to Telegram") -} - -func (b *baseTelegram) chooseAddress(addresses map[int16][]string, idx, defaultIdx int16) string { - if addr, ok := addresses[idx]; ok { - return b.chooseRandomAddress(addr) - } else if addr, ok := addresses[defaultIdx]; ok { - return b.chooseRandomAddress(addr) - } - - return "" -} - -func (b *baseTelegram) chooseRandomAddress(addresses []string) string { - if len(addresses) > 0 { - return addresses[rand.Intn(len(addresses))] - } - - return "" -} diff --git a/tlstypes/client_hello.go b/tlstypes/client_hello.go new file mode 100644 index 0000000..deef64a --- /dev/null +++ b/tlstypes/client_hello.go @@ -0,0 +1,86 @@ +package tlstypes + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "fmt" + + "mtg/config" + "mtg/utils" +) + +type ClientHello struct { + Handshake +} + +func (c ClientHello) Digest() []byte { + dirtyDigest := c.Random + c.Random = [32]byte{} + + rec := Record{ + Type: RecordTypeHandshake, + Version: Version10, + Data: &c, + } + + mac := hmac.New(sha256.New, config.C.Secret) + mac.Write(rec.Bytes()) // nolint: errcheck + computedDigest := mac.Sum(nil) + + for i := range computedDigest { + computedDigest[i] ^= dirtyDigest[i] + } + + return computedDigest +} + +func ParseClientHello(raw []byte) (*ClientHello, error) { + rv := &ClientHello{} + + rv.Type = HandshakeType(raw[0]) + if rv.Type != HandshakeTypeClient { + return nil, fmt.Errorf("incorrect handshake type %v", rv.Type) + } + + raw = raw[1:] + sizeUint24 := utils.Uint24{} + copy(sizeUint24[:], utils.ReverseBytes(raw[:3])) + size := int(utils.FromUint24(sizeUint24)) + + raw = raw[3:] + if len(raw) != size { + return nil, fmt.Errorf("payload size mismatch (%d != %d)", len(raw), size) + } + + versionRaw := raw[:2] + + switch { + case bytes.Equal(versionRaw, Version13Bytes): + rv.Version = Version13 + case bytes.Equal(versionRaw, Version12Bytes): + rv.Version = Version12 + case bytes.Equal(versionRaw, Version11Bytes): + rv.Version = Version11 + case bytes.Equal(versionRaw, Version10Bytes): + rv.Version = Version10 + default: + return nil, fmt.Errorf("unknown protocol version %v", versionRaw) + } + + raw = raw[2:] + copy(rv.Random[:], raw[:32]) + raw = raw[32:] + + sessionIDLength := int(raw[0]) + raw = raw[1:] + rv.SessionID = make([]byte, sessionIDLength) + copy(rv.SessionID, raw) + raw = raw[sessionIDLength:] + + tail := make([]byte, len(raw)) + copy(tail, raw) + rv.Tail = RawBytes(tail) + + return rv, nil +} diff --git a/tlstypes/consts.go b/tlstypes/consts.go new file mode 100644 index 0000000..72e6935 --- /dev/null +++ b/tlstypes/consts.go @@ -0,0 +1,79 @@ +package tlstypes + +type RecordType uint8 + +const ( + RecordTypeHandshake RecordType = 0x16 + RecordTypeApplicationData RecordType = 0x17 + RecordTypeChangeCipherSpec RecordType = 0x14 +) + +type HandshakeType uint8 + +const ( + HandshakeTypeClient HandshakeType = 0x01 + HandshakeTypeServer HandshakeType = 0x02 +) + +type CipherSuiteType uint8 + +const ( + CipherSuiteType_TLS_AES_128_GCM_SHA256 CipherSuiteType = iota // nolint: stylecheck, golint + CipherSuiteType_TLS_AES_256_GCM_SHA384 // nolint: stylecheck, golint + CipherSuiteType_TLS_CHACHA20_POLY1305_SHA256 // nolint: stylecheck, golint +) + +func (c CipherSuiteType) Bytes() []byte { + switch c { + case CipherSuiteType_TLS_AES_128_GCM_SHA256: + return CipherSuiteType_TLS_AES_128_GCM_SHA256_Bytes + case CipherSuiteType_TLS_AES_256_GCM_SHA384: + return CipherSuiteType_TLS_AES_256_GCM_SHA384_Bytes + } + + return CipherSuiteType_TLS_CHACHA20_POLY1305_SHA256_Bytes +} + +type Version uint8 + +func (v Version) Bytes() []byte { + switch v { + case Version13: + return Version13Bytes + case Version12: + return Version12Bytes + case Version11: + return Version11Bytes + } + + return Version10Bytes +} + +const ( + VersionUnknown Version = iota + Version10 + Version11 + Version12 + Version13 +) + +var ( + Version10Bytes = []byte{0x03, 0x01} + Version11Bytes = []byte{0x03, 0x02} + Version12Bytes = []byte{0x03, 0x03} + Version13Bytes = []byte{0x03, 0x04} + + CipherSuiteType_TLS_AES_128_GCM_SHA256_Bytes = []byte{0x13, 0x01} // nolint: stylecheck, golint + CipherSuiteType_TLS_AES_256_GCM_SHA384_Bytes = []byte{0x13, 0x02} // nolint: stylecheck, golint + CipherSuiteType_TLS_CHACHA20_POLY1305_SHA256_Bytes = []byte{0x13, 0x03} // nolint; stylecheck, golint +) + +type Byter interface { + Bytes() []byte +} + +type RawBytes []byte + +func (r RawBytes) Bytes() []byte { + return []byte(r) +} diff --git a/tlstypes/handshake.go b/tlstypes/handshake.go new file mode 100644 index 0000000..6afe0ba --- /dev/null +++ b/tlstypes/handshake.go @@ -0,0 +1,37 @@ +package tlstypes + +import ( + "bytes" + + "mtg/utils" +) + +type Handshake struct { + Type HandshakeType + Version Version + Random [32]byte + SessionID []byte + Tail Byter +} + +func (h *Handshake) Bytes() []byte { + buf := bytes.Buffer{} + packetBuf := bytes.Buffer{} + + buf.WriteByte(byte(h.Type)) + + packetBuf.Write(h.Version.Bytes()) + packetBuf.Write(h.Random[:]) + packetBuf.WriteByte(byte(len(h.SessionID))) + packetBuf.Write(h.SessionID) + packetBuf.Write(h.Tail.Bytes()) + + sizeUint24 := utils.ToUint24(uint32(packetBuf.Len())) + sizeUint24Bytes := sizeUint24[:] + sizeUint24Bytes[0], sizeUint24Bytes[2] = sizeUint24Bytes[2], sizeUint24Bytes[0] + + buf.Write(sizeUint24Bytes) + packetBuf.WriteTo(&buf) // nolint: errcheck + + return buf.Bytes() +} diff --git a/tlstypes/record.go b/tlstypes/record.go new file mode 100644 index 0000000..d6a71dd --- /dev/null +++ b/tlstypes/record.go @@ -0,0 +1,85 @@ +package tlstypes + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" +) + +const recordMaxChunkSize = 16384 + 24 + +type Record struct { + Type RecordType + Version Version + Data Byter +} + +func (r Record) Bytes() []byte { + buf := bytes.Buffer{} + data := r.Data.Bytes() + + buf.WriteByte(byte(r.Type)) + buf.Write(r.Version.Bytes()) + binary.Write(&buf, binary.BigEndian, uint16(len(data))) // nolint: errcheck + buf.Write(data) + + return buf.Bytes() +} + +func ReadRecord(reader io.Reader) (Record, error) { + buf := [2]byte{} + rec := Record{} + + if _, err := io.ReadFull(reader, buf[:1]); err != nil { + return rec, fmt.Errorf("cannot read record type: %w", err) + } + + rec.Type = RecordType(buf[0]) + + if _, err := io.ReadFull(reader, buf[:]); err != nil { + return rec, fmt.Errorf("cannot read version: %w", err) + } + + switch { + case bytes.Equal(buf[:], Version13Bytes): + rec.Version = Version13 + case bytes.Equal(buf[:], Version12Bytes): + rec.Version = Version12 + case bytes.Equal(buf[:], Version11Bytes): + rec.Version = Version11 + case bytes.Equal(buf[:], Version10Bytes): + rec.Version = Version10 + } + + if _, err := io.ReadFull(reader, buf[:]); err != nil { + return rec, fmt.Errorf("cannot read data length: %w", err) + } + + data := make([]byte, binary.BigEndian.Uint16(buf[:])) + if _, err := io.ReadFull(reader, data); err != nil { + return rec, fmt.Errorf("cannot read data: %w", err) + } + + rec.Data = RawBytes(data) + + return rec, nil +} + +func MakeRecords(raw []byte) (arr []Record) { + for len(raw) > 0 { + chunkSize := recordMaxChunkSize + if chunkSize > len(raw) { + chunkSize = len(raw) + } + + arr = append(arr, Record{ + Type: RecordTypeApplicationData, + Version: Version12, + Data: RawBytes(raw[:chunkSize]), + }) + raw = raw[chunkSize:] + } + + return +} diff --git a/tlstypes/server_hello.go b/tlstypes/server_hello.go new file mode 100644 index 0000000..4918926 --- /dev/null +++ b/tlstypes/server_hello.go @@ -0,0 +1,91 @@ +package tlstypes + +import ( + "bytes" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "io" + + "golang.org/x/crypto/curve25519" + + "mtg/config" +) + +type ServerHello struct { + Handshake + + clientHello *ClientHello +} + +func (s ServerHello) WelcomePacket(hostCert []byte) []byte { + s.Random = [32]byte{} + rec := Record{ + Type: RecordTypeHandshake, + Version: Version12, + Data: &s, + } + buf := bytes.NewBuffer(rec.Bytes()) + + recChangeCipher := Record{ + Type: RecordTypeChangeCipherSpec, + Version: Version12, + Data: RawBytes([]byte{0x01}), + } + buf.Write(recChangeCipher.Bytes()) + + recData := Record{ + Type: RecordTypeApplicationData, + Version: Version12, + Data: RawBytes(hostCert), + } + buf.Write(recData.Bytes()) + packet := buf.Bytes() + + mac := hmac.New(sha256.New, config.C.Secret) + mac.Write(s.clientHello.Random[:]) // nolint: errcheck + mac.Write(packet) // nolint: errcheck + copy(packet[11:], mac.Sum(nil)) + + return packet +} + +func NewServerHello(clientHello *ClientHello) *ServerHello { + rv := &ServerHello{ + clientHello: clientHello, + } + + rv.Type = HandshakeTypeServer + rv.Version = Version12 + rv.SessionID = make([]byte, len(clientHello.SessionID)) + copy(rv.SessionID, clientHello.SessionID) + + tail := bytes.NewBuffer(CipherSuiteType_TLS_AES_128_GCM_SHA256_Bytes) + tail.WriteByte(0x00) // no compression + makeTLSExtensions(tail) + rv.Tail = RawBytes(tail.Bytes()) + + return rv +} + +func makeTLSExtensions(buf io.Writer) { + buf.Write([]byte{ // nolint: errcheck + 0x00, 0x2e, // 46 bytes of data + 0x00, 0x33, // Extension - Key Share + 0x00, 0x24, // 36 bytes + 0x00, 0x1d, // x25519 curve + 0x00, 0x20, // 32 bytes of key + }) + + var scalar [32]byte + + rand.Read(scalar[:]) // nolint: errcheck + curve, _ := curve25519.X25519(scalar[:], curve25519.Basepoint) + buf.Write(curve) // nolint: errcheck + + buf.Write([]byte{ // nolint: errcheck + 0x00, 0x2b, // Extension - Supported Versions + 0x00, 0x02, // 2 bytes are following + 0x03, 0x04, // TLS 1.3 + }) +} diff --git a/utils/init_tcp.go b/utils/init_tcp.go new file mode 100644 index 0000000..d59b281 --- /dev/null +++ b/utils/init_tcp.go @@ -0,0 +1,26 @@ +package utils + +import ( + "fmt" + "net" + + "mtg/config" +) + +func InitTCP(conn net.Conn) error { + tcpConn := conn.(*net.TCPConn) + + if err := tcpConn.SetNoDelay(true); err != nil { + return fmt.Errorf("cannot set TCP_NO_DELAY: %w", err) + } + + if err := tcpConn.SetReadBuffer(config.C.ReadBuffer); err != nil { + return fmt.Errorf("cannot set read buffer size: %w", err) + } + + if err := tcpConn.SetWriteBuffer(config.C.WriteBuffer); err != nil { + return fmt.Errorf("cannot set write buffer size: %w", err) + } + + return nil +} diff --git a/utils/read_current_data.go b/utils/read_current_data.go deleted file mode 100644 index 284369c..0000000 --- a/utils/read_current_data.go +++ /dev/null @@ -1,21 +0,0 @@ -package utils - -import "io" - -const readCurrentDataBufferSize = 1024 + 1 // + 1 because telegram operates with blocks mod 4 - -// ReadCurrentData reads all data from io.Reader which is ready to be read. -func ReadCurrentData(src io.Reader) (rv []byte, err error) { - buf := make([]byte, readCurrentDataBufferSize) - n := readCurrentDataBufferSize - - for n == len(buf) { - n, err = src.Read(buf) - if err != nil { - return nil, err - } - rv = append(rv, buf[:n]...) - } - - return rv, nil -} diff --git a/utils/read_full.go b/utils/read_full.go new file mode 100644 index 0000000..912aa19 --- /dev/null +++ b/utils/read_full.go @@ -0,0 +1,21 @@ +package utils + +import "io" + +const readFullBufferSize = 1024 + 1 // +1 because telegram opreates with blocks mod 4 + +func ReadFull(src io.Reader) (rv []byte, err error) { + buf := make([]byte, readFullBufferSize) + n := readFullBufferSize + + for n == len(buf) { + n, err = src.Read(buf) + if err != nil { + return nil, err + } + + rv = append(rv, buf[:n]...) + } + + return rv, nil +} diff --git a/utils/reverse_bytes.go b/utils/reverse_bytes.go index ab7cd5b..87ad8f6 100644 --- a/utils/reverse_bytes.go +++ b/utils/reverse_bytes.go @@ -4,8 +4,8 @@ package utils func ReverseBytes(data []byte) []byte { dataLen := len(data) rv := make([]byte, dataLen) - rv[dataLen/2] = data[dataLen/2] + for i := dataLen/2 - 1; i >= 0; i-- { opp := dataLen - i - 1 rv[i], rv[opp] = data[opp], data[i] diff --git a/utils/rlimit.go b/utils/rlimit.go new file mode 100644 index 0000000..b474076 --- /dev/null +++ b/utils/rlimit.go @@ -0,0 +1,24 @@ +// +build !windows + +package utils + +import ( + "fmt" + + "golang.org/x/sys/unix" +) + +func SetLimits() error { + rLimit := unix.Rlimit{} + if err := unix.Getrlimit(unix.RLIMIT_NOFILE, &rLimit); err != nil { + return fmt.Errorf("cannot get rlimit: %w", err) + } + + rLimit.Cur = rLimit.Max + + if err := unix.Setrlimit(unix.RLIMIT_NOFILE, &rLimit); err != nil { + return fmt.Errorf("cannot set rlimit: %w", err) + } + + return nil +} diff --git a/utils/rlimit_windows.go b/utils/rlimit_windows.go new file mode 100644 index 0000000..58bdf39 --- /dev/null +++ b/utils/rlimit_windows.go @@ -0,0 +1,7 @@ +// +build windows + +package utils + +func SetLimits() error { + return nil +} diff --git a/utils/signal_context.go b/utils/signal_context.go new file mode 100644 index 0000000..6d3951f --- /dev/null +++ b/utils/signal_context.go @@ -0,0 +1,25 @@ +// +build !windows + +package utils + +import ( + "context" + "os" + "os/signal" + "syscall" +) + +func GetSignalContext() context.Context { + ctx, cancel := context.WithCancel(context.Background()) + sigChan := make(chan os.Signal, 1) + + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + go func() { + for range sigChan { + cancel() + } + }() + + return ctx +} diff --git a/utils/signal_context_windows.go b/utils/signal_context_windows.go new file mode 100644 index 0000000..d18ce71 --- /dev/null +++ b/utils/signal_context_windows.go @@ -0,0 +1,23 @@ +// +build windows + +package utils + +import ( + "context" + "os" + "os/signal" +) + +func GetSignalContext() context.Context { + ctx, cancel := context.WithCancel(context.Background()) + sigChan := make(chan os.Signal, 1) + + signal.Notify(sigChan, os.Interrupt) + go func() { + for range sigChan { + cancel() + } + }() + + return ctx +} diff --git a/utils/stream_cipher.go b/utils/stream_cipher.go new file mode 100644 index 0000000..072d607 --- /dev/null +++ b/utils/stream_cipher.go @@ -0,0 +1,11 @@ +package utils + +import ( + "crypto/aes" + "crypto/cipher" +) + +func MakeStreamCipher(key, iv []byte) cipher.Stream { + block, _ := aes.NewCipher(key) // nolint: gosec + return cipher.NewCTR(block, iv) +} diff --git a/utils/uint24.go b/utils/uint24.go index 66c5f1b..350f3d5 100644 --- a/utils/uint24.go +++ b/utils/uint24.go @@ -1,15 +1,11 @@ package utils -// Uint24 is a replacement for the absent Go uint24 data type. -// This data type is little endian. type Uint24 [3]byte -// ToUint24 converts number to Uint24. func ToUint24(number uint32) Uint24 { return Uint24{byte(number), byte(number >> 8), byte(number >> 16)} } -// FromUint24 converts Uint24 to number. func FromUint24(number Uint24) uint32 { return uint32(number[0]) + (uint32(number[1]) << 8) + (uint32(number[2]) << 16) } diff --git a/wrappers/blockcipher.go b/wrappers/blockcipher.go deleted file mode 100644 index 745efca..0000000 --- a/wrappers/blockcipher.go +++ /dev/null @@ -1,99 +0,0 @@ -package wrappers - -import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "net" - - "go.uber.org/zap" - - "github.com/9seconds/mtg/utils" - "github.com/juju/errors" -) - -// BlockCipher is a stream writer which encrypts/decrypts blocks of data -// with AES CBC. This also is buffered reader. It means, that block -// reading is transparent for it, you can assume you are working with -// good old io.Reader. -type BlockCipher struct { - buf *bytes.Buffer - - logger *zap.SugaredLogger - conn StreamReadWriteCloser - encryptor cipher.BlockMode - decryptor cipher.BlockMode -} - -func (b *BlockCipher) Read(p []byte) (int, error) { - if b.buf.Len() > 0 { - return b.flush(p) - } - - buf := []byte{} - for len(buf) == 0 || len(buf)%aes.BlockSize != 0 { - rv, err := utils.ReadCurrentData(b.conn) - if err != nil { - return 0, errors.Annotate(err, "Cannot read from socket") - } - buf = append(buf, rv...) - } - - b.decryptor.CryptBlocks(buf, buf) - b.buf.Write(buf) // nolint: gosec - - return b.flush(p) -} - -func (b *BlockCipher) flush(p []byte) (int, error) { - if b.buf.Len() <= len(p) { - sizeToReturn := b.buf.Len() - copy(p, b.buf.Bytes()) - b.buf.Reset() - return sizeToReturn, nil - } - - return b.buf.Read(p) -} - -func (b *BlockCipher) Write(p []byte) (int, error) { - if len(p)%aes.BlockSize > 0 { - return 0, errors.Errorf("Incorrect block size %d", len(p)) - } - - encrypted := make([]byte, len(p)) - b.encryptor.CryptBlocks(encrypted, p) - - return b.conn.Write(encrypted) -} - -// Logger returns an instance of the logger for this wrapper. -func (b *BlockCipher) Logger() *zap.SugaredLogger { - return b.logger -} - -// LocalAddr returns local address of the underlying net.Conn. -func (b *BlockCipher) LocalAddr() *net.TCPAddr { - return b.conn.LocalAddr() -} - -// RemoteAddr returns remote address of the underlying net.Conn. -func (b *BlockCipher) RemoteAddr() *net.TCPAddr { - return b.conn.RemoteAddr() -} - -// Close closes underlying net.Conn. -func (b *BlockCipher) Close() error { - return b.conn.Close() -} - -// NewBlockCipher creates new instance of BlockCipher based on given data. -func NewBlockCipher(conn StreamReadWriteCloser, encryptor, decryptor cipher.BlockMode) StreamReadWriteCloser { - return &BlockCipher{ - buf: &bytes.Buffer{}, - conn: conn, - logger: conn.Logger().Named("block-cipher"), - encryptor: encryptor, - decryptor: decryptor, - } -} diff --git a/wrappers/conn.go b/wrappers/conn.go deleted file mode 100644 index 239af53..0000000 --- a/wrappers/conn.go +++ /dev/null @@ -1,154 +0,0 @@ -package wrappers - -import ( - "context" - "net" - "time" - - "go.uber.org/zap" - - "github.com/9seconds/mtg/stats" - "github.com/juju/errors" -) - -// ConnPurpose is intended to be identifier of connection purpose. We -// sometimes want to treat client/telegram connection differently (for -// logging for example). -type ConnPurpose uint8 - -func (c ConnPurpose) String() string { - switch c { - case ConnPurposeClient: - return "client" - case ConnPurposeTelegram: - return "telegram" - } - - return "" -} - -// ConnPurpose* define different connection types. -const ( - ConnPurposeClient = iota - ConnPurposeTelegram -) - -const ( - connTimeoutRead = 2 * time.Minute - connTimeoutWrite = 2 * time.Minute -) - -// Conn is a basic wrapper for net.Conn providing the most low-level -// logic and management as possible. -type Conn struct { - conn net.Conn - ctx context.Context - cancel context.CancelFunc - connID string - logger *zap.SugaredLogger - - publicIPv4 net.IP - publicIPv6 net.IP -} - -func (c *Conn) Write(p []byte) (int, error) { - select { - case <-c.ctx.Done(): - c.Close() // nolint: gosec - return 0, errors.Annotate(c.ctx.Err(), "Cannot write because context was closed") - default: - if err := c.conn.SetWriteDeadline(time.Now().Add(connTimeoutWrite)); err != nil { - c.Close() // nolint: gosec - return 0, errors.Annotate(err, "Cannot set write deadline to the socket") - } - - n, err := c.conn.Write(p) - c.logger.Debugw("Write to stream", "bytes", n, "error", err) - stats.EgressTraffic(n) - if err != nil { - c.Close() // nolint: gosec - } - - return n, err - } -} - -func (c *Conn) Read(p []byte) (int, error) { - select { - case <-c.ctx.Done(): - c.Close() // nolint: gosec - return 0, errors.Annotate(c.ctx.Err(), "Cannot read because context was closed") - default: - if err := c.conn.SetReadDeadline(time.Now().Add(connTimeoutRead)); err != nil { - c.Close() // nolint: gosec - return 0, errors.Annotate(err, "Cannot set read deadline to the socket") - } - - n, err := c.conn.Read(p) - c.logger.Debugw("Read from stream", "bytes", n, "error", err) - stats.IngressTraffic(n) - if err != nil { - c.Close() // nolint: gosec - } - - return n, err - } -} - -// Close closes underlying net.Conn instance. -func (c *Conn) Close() error { - c.logger.Debugw("Close connection") - c.cancel() - - return c.conn.Close() -} - -// Logger returns an instance of the logger for this wrapper. -func (c *Conn) Logger() *zap.SugaredLogger { - return c.logger -} - -// LocalAddr returns local address of the underlying net.Conn. -func (c *Conn) LocalAddr() *net.TCPAddr { - addr := c.conn.LocalAddr().(*net.TCPAddr) - newAddr := *addr - - if c.RemoteAddr().IP.To4() != nil { - if c.publicIPv4 != nil { - newAddr.IP = c.publicIPv4 - } - } else if c.publicIPv6 != nil { - newAddr.IP = c.publicIPv6 - } - - return &newAddr -} - -// RemoteAddr returns remote address of the underlying net.Conn. -func (c *Conn) RemoteAddr() *net.TCPAddr { - return c.conn.RemoteAddr().(*net.TCPAddr) -} - -// NewConn initializes Conn wrapper for net.Conn. -func NewConn(ctx context.Context, cancel context.CancelFunc, conn net.Conn, - connID string, purpose ConnPurpose, publicIPv4, publicIPv6 net.IP) StreamReadWriteCloser { - logger := zap.S().With( - "connection_id", connID, - "local_address", conn.LocalAddr(), - "remote_address", conn.RemoteAddr(), - "purpose", purpose, - ).Named("conn") - - wrapper := Conn{ - conn: conn, - ctx: ctx, - cancel: cancel, - connID: connID, - logger: logger, - publicIPv4: publicIPv4, - publicIPv6: publicIPv6, - } - wrapper.logger = logger.With("faked_local_addr", wrapper.LocalAddr()) - - return &wrapper -} diff --git a/wrappers/mtproto_abridged.go b/wrappers/mtproto_abridged.go deleted file mode 100644 index 6332af6..0000000 --- a/wrappers/mtproto_abridged.go +++ /dev/null @@ -1,159 +0,0 @@ -package wrappers - -import ( - "bytes" - "io" - "net" - - "github.com/juju/errors" - "go.uber.org/zap" - - "github.com/9seconds/mtg/mtproto" - "github.com/9seconds/mtg/utils" -) - -const ( - mtprotoAbridgedSmallPacketLength = 0x7f - mtprotoAbridgedQuickAckLength = 0x80 - mtprotoAbridgedLargePacketLength = 16777216 // 256 ^ 3 -) - -// MTProtoAbridged presents abridged connection between client and -// middle proxy. -type MTProtoAbridged struct { - conn StreamReadWriteCloser - opts *mtproto.ConnectionOpts - logger *zap.SugaredLogger - - readCounter uint32 - writeCounter uint32 -} - -func (m *MTProtoAbridged) Read() ([]byte, error) { - defer func() { - m.readCounter++ - }() - - m.logger.Debugw("Read packet", - "simple_ack", m.opts.ReadHacks.SimpleAck, - "quick_ack", m.opts.ReadHacks.QuickAck, - "counter", m.readCounter, - ) - - buf := &bytes.Buffer{} - buf.Grow(3) - - if _, err := io.CopyN(buf, m.conn, 1); err != nil { - return nil, errors.Annotate(err, "Cannot read message length") - } - msgLength := uint32(buf.Bytes()[0]) - buf.Reset() - - m.logger.Debugw("Packet first byte", - "byte", msgLength, - "counter", m.readCounter, - "simple_ack", m.opts.ReadHacks.SimpleAck, - "quick_ack", m.opts.ReadHacks.QuickAck, - ) - - if msgLength >= mtprotoAbridgedQuickAckLength { - m.opts.ReadHacks.QuickAck = true - msgLength -= mtprotoAbridgedQuickAckLength - } - - if msgLength == mtprotoAbridgedSmallPacketLength { - if _, err := io.CopyN(buf, m.conn, 3); err != nil { - return nil, errors.Annotate(err, "Cannot read the correct message length") - } - number := utils.Uint24{} - copy(number[:], buf.Bytes()) - msgLength = utils.FromUint24(number) - } - msgLength *= 4 - - m.logger.Debugw("Packet length", - "length", msgLength, - "simple_ack", m.opts.ReadHacks.SimpleAck, - "quick_ack", m.opts.ReadHacks.QuickAck, - "counter", m.readCounter, - ) - - buf.Reset() - buf.Grow(int(msgLength)) - if _, err := io.CopyN(buf, m.conn, int64(msgLength)); err != nil { - return nil, errors.Annotate(err, "Cannot read message") - } - - return buf.Bytes(), nil -} - -func (m *MTProtoAbridged) Write(p []byte) (int, error) { - defer func() { - m.writeCounter++ - }() - - m.logger.Debugw("Write packet", - "length", len(p), - "simple_ack", m.opts.WriteHacks.SimpleAck, - "quick_ack", m.opts.WriteHacks.QuickAck, - "counter", m.writeCounter, - ) - - if len(p)%4 != 0 { - return 0, errors.Errorf("Incorrect packet length %d", len(p)) - } - - if m.opts.WriteHacks.SimpleAck { - return m.conn.Write(utils.ReverseBytes(p)) - } - - packetLength := len(p) / 4 - switch { - case packetLength < mtprotoAbridgedSmallPacketLength: - newData := append([]byte{byte(packetLength)}, p...) - return m.conn.Write(newData) - - case packetLength < mtprotoAbridgedLargePacketLength: - length24 := utils.ToUint24(uint32(packetLength)) - - buf := &bytes.Buffer{} - buf.Grow(1 + 3 + len(p)) - - buf.WriteByte(byte(mtprotoAbridgedSmallPacketLength)) // nolint: gosec - buf.Write(length24[:]) // nolint: gosec - buf.Write(p) // nolint: gosec - - return m.conn.Write(buf.Bytes()) - } - - return 0, errors.Errorf("Packet is too big %d", len(p)) -} - -// Logger returns an instance of the logger for this wrapper. -func (m *MTProtoAbridged) Logger() *zap.SugaredLogger { - return m.logger -} - -// LocalAddr returns local address of the underlying net.Conn. -func (m *MTProtoAbridged) LocalAddr() *net.TCPAddr { - return m.conn.LocalAddr() -} - -// RemoteAddr returns remote address of the underlying net.Conn. -func (m *MTProtoAbridged) RemoteAddr() *net.TCPAddr { - return m.conn.RemoteAddr() -} - -// Close closes underlying net.Conn instance. -func (m *MTProtoAbridged) Close() error { - return m.conn.Close() -} - -// NewMTProtoAbridged creates new wrapper for abridged client connection. -func NewMTProtoAbridged(conn StreamReadWriteCloser, opts *mtproto.ConnectionOpts) PacketReadWriteCloser { - return &MTProtoAbridged{ - conn: conn, - opts: opts, - logger: conn.Logger().Named("mtproto-abridged"), - } -} diff --git a/wrappers/mtproto_frame.go b/wrappers/mtproto_frame.go deleted file mode 100644 index 2e39d6b..0000000 --- a/wrappers/mtproto_frame.go +++ /dev/null @@ -1,161 +0,0 @@ -package wrappers - -import ( - "bytes" - "crypto/aes" - "encoding/binary" - "hash/crc32" - "io" - "io/ioutil" - "net" - - "github.com/juju/errors" - "go.uber.org/zap" -) - -const ( - mtprotoFrameMinMessageLength = 12 - mtprotoFrameMaxMessageLength = 16777216 -) - -var mtprotoFramePadding = []byte{0x04, 0x00, 0x00, 0x00} - -// MTProtoFrame is a wrapper which converts written data to the MTProtoFrame. -// The format of the frame: -// -// [ MSGLEN(4) | SEQNO(4) | MSG(...) | CRC32(4) | PADDING(4*x) ] -// -// MSGLEN is the length of the message + len of seqno and msglen. -// SEQNO is the number of frame in the receive/send sequence. If client -// sends a message with SeqNo 18, it has to receive message with SeqNo 18. -// MSG is the data which has to be written -// CRC32 is the CRC32 checksum of MSGLEN + SEQNO + MSG -// PADDING is custom padding schema to complete frame length to such that -// len(frame) % 16 == 0 -type MTProtoFrame struct { - conn StreamReadWriteCloser - logger *zap.SugaredLogger - - readSeqNo int32 - writeSeqNo int32 -} - -func (m *MTProtoFrame) Read() ([]byte, error) { // nolint: gocyclo - buf := &bytes.Buffer{} - sum := crc32.NewIEEE() - writer := io.MultiWriter(buf, sum) - - for { - buf.Reset() - sum.Reset() - if _, err := io.CopyN(writer, m.conn, 4); err != nil { - return nil, errors.Annotate(err, "Cannot read frame padding") - } - if !bytes.Equal(buf.Bytes(), mtprotoFramePadding) { - break - } - } - - messageLength := binary.LittleEndian.Uint32(buf.Bytes()) - m.logger.Debugw("Read MTProto frame", - "messageLength", messageLength, - "sequence_number", m.readSeqNo, - ) - if messageLength%4 != 0 || messageLength < mtprotoFrameMinMessageLength || - messageLength > mtprotoFrameMaxMessageLength { - return nil, errors.Errorf("Incorrect frame message length %d", messageLength) - } - - buf.Reset() - buf.Grow(int(messageLength) - 4 - 4) - if _, err := io.CopyN(writer, m.conn, int64(messageLength)-4-4); err != nil { - return nil, errors.Annotate(err, "Cannot read the message frame") - } - - var seqNo int32 - binary.Read(buf, binary.LittleEndian, &seqNo) // nolint: errcheck, gosec - if seqNo != m.readSeqNo { - return nil, errors.Errorf("Unexpected sequence number %d (wait for %d)", seqNo, m.readSeqNo) - } - - data, _ := ioutil.ReadAll(buf) // nolint: gosec - buf.Reset() - // write to buf, not to writer. This is because we are going to fetch - // crc32 checksum. - if _, err := io.CopyN(buf, m.conn, 4); err != nil { - return nil, errors.Annotate(err, "Cannot read checksum") - } - - checksum := binary.LittleEndian.Uint32(buf.Bytes()) - if checksum != sum.Sum32() { - return nil, errors.Errorf("CRC32 checksum mismatch. Wait for %d, got %d", sum.Sum32(), checksum) - } - - m.logger.Debugw("Read MTProto frame", - "messageLength", messageLength, - "sequence_number", m.readSeqNo, - "dataLength", len(data), - "checksum", checksum, - ) - m.readSeqNo++ - - return data, nil -} - -func (m *MTProtoFrame) Write(p []byte) (int, error) { - messageLength := 4 + 4 + len(p) + 4 - paddingLength := (aes.BlockSize - messageLength%aes.BlockSize) % aes.BlockSize - - buf := &bytes.Buffer{} - buf.Grow(messageLength + paddingLength) - - binary.Write(buf, binary.LittleEndian, uint32(messageLength)) // nolint: errcheck, gosec - binary.Write(buf, binary.LittleEndian, m.writeSeqNo) // nolint: errcheck, gosec - buf.Write(p) // nolint: gosec - - checksum := crc32.ChecksumIEEE(buf.Bytes()) - binary.Write(buf, binary.LittleEndian, checksum) // nolint: errcheck, gosec - buf.Write(bytes.Repeat(mtprotoFramePadding, paddingLength/4)) // nolint: gosec - - m.logger.Debugw("Write MTProto frame", - "length", len(p), - "sequence_number", m.writeSeqNo, - "crc32", checksum, - "frame_length", buf.Len(), - ) - m.writeSeqNo++ - - _, err := m.conn.Write(buf.Bytes()) - - return len(p), err -} - -// Logger returns an instance of the logger for this wrapper. -func (m *MTProtoFrame) Logger() *zap.SugaredLogger { - return m.logger -} - -// LocalAddr returns local address of the underlying net.Conn. -func (m *MTProtoFrame) LocalAddr() *net.TCPAddr { - return m.conn.LocalAddr() -} - -// RemoteAddr returns remote address of the underlying net.Conn. -func (m *MTProtoFrame) RemoteAddr() *net.TCPAddr { - return m.conn.RemoteAddr() -} - -// Close closes underlying net.Conn instance. -func (m *MTProtoFrame) Close() error { - return m.conn.Close() -} - -// NewMTProtoFrame creates new PacketWrapper for underlying connection. -func NewMTProtoFrame(conn StreamReadWriteCloser, seqNo int32) PacketReadWriteCloser { - return &MTProtoFrame{ - conn: conn, - logger: conn.Logger().Named("mtproto-frame"), - readSeqNo: seqNo, - writeSeqNo: seqNo, - } -} diff --git a/wrappers/mtproto_intermediate.go b/wrappers/mtproto_intermediate.go deleted file mode 100644 index 5b88d74..0000000 --- a/wrappers/mtproto_intermediate.go +++ /dev/null @@ -1,117 +0,0 @@ -package wrappers - -import ( - "bytes" - "encoding/binary" - "io" - "net" - - "github.com/juju/errors" - "go.uber.org/zap" - - "github.com/9seconds/mtg/mtproto" -) - -const mtprotoIntermediateQuickAckLength = 0x80000000 - -// MTProtoIntermediate presents intermediate connection between client -// and Telegram. -type MTProtoIntermediate struct { - conn StreamReadWriteCloser - opts *mtproto.ConnectionOpts - logger *zap.SugaredLogger - - readCounter uint32 - writeCounter uint32 -} - -func (m *MTProtoIntermediate) Read() ([]byte, error) { - defer func() { - m.readCounter++ - }() - - m.logger.Debugw("Read packet", - "simple_ack", m.opts.ReadHacks.SimpleAck, - "quick_ack", m.opts.ReadHacks.QuickAck, - "counter", m.readCounter, - ) - - buf := &bytes.Buffer{} - buf.Grow(4) - - if _, err := io.CopyN(buf, m.conn, 4); err != nil { - return nil, errors.Annotate(err, "Cannot read message length") - } - length := binary.LittleEndian.Uint32(buf.Bytes()) - - m.logger.Debugw("Packet message length", - "simple_ack", m.opts.ReadHacks.SimpleAck, - "quick_ack", m.opts.ReadHacks.QuickAck, - "counter", m.readCounter, - "length", length, - ) - - if length > mtprotoIntermediateQuickAckLength { - m.opts.ReadHacks.QuickAck = true - length -= mtprotoIntermediateQuickAckLength - } - - buf.Reset() - buf.Grow(int(length)) - if _, err := io.CopyN(buf, m.conn, int64(length)); err != nil { - return nil, errors.Annotate(err, "Cannot read the message") - } - - return buf.Bytes()[:length], nil -} - -func (m *MTProtoIntermediate) Write(p []byte) (int, error) { - defer func() { - m.writeCounter++ - }() - - m.logger.Debugw("Write packet", - "simple_ack", m.opts.WriteHacks.SimpleAck, - "quick_ack", m.opts.WriteHacks.QuickAck, - "counter", m.writeCounter, - ) - - if m.opts.WriteHacks.SimpleAck { - return m.conn.Write(p) - } - - var length [4]byte - binary.LittleEndian.PutUint32(length[:], uint32(len(p))) - - return m.conn.Write(append(length[:], p...)) -} - -// Logger returns an instance of the logger for this wrapper. -func (m *MTProtoIntermediate) Logger() *zap.SugaredLogger { - return m.logger -} - -// LocalAddr returns local address of the underlying net.Conn. -func (m *MTProtoIntermediate) LocalAddr() *net.TCPAddr { - return m.conn.LocalAddr() -} - -// RemoteAddr returns remote address of the underlying net.Conn. -func (m *MTProtoIntermediate) RemoteAddr() *net.TCPAddr { - return m.conn.RemoteAddr() -} - -// Close closes underlying net.Conn instance. -func (m *MTProtoIntermediate) Close() error { - return m.conn.Close() -} - -// NewMTProtoIntermediate creates new PacketWrapper for intermediate -// client connection. -func NewMTProtoIntermediate(conn StreamReadWriteCloser, opts *mtproto.ConnectionOpts) PacketReadWriteCloser { - return &MTProtoIntermediate{ - conn: conn, - logger: conn.Logger().Named("mtproto-intermediate"), - opts: opts, - } -} diff --git a/wrappers/mtproto_intermediate_secure.go b/wrappers/mtproto_intermediate_secure.go deleted file mode 100644 index f26612b..0000000 --- a/wrappers/mtproto_intermediate_secure.go +++ /dev/null @@ -1,74 +0,0 @@ -package wrappers - -import ( - "bytes" - "encoding/binary" - "math/rand" - - "github.com/9seconds/mtg/mtproto" -) - -// MTProtoIntermediateSecure is an extension of MTProtoIntermediate -// mode which supports random paddings (socalled 'secure mode' or -// 'dd-secrets'). -type MTProtoIntermediateSecure struct { - MTProtoIntermediate -} - -func (m *MTProtoIntermediateSecure) Read() ([]byte, error) { - data, err := m.MTProtoIntermediate.Read() - if err != nil { - return nil, err - } - length := len(data) - (len(data) % 4) - - return data[:length], nil -} - -func (m *MTProtoIntermediateSecure) Write(p []byte) (int, error) { - defer func() { - m.writeCounter++ - }() - - m.logger.Debugw("Write packet", - "simple_ack", m.opts.WriteHacks.SimpleAck, - "quick_ack", m.opts.WriteHacks.QuickAck, - "counter", m.writeCounter, - ) - - if m.opts.WriteHacks.SimpleAck { - return m.conn.Write(p) - } - - buf := &bytes.Buffer{} - paddingLength := rand.Intn(4) - buf.Grow(4 + len(p) + paddingLength) - - binary.Write(buf, binary.LittleEndian, uint32(len(p)+paddingLength)) // nolint: errcheck, gosec - buf.Write(p) // nolint: gosec - buf.Write(make([]byte, paddingLength)) // nolint: gosec - - m.logger.Debugw("Write packet with padding", - "simple_ack", m.opts.WriteHacks.SimpleAck, - "quick_ack", m.opts.WriteHacks.QuickAck, - "counter", m.writeCounter, - "padding_length", paddingLength, - "length", len(p), - ) - - _, err := m.conn.Write(buf.Bytes()) - - return len(p), err -} - -// NewMTProtoIntermediateSecure create new instance of -// MTProtoIntermediateSecure instance. -func NewMTProtoIntermediateSecure(conn StreamReadWriteCloser, opts *mtproto.ConnectionOpts) PacketReadWriteCloser { - return &MTProtoIntermediateSecure{ - MTProtoIntermediate: MTProtoIntermediate{ - conn: conn, - logger: conn.Logger().Named("mtproto-intermediate-secure"), - opts: opts, - }, - } -} diff --git a/wrappers/mtproto_proxy.go b/wrappers/mtproto_proxy.go deleted file mode 100644 index e3ac897..0000000 --- a/wrappers/mtproto_proxy.go +++ /dev/null @@ -1,165 +0,0 @@ -package wrappers - -import ( - "bytes" - "fmt" - "net" - - "github.com/juju/errors" - "go.uber.org/zap" - - "github.com/9seconds/mtg/mtproto" - "github.com/9seconds/mtg/mtproto/rpc" -) - -// MTProtoProxy is a wrapper which creates/reads RPC responses from Telegram. -type MTProtoProxy struct { - conn PacketReadWriteCloser - req *rpc.ProxyRequest - logger *zap.SugaredLogger - - readCounter uint32 - writeCounter uint32 -} - -func (m *MTProtoProxy) Read() ([]byte, error) { - defer func() { - m.readCounter++ - }() - - m.logger.Debugw("Read packet", - "counter", m.readCounter, - "simple_ack", m.req.Options.WriteHacks.SimpleAck, - "quick_ack", m.req.Options.WriteHacks.QuickAck, - ) - - packet, err := m.conn.Read() - if err != nil { - return nil, errors.Annotate(err, "Cannot read packet") - } - - m.logger.Debugw("Read packet length", - "counter", m.readCounter, - "simple_ack", m.req.Options.WriteHacks.SimpleAck, - "quick_ack", m.req.Options.WriteHacks.QuickAck, - "length", len(packet), - ) - - if len(packet) < 4 { - return nil, errors.Annotate(err, "Incorrect packet length") - } - - tag, packet := packet[:4], packet[4:] - switch { - case bytes.Equal(tag, rpc.TagProxyAns): - return m.readProxyAns(packet) - case bytes.Equal(tag, rpc.TagSimpleAck): - return m.readSimpleAck(packet) - case bytes.Equal(tag, rpc.TagCloseExt): - return m.readCloseExt() - } - - return nil, errors.Errorf("Unknown RPC answer %v", tag) -} - -func (m *MTProtoProxy) readProxyAns(data []byte) ([]byte, error) { - if len(data) < 12 { - return nil, errors.Errorf("Incorrect data of proxy answer: %d", len(data)) - } - data = data[12:] - - m.logger.Debugw("Read RPC_PROXY_ANS", - "counter", m.readCounter, - "length", len(data), - ) - - return data, nil -} - -func (m *MTProtoProxy) readSimpleAck(data []byte) ([]byte, error) { - if len(data) != 12 { - return nil, errors.Errorf("Incorrect data of simple ack: %d", len(data)) - } - data = data[8:12] - m.req.Options.WriteHacks.SimpleAck = true - - m.logger.Debugw("Read RPC_SIMPLE_ACK", - "counter", m.readCounter, - "length", len(data), - ) - - return data, nil -} - -func (m *MTProtoProxy) readCloseExt() ([]byte, error) { - m.logger.Debugw("Read RPC_CLOSE_EXT", "counter", m.readCounter) - - return nil, errors.New("Connection has been closed remotely by RPC call") -} - -func (m *MTProtoProxy) Write(p []byte) (int, error) { - defer func() { - m.writeCounter++ - }() - - m.logger.Debugw("Write packet", - "length", len(p), - "counter", m.writeCounter, - "simple_ack", m.req.Options.ReadHacks.SimpleAck, - "quick_ack", m.req.Options.ReadHacks.QuickAck, - ) - - header, flags := m.req.MakeHeader(p) - if ce := m.logger.Desugar().Check(zap.DebugLevel, "RPC_PROXY_REQ header"); ce != nil { - ce.Write( - zap.Int("length", len(p)), - zap.Uint32("counter", m.writeCounter), - zap.Bool("simple_ack", m.req.Options.ReadHacks.QuickAck), - zap.Bool("quick_ack", m.req.Options.ReadHacks.SimpleAck), - zap.String("header", fmt.Sprintf("%v", header.Bytes())), - zap.Stringer("flags", flags), - ) - } - header.Write(p) // nolint: gosec - - if _, err := m.conn.Write(header.Bytes()); err != nil { - return 0, err - } - - return len(p), nil -} - -// Logger returns an instance of the logger for this wrapper. -func (m *MTProtoProxy) Logger() *zap.SugaredLogger { - return m.logger -} - -// LocalAddr returns local address of the underlying net.Conn. -func (m *MTProtoProxy) LocalAddr() *net.TCPAddr { - return m.conn.LocalAddr() -} - -// RemoteAddr returns remote address of the underlying net.Conn. -func (m *MTProtoProxy) RemoteAddr() *net.TCPAddr { - return m.conn.RemoteAddr() -} - -// Close closes underlying net.Conn instance. -func (m *MTProtoProxy) Close() error { - return m.conn.Close() -} - -// NewMTProtoProxy creates new RPC wrapper. -func NewMTProtoProxy(conn PacketReadWriteCloser, connOpts *mtproto.ConnectionOpts, - adTag []byte) (PacketReadWriteCloser, error) { - req, err := rpc.NewProxyRequest(connOpts.ClientAddr, conn.LocalAddr(), connOpts, adTag) - if err != nil { - return nil, errors.Annotate(err, "Cannot create new RPC proxy request") - } - - return &MTProtoProxy{ - conn: conn, - logger: conn.Logger().Named("mtproto-proxy"), - req: req, - }, nil -} diff --git a/wrappers/packet/mtproto_frame.go b/wrappers/packet/mtproto_frame.go new file mode 100644 index 0000000..34553d3 --- /dev/null +++ b/wrappers/packet/mtproto_frame.go @@ -0,0 +1,167 @@ +package packet + +import ( + "bytes" + "crypto/aes" + "encoding/binary" + "fmt" + "hash/crc32" + "io" + "io/ioutil" + "net" + + "go.uber.org/zap" + + "mtg/conntypes" +) + +const ( + mtprotoFrameMinMessageLength = 12 + mtprotoFrameMaxMessageLength = 16777216 +) + +var mtprotoFramePadding = []byte{0x04, 0x00, 0x00, 0x00} + +// MTProtoFrame is a wrapper which converts written data to the MTProtoFrame. +// The format of the frame: +// +// [ MSGLEN(4) | SEQNO(4) | MSG(...) | CRC32(4) | PADDING(4*x) ] +// +// MSGLEN is the length of the message + len of seqno and msglen. +// SEQNO is the number of frame in the receive/send sequence. If client +// sends a message with SeqNo 18, it has to receive message with SeqNo 18. +// MSG is the data which has to be written +// CRC32 is the CRC32 checksum of MSGLEN + SEQNO + MSG +// PADDING is custom padding schema to complete frame length to such that +// len(frame) % 16 == 0 +type wrapperMtprotoFrame struct { + parent conntypes.StreamReadWriteCloser + logger *zap.SugaredLogger + readSeqNo int32 + writeSeqNo int32 +} + +func (w *wrapperMtprotoFrame) Read() (conntypes.Packet, error) { // nolint: funlen + buf := &bytes.Buffer{} + sum := crc32.NewIEEE() + writer := io.MultiWriter(buf, sum) + + for { + buf.Reset() + sum.Reset() + + if _, err := io.CopyN(writer, w.parent, 4); err != nil { + return nil, fmt.Errorf("cannot read frame padding: %w", err) + } + + if !bytes.Equal(buf.Bytes(), mtprotoFramePadding) { + break + } + } + + messageLength := binary.LittleEndian.Uint32(buf.Bytes()) + w.logger.Debugw("Read MTProto frame", + "messageLength", messageLength, + "sequence_number", w.readSeqNo, + ) + + if messageLength%4 != 0 || messageLength < mtprotoFrameMinMessageLength || + messageLength > mtprotoFrameMaxMessageLength { + return nil, fmt.Errorf("incorrect frame message length %d", messageLength) + } + + buf.Reset() + buf.Grow(int(messageLength) - 4 - 4) + + if _, err := io.CopyN(writer, w.parent, int64(messageLength)-4-4); err != nil { + return nil, fmt.Errorf("cannot read the message frame: %w", err) + } + + var seqNo int32 + + binary.Read(buf, binary.LittleEndian, &seqNo) // nolint: errcheck + + if seqNo != w.readSeqNo { + return nil, fmt.Errorf("unexpected sequence number %d (wait for %d)", seqNo, w.readSeqNo) + } + + data, _ := ioutil.ReadAll(buf) // nolint: gosec + buf.Reset() + // write to buf, not to writer. This is because we are going to fetch + // crc32 checksum. + if _, err := io.CopyN(buf, w.parent, 4); err != nil { + return nil, fmt.Errorf("cannot read checksum: %w", err) + } + + checksum := binary.LittleEndian.Uint32(buf.Bytes()) + if checksum != sum.Sum32() { + return nil, fmt.Errorf("CRC32 checksum mismatch. wait for %d, got %d", sum.Sum32(), checksum) + } + + w.logger.Debugw("Read MTProto frame", + "messageLength", messageLength, + "sequence_number", w.readSeqNo, + "dataLength", len(data), + "checksum", checksum, + ) + w.readSeqNo++ + + return data, nil +} + +func (w *wrapperMtprotoFrame) Write(p conntypes.Packet) error { + messageLength := 4 + 4 + len(p) + 4 + paddingLength := (aes.BlockSize - messageLength%aes.BlockSize) % aes.BlockSize + + buf := &bytes.Buffer{} + buf.Grow(messageLength + paddingLength) + + binary.Write(buf, binary.LittleEndian, uint32(messageLength)) // nolint: errcheck + binary.Write(buf, binary.LittleEndian, w.writeSeqNo) // nolint: errcheck + buf.Write(p) + + checksum := crc32.ChecksumIEEE(buf.Bytes()) + binary.Write(buf, binary.LittleEndian, checksum) // nolint: errcheck + buf.Write(bytes.Repeat(mtprotoFramePadding, paddingLength/4)) + + w.logger.Debugw("Write MTProto frame", + "length", len(p), + "sequence_number", w.writeSeqNo, + "crc32", checksum, + "frame_length", buf.Len(), + ) + w.writeSeqNo++ + + _, err := w.parent.Write(buf.Bytes()) + + return err +} + +func (w *wrapperMtprotoFrame) Close() error { + return w.parent.Close() +} + +func (w *wrapperMtprotoFrame) Conn() net.Conn { + return w.parent.Conn() +} + +func (w *wrapperMtprotoFrame) Logger() *zap.SugaredLogger { + return w.logger +} + +func (w *wrapperMtprotoFrame) LocalAddr() *net.TCPAddr { + return w.parent.LocalAddr() +} + +func (w *wrapperMtprotoFrame) RemoteAddr() *net.TCPAddr { + return w.parent.RemoteAddr() +} + +func NewMtprotoFrame(parent conntypes.StreamReadWriteCloser, seqNo int32) conntypes.PacketReadWriteCloser { + return &wrapperMtprotoFrame{ + parent: parent, + logger: parent.Logger().Named("mtproto-frame"), + readSeqNo: seqNo, + writeSeqNo: seqNo, + } +} diff --git a/wrappers/packetack/client_abridged.go b/wrappers/packetack/client_abridged.go new file mode 100644 index 0000000..1c7a94c --- /dev/null +++ b/wrappers/packetack/client_abridged.go @@ -0,0 +1,131 @@ +package packetack + +import ( + "bytes" + "fmt" + "io" + "net" + + "go.uber.org/zap" + + "mtg/conntypes" + "mtg/utils" +) + +const ( + clientAbridgedSmallPacketLength = 0x7f + clientAbridgedQuickAckLength = 0x80 + clientAbridgedLargePacketLength = 16777216 // 256 ^ 3 +) + +type wrapperClientAbridged struct { + parent conntypes.StreamReadWriteCloser +} + +func (w *wrapperClientAbridged) Read(acks *conntypes.ConnectionAcks) (conntypes.Packet, error) { + buf := bytes.Buffer{} + + buf.Grow(1) + + if _, err := io.CopyN(&buf, w.parent, 1); err != nil { + return nil, fmt.Errorf("cannot read message length: %w", err) + } + + msgLength := uint32(buf.Bytes()[0]) + buf.Reset() + + if msgLength >= clientAbridgedQuickAckLength { + acks.Quick = true + msgLength -= clientAbridgedQuickAckLength + } + + if msgLength == clientAbridgedSmallPacketLength { + buf.Grow(3) + + if _, err := io.CopyN(&buf, w.parent, 3); err != nil { + return nil, fmt.Errorf("cannot read correct message length: %w", err) + } + + number := utils.Uint24{} + copy(number[:], buf.Bytes()) + msgLength = utils.FromUint24(number) + } + + msgLength *= 4 + + buf.Reset() + buf.Grow(int(msgLength)) + + if _, err := io.CopyN(&buf, w.parent, int64(msgLength)); err != nil { + return nil, fmt.Errorf("cannot read message: %w", err) + } + + return conntypes.Packet(buf.Bytes()), nil +} + +func (w *wrapperClientAbridged) Write(packet conntypes.Packet, acks *conntypes.ConnectionAcks) error { + if len(packet)%4 != 0 { + return fmt.Errorf("incorrect packet length %d", len(packet)) + } + + if acks.Simple { + if _, err := w.parent.Write(utils.ReverseBytes(packet)); err != nil { + return fmt.Errorf("cannot send a simpleacked packet: %w", err) + } + + return nil + } + + packetLength := len(packet) / 4 + + switch { + case packetLength < clientAbridgedSmallPacketLength: + data := append([]byte{byte(packetLength)}, packet...) + if _, err := w.parent.Write(data); err != nil { + return fmt.Errorf("cannot send small packet: %w", err) + } + + return nil + case packetLength < clientAbridgedLargePacketLength: + length24 := utils.ToUint24(uint32(packetLength)) + buf := bytes.Buffer{} + + buf.WriteByte(byte(clientAbridgedSmallPacketLength)) + buf.Write(length24[:]) + buf.Write(packet) + + if _, err := w.parent.Write(buf.Bytes()); err != nil { + return fmt.Errorf("cannot send large packet: %w", err) + } + + return nil + } + + return fmt.Errorf("packet is too big: %d", len(packet)) +} + +func (w *wrapperClientAbridged) Close() error { + return w.parent.Close() +} + +func (w *wrapperClientAbridged) Conn() net.Conn { + return w.parent.Conn() +} + +func (w *wrapperClientAbridged) LocalAddr() *net.TCPAddr { + return w.parent.LocalAddr() +} + +func (w *wrapperClientAbridged) RemoteAddr() *net.TCPAddr { + return w.parent.RemoteAddr() +} + +func (w *wrapperClientAbridged) Logger() *zap.SugaredLogger { + return w.parent.Logger().Named("client-abridged") +} + +func NewClientAbridged(parent conntypes.StreamReadWriteCloser) conntypes.PacketAckFullReadWriteCloser { + return &wrapperClientAbridged{ + parent: parent, + } +} diff --git a/wrappers/packetack/client_intermediate.go b/wrappers/packetack/client_intermediate.go new file mode 100644 index 0000000..d454147 --- /dev/null +++ b/wrappers/packetack/client_intermediate.go @@ -0,0 +1,90 @@ +package packetack + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "net" + + "go.uber.org/zap" + + "mtg/conntypes" +) + +const clientIntermediateQuickAckLength = 0x80000000 + +type wrapperClientIntermediate struct { + parent conntypes.StreamReadWriteCloser +} + +func (w *wrapperClientIntermediate) Read(acks *conntypes.ConnectionAcks) (conntypes.Packet, error) { + buf := bytes.Buffer{} + + buf.Grow(4) + + if _, err := io.CopyN(&buf, w.parent, 4); err != nil { + return nil, fmt.Errorf("cannot read message length: %w", err) + } + + length := binary.LittleEndian.Uint32(buf.Bytes()) + + if length > clientIntermediateQuickAckLength { + acks.Quick = true + length -= clientIntermediateQuickAckLength + } + + buf.Reset() + buf.Grow(int(length)) + + if _, err := io.CopyN(&buf, w.parent, int64(length)); err != nil { + return nil, fmt.Errorf("cannot read the message: %w", err) + } + + return buf.Bytes(), nil +} + +func (w *wrapperClientIntermediate) Write(packet conntypes.Packet, acks *conntypes.ConnectionAcks) error { + if acks.Simple { + if _, err := w.parent.Write(packet); err != nil { + return fmt.Errorf("cannot send simpleacked packet: %w", err) + } + + return nil + } + + length := [4]byte{} + binary.LittleEndian.PutUint32(length[:], uint32(len(packet))) + + if _, err := w.parent.Write(append(length[:], packet...)); err != nil { + return fmt.Errorf("cannot send packet: %w", err) + } + + return nil +} + +func (w *wrapperClientIntermediate) Close() error { + return w.parent.Close() +} + +func (w *wrapperClientIntermediate) Conn() net.Conn { + return w.parent.Conn() +} + +func (w *wrapperClientIntermediate) LocalAddr() *net.TCPAddr { + return w.parent.LocalAddr() +} + +func (w *wrapperClientIntermediate) RemoteAddr() *net.TCPAddr { + return w.parent.RemoteAddr() +} + +func (w *wrapperClientIntermediate) Logger() *zap.SugaredLogger { + return w.parent.Logger().Named("client-intermediate") +} + +func NewClientIntermediate(parent conntypes.StreamReadWriteCloser) conntypes.PacketAckFullReadWriteCloser { + return &wrapperClientIntermediate{ + parent: parent, + } +} diff --git a/wrappers/packetack/client_intermediate_secure.go b/wrappers/packetack/client_intermediate_secure.go new file mode 100644 index 0000000..85a9888 --- /dev/null +++ b/wrappers/packetack/client_intermediate_secure.go @@ -0,0 +1,63 @@ +package packetack + +import ( + "bytes" + "encoding/binary" + "fmt" + "math/rand" + + "go.uber.org/zap" + + "mtg/conntypes" +) + +type wrapperClientIntermediateSecure struct { + wrapperClientIntermediate +} + +func (w *wrapperClientIntermediateSecure) Read(acks *conntypes.ConnectionAcks) (conntypes.Packet, error) { + data, err := w.wrapperClientIntermediate.Read(acks) + if err != nil { + return nil, err + } + + length := len(data) - (len(data) % 4) + + return data[:length], nil +} + +func (w *wrapperClientIntermediateSecure) Write(packet conntypes.Packet, acks *conntypes.ConnectionAcks) error { + if acks.Simple { + if _, err := w.parent.Write(packet); err != nil { + return fmt.Errorf("cannot send simpleacked packet: %w", err) + } + + return nil + } + + buf := bytes.Buffer{} + paddingLength := rand.Intn(4) + buf.Grow(4 + len(packet) + paddingLength) + + binary.Write(&buf, binary.LittleEndian, uint32(len(packet)+paddingLength)) // nolint: errcheck + buf.Write(packet) + buf.Write(make([]byte, paddingLength)) + + if _, err := w.parent.Write(buf.Bytes()); err != nil { + return fmt.Errorf("cannot send packet: %w", err) + } + + return nil +} + +func (w *wrapperClientIntermediateSecure) Logger() *zap.SugaredLogger { + return w.parent.Logger().Named("client-intermediate-secure") +} + +func NewClientIntermediateSecure(parent conntypes.StreamReadWriteCloser) conntypes.PacketAckFullReadWriteCloser { + return &wrapperClientIntermediateSecure{ + wrapperClientIntermediate: wrapperClientIntermediate{ + parent: parent, + }, + } +} diff --git a/wrappers/packetack/proxy.go b/wrappers/packetack/proxy.go new file mode 100644 index 0000000..6129615 --- /dev/null +++ b/wrappers/packetack/proxy.go @@ -0,0 +1,106 @@ +package packetack + +import ( + "bytes" + "encoding/binary" + "fmt" + "net" + + "mtg/config" + "mtg/conntypes" + "mtg/hub" + "mtg/mtproto/rpc" + "mtg/protocol" +) + +type wrapperProxy struct { + request *protocol.TelegramRequest + proxy *hub.ProxyConn + clientIPPort []byte + ourIPPort []byte + flags rpc.ProxyRequestFlags +} + +func (w *wrapperProxy) Write(packet conntypes.Packet, acks *conntypes.ConnectionAcks) error { + buf := bytes.Buffer{} + + flags := w.flags + if acks.Quick { + flags |= rpc.ProxyRequestFlagsQuickAck + } + + if bytes.HasPrefix(packet, rpc.ProxyRequestFlagsEncryptedPrefix[:]) { + flags |= rpc.ProxyRequestFlagsEncrypted + } + + buf.Write(rpc.TagProxyRequest) + buf.Write(flags.Bytes()) + buf.Write(w.request.ConnID[:]) + buf.Write(w.clientIPPort) + buf.Write(w.ourIPPort) + buf.Write(rpc.ProxyRequestExtraSize) + buf.Write(rpc.ProxyRequestProxyTag) + buf.WriteByte(byte(len(config.C.AdTag))) + buf.Write(config.C.AdTag) + buf.Write(make([]byte, (4-buf.Len()%4)%4)) + buf.Write(packet) + + return w.proxy.Write(buf.Bytes()) +} + +func (w *wrapperProxy) Read(acks *conntypes.ConnectionAcks) (conntypes.Packet, error) { + resp, err := w.proxy.Read() + if err != nil { + return nil, fmt.Errorf("cannot read a response: %w", err) + } + + if resp.Type == rpc.ProxyResponseTypeSimpleAck { + acks.Simple = true + } + + return resp.Payload, nil +} + +func (w *wrapperProxy) Close() error { + w.proxy.Close() + return nil +} + +func NewProxy(request *protocol.TelegramRequest) (conntypes.PacketAckReadWriteCloser, error) { + flags := rpc.ProxyRequestFlagsHasAdTag | rpc.ProxyRequestFlagsMagic | rpc.ProxyRequestFlagsExtMode2 + + switch request.ClientProtocol.ConnectionType() { + case conntypes.ConnectionTypeAbridged: + flags |= rpc.ProxyRequestFlagsAbdridged + case conntypes.ConnectionTypeIntermediate: + flags |= rpc.ProxyRequestFlagsIntermediate + case conntypes.ConnectionTypeSecure: + flags |= rpc.ProxyRequestFlagsIntermediate | rpc.ProxyRequestFlagsPad + default: + panic("unknown connection type") + } + + proxy, err := hub.Hub.Register(request) + if err != nil { + return nil, fmt.Errorf("cannot make a new proxy wrapper: %w", err) + } + + return &wrapperProxy{ + flags: flags, + request: request, + proxy: proxy, + clientIPPort: proxyGetIPPort(request.ClientConn.RemoteAddr()), + ourIPPort: proxyGetIPPort(request.ClientConn.LocalAddr()), + }, nil +} + +func proxyGetIPPort(addr *net.TCPAddr) []byte { + rv := [16 + 4]byte{} + port := [4]byte{} + + copy(rv[:16], addr.IP.To16()) + binary.LittleEndian.PutUint32(port[:], uint32(addr.Port)) + copy(rv[16:], port[:]) + + return rv[:] +} diff --git a/wrappers/stream/base.go b/wrappers/stream/base.go new file mode 100644 index 0000000..3780ebd --- /dev/null +++ b/wrappers/stream/base.go @@ -0,0 +1,21 @@ +package stream + +import ( + "net" + + "mtg/conntypes" +) + +func NewClientConn(parent net.Conn, connID conntypes.ConnID) conntypes.StreamReadWriteCloser { + conn := newConn(parent, connID, connPurposeClient) + conn = NewTrafficStats(conn) + + return conn +} + +func NewTelegramConn(dc conntypes.DC, parent net.Conn) conntypes.StreamReadWriteCloser { + conn := newConn(parent, conntypes.ConnID{}, connPurposeTelegram) + conn = NewTelegramStats(dc, conn) + + return conn +} diff --git a/wrappers/stream/blockcipher.go b/wrappers/stream/blockcipher.go new file mode 100644 index 0000000..a5463b9 --- /dev/null +++ b/wrappers/stream/blockcipher.go @@ -0,0 +1,97 @@ +package stream + +import ( + "crypto/aes" + "crypto/cipher" + "fmt" + "net" + "time" + + "go.uber.org/zap" + + "mtg/conntypes" + "mtg/utils" +) + +type wrapperBlockCipher struct { + bufferedReader + + parent conntypes.StreamReadWriteCloser + encryptor cipher.BlockMode + decryptor cipher.BlockMode +} + +func (w *wrapperBlockCipher) Write(p []byte) (int, error) { + encrypted, err := w.encrypt(p) + if err != nil { + return 0, err + } + + return w.parent.Write(encrypted) +} + +func (w *wrapperBlockCipher) WriteTimeout(p []byte, timeout time.Duration) (int, error) { + encrypted, err := w.encrypt(p) + if err != nil { + return 0, err + } + + return w.parent.WriteTimeout(encrypted, timeout) +} + +func (w *wrapperBlockCipher) encrypt(p []byte) ([]byte, error) { + if len(p)%aes.BlockSize > 0 { + return nil, fmt.Errorf("incorrect block size %d", len(p)) + } + + encrypted := make([]byte, len(p)) + w.encryptor.CryptBlocks(encrypted, p) + + return encrypted, nil +} + +func (w *wrapperBlockCipher) Close() error { + return w.parent.Close() +} + +func (w *wrapperBlockCipher) Conn() net.Conn { + return w.parent.Conn() +} + +func (w *wrapperBlockCipher) Logger() *zap.SugaredLogger { + return w.parent.Logger().Named("block-cipher") +} + +func (w *wrapperBlockCipher) LocalAddr() *net.TCPAddr { + return w.parent.LocalAddr() +} + +func (w *wrapperBlockCipher) RemoteAddr() *net.TCPAddr { + return w.parent.RemoteAddr() +} + +func newBlockCipher(parent conntypes.StreamReadWriteCloser, + encryptor, decryptor cipher.BlockMode) conntypes.StreamReadWriteCloser { + cipher := &wrapperBlockCipher{ + parent: parent, + encryptor: encryptor, + decryptor: decryptor, + } + + cipher.readFunc = func() ([]byte, error) { + var currentBuffer []byte + for len(currentBuffer) == 0 || len(currentBuffer)%aes.BlockSize != 0 { + rv, err := utils.ReadFull(cipher.parent) + if err != nil { + return nil, fmt.Errorf("cannot read data: %w", err) + } + + currentBuffer = append(currentBuffer, rv...) + } + cipher.decryptor.CryptBlocks(currentBuffer, currentBuffer) + + return currentBuffer, nil + } + + return cipher +} diff --git a/wrappers/stream/buffered_reader.go b/wrappers/stream/buffered_reader.go new file mode 100644 index 0000000..6a551f7 --- /dev/null +++ b/wrappers/stream/buffered_reader.go @@ -0,0 +1,44 @@ +package stream + +import ( + "bytes" + "time" +) + +type bufferedReaderReadFunc func() ([]byte, error) + +type bufferedReader struct { + buf bytes.Buffer + readFunc bufferedReaderReadFunc +} + +func (b *bufferedReader) Read(p []byte) (int, error) { + if b.buf.Len() > 0 { + return b.flush(p) + } + + res, err := b.readFunc() + if err != nil { + return 0, err + } + + b.buf.Write(res) + + return b.flush(p) +} + +func (b *bufferedReader) ReadTimeout(p []byte, _ time.Duration) (int, error) { + return b.Read(p) +} + +func (b *bufferedReader) flush(p []byte) (int, error) { + if b.buf.Len() > len(p) { + return b.buf.Read(p) + } + + sizeToReturn := b.buf.Len() + copy(p, b.buf.Bytes()) + b.buf.Reset() + + return sizeToReturn, nil +} diff --git a/wrappers/stream/conn.go b/wrappers/stream/conn.go new file mode 100644 index 0000000..fc85e3f --- /dev/null +++ b/wrappers/stream/conn.go @@ -0,0 +1,119 @@ +package stream + +import ( + "fmt" + "net" + "time" + + "go.uber.org/zap" + + "mtg/config" + "mtg/conntypes" +) + +type connPurpose uint8 + +const ( + connPurposeClient connPurpose = 1 << iota + connPurposeTelegram +) + +type wrapperConn struct { + parent net.Conn + connID conntypes.ConnID + logger *zap.SugaredLogger + localAddr *net.TCPAddr + remoteAddr *net.TCPAddr +} + +func (w *wrapperConn) WriteTimeout(p []byte, timeout time.Duration) (int, error) { + if err := w.parent.SetWriteDeadline(time.Now().Add(timeout)); err != nil { + w.Close() + return 0, fmt.Errorf("cannot set write deadline to the socket: %w", err) + } + + return w.Write(p) +} + +func (w *wrapperConn) Write(p []byte) (int, error) { + n, err := w.parent.Write(p) + w.logger.Debugw("write to stream", "bytes", n, "error", err) + + if err != nil { + w.Close() // nolint: gosec + } + + return n, err +} + +func (w *wrapperConn) ReadTimeout(p []byte, timeout time.Duration) (int, error) { + if err := w.parent.SetReadDeadline(time.Now().Add(timeout)); err != nil { + w.Close() + return 0, fmt.Errorf("cannot set read deadline to the socket: %w", err) + } + + return w.Read(p) +} + +func (w *wrapperConn) Read(p []byte) (int, error) { + n, err := w.parent.Read(p) + w.logger.Debugw("Read from stream", "bytes", n, "error", err) + + if err != nil { + w.Close() + } + + return n, err +} + +func (w *wrapperConn) Close() error { + w.logger.Debugw("Close connection") + return w.parent.Close() +} + +func (w *wrapperConn) Conn() net.Conn { + return w.parent +} + +func (w *wrapperConn) Logger() *zap.SugaredLogger { + return w.logger +} + +func (w *wrapperConn) LocalAddr() *net.TCPAddr { + return w.localAddr +} + +func (w *wrapperConn) RemoteAddr() *net.TCPAddr { + return w.remoteAddr +} + +func newConn(parent net.Conn, + connID conntypes.ConnID, + purpose connPurpose) conntypes.StreamReadWriteCloser { + localAddr := *parent.LocalAddr().(*net.TCPAddr) + + if parent.RemoteAddr().(*net.TCPAddr).IP.To4() != nil { + if config.C.PublicIPv4.IP != nil { + localAddr.IP = config.C.PublicIPv4.IP + } + } else if config.C.PublicIPv6.IP != nil { + localAddr.IP = config.C.PublicIPv6.IP + } + + logger := zap.S().With( + "local_address", localAddr, + "remote_address", parent.RemoteAddr(), + ).Named("conn") + + if purpose == connPurposeClient { + logger = logger.With("connection_id", connID.String()) + } + + return &wrapperConn{ + parent: parent, + connID: connID, + logger: logger, + remoteAddr: parent.RemoteAddr().(*net.TCPAddr), + localAddr: &localAddr, + } +} diff --git a/wrappers/stream/ctx.go b/wrappers/stream/ctx.go new file mode 100644 index 0000000..3a89677 --- /dev/null +++ b/wrappers/stream/ctx.go @@ -0,0 +1,89 @@ +package stream + +import ( + "context" + "fmt" + "net" + "time" + + "go.uber.org/zap" + + "mtg/conntypes" +) + +type wrapperCtx struct { + parent conntypes.StreamReadWriteCloser + ctx context.Context + cancel context.CancelFunc +} + +func (w *wrapperCtx) WriteTimeout(p []byte, timeout time.Duration) (int, error) { + select { + case <-w.ctx.Done(): + w.Close() + return 0, fmt.Errorf("cannot write because context was closed: %w", w.ctx.Err()) + default: + return w.parent.WriteTimeout(p, timeout) + } +} + +func (w *wrapperCtx) Write(p []byte) (int, error) { + select { + case <-w.ctx.Done(): + w.Close() + return 0, fmt.Errorf("cannot write because context was closed: %w", w.ctx.Err()) + default: + return w.parent.Write(p) + } +} + +func (w *wrapperCtx) ReadTimeout(p []byte, timeout time.Duration) (int, error) { + select { + case <-w.ctx.Done(): + w.Close() + return 0, fmt.Errorf("cannot write because context was closed: %w", w.ctx.Err()) + default: + return w.parent.ReadTimeout(p, timeout) + } +} + +func (w *wrapperCtx) Read(p []byte) (int, error) { + select { + case <-w.ctx.Done(): + w.Close() + return 0, fmt.Errorf("cannot write because context was closed: %w", w.ctx.Err()) + default: + return w.parent.Read(p) + } +} + +func (w *wrapperCtx) Close() error { + w.cancel() + return w.parent.Close() +} + +func (w *wrapperCtx) Conn() net.Conn { + return w.parent.Conn() +} + +func (w *wrapperCtx) Logger() *zap.SugaredLogger { + return w.parent.Logger().Named("ctx") +} + +func (w *wrapperCtx) LocalAddr() *net.TCPAddr { + return w.parent.LocalAddr() +} + +func (w *wrapperCtx) RemoteAddr() *net.TCPAddr { + return w.parent.RemoteAddr() +} + +func NewCtx(ctx context.Context, + cancel context.CancelFunc, + parent conntypes.StreamReadWriteCloser) conntypes.StreamReadWriteCloser { + return &wrapperCtx{ + parent: parent, + ctx: ctx, + cancel: cancel, + } +} diff --git a/wrappers/stream/faketls.go b/wrappers/stream/faketls.go new file mode 100644 index 0000000..10ef688 --- /dev/null +++ b/wrappers/stream/faketls.go @@ -0,0 +1,97 @@ +package stream + +import ( + "errors" + "fmt" + "net" + "time" + + "go.uber.org/zap" + + "mtg/conntypes" + "mtg/tlstypes" +) + +type wrapperFakeTLS struct { + bufferedReader + + parent conntypes.StreamReadWriteCloser +} + +func (w *wrapperFakeTLS) Write(p []byte) (int, error) { + return w.write(p, func(b []byte) (int, error) { + return w.parent.Write(b) + }) +} + +func (w *wrapperFakeTLS) WriteTimeout(p []byte, timeout time.Duration) (int, error) { + startTime := time.Now() + + return w.write(p, func(b []byte) (int, error) { + elapsed := time.Since(startTime) + if elapsed > timeout { + return w.parent.WriteTimeout(b, timeout-elapsed) + } + return 0, errors.New("timeout") + }) +} + +func (w *wrapperFakeTLS) write(p []byte, writeFunc func([]byte) (int, error)) (int, error) { + sum := 0 + + for _, v := range tlstypes.MakeRecords(p) { + _, err := writeFunc(v.Bytes()) + if err != nil { + return sum, err + } + + sum += len(v.Data.Bytes()) + } + + return sum, nil +} + +func (w *wrapperFakeTLS) Conn() net.Conn { + return w.parent.Conn() +} + +func (w *wrapperFakeTLS) Logger() *zap.SugaredLogger { + return w.parent.Logger().Named("faketls") +} + +func (w *wrapperFakeTLS) LocalAddr() *net.TCPAddr { + return w.parent.LocalAddr() +} + +func (w *wrapperFakeTLS) RemoteAddr() *net.TCPAddr { + return w.parent.RemoteAddr() +} + +func (w *wrapperFakeTLS) Close() error { + return w.parent.Close() +} + +func NewFakeTLS(socket conntypes.StreamReadWriteCloser) conntypes.StreamReadWriteCloser { + faketls := &wrapperFakeTLS{ + parent: socket, + } + + faketls.readFunc = func() ([]byte, error) { + for { + rec, err := tlstypes.ReadRecord(faketls.parent) + if err != nil { + return nil, err + } + + switch rec.Type { + case tlstypes.RecordTypeChangeCipherSpec: + case tlstypes.RecordTypeApplicationData: + return rec.Data.Bytes(), nil + default: + return nil, fmt.Errorf("unsupported record type %v", rec.Type) + } + } + } + + return faketls +} diff --git a/wrappers/mtproto_cipher.go b/wrappers/stream/mtproto_cipher.go similarity index 54% rename from wrappers/mtproto_cipher.go rename to wrappers/stream/mtproto_cipher.go index 2f65b0b..350dbbb 100644 --- a/wrappers/mtproto_cipher.go +++ b/wrappers/stream/mtproto_cipher.go @@ -1,66 +1,83 @@ -package wrappers +package stream import ( "bytes" "crypto/aes" "crypto/cipher" - "crypto/md5" // nolint: gas + "crypto/md5" // nolint: gosec "crypto/sha1" // nolint: gosec "encoding/binary" "net" - "github.com/9seconds/mtg/mtproto/rpc" - "github.com/9seconds/mtg/utils" + "mtg/conntypes" + "mtg/mtproto/rpc" + "mtg/utils" ) -type cipherPurpose uint8 +type mtprotoCipherPurpose uint8 const ( - cipherPurposeClient cipherPurpose = iota - cipherPurposeServer + mtprotoCipherPurposeClient mtprotoCipherPurpose = iota + mtprotoCipherPurposeServer ) -var emptyIP = [4]byte{0x00, 0x00, 0x00, 0x00} +var mtprotoEmptyIP = [4]byte{0x00, 0x00, 0x00, 0x00} -// NewMiddleProxyCipher creates new block cipher to proxy<->telegram -// connection. -func NewMiddleProxyCipher(conn StreamReadWriteCloser, - req *rpc.NonceRequest, resp *rpc.NonceResponse, secret []byte) StreamReadWriteCloser { - localAddr := conn.LocalAddr() - remoteAddr := conn.RemoteAddr() +func NewMiddleProxyCipher(parent conntypes.StreamReadWriteCloser, + req *rpc.NonceRequest, + resp *rpc.NonceResponse, + secret []byte) conntypes.StreamReadWriteCloser { + localAddr := parent.LocalAddr() + remoteAddr := parent.RemoteAddr() - encKey, encIV := deriveKeys(cipherPurposeClient, req, resp, localAddr, remoteAddr, secret) - decKey, decIV := deriveKeys(cipherPurposeServer, req, resp, localAddr, remoteAddr, secret) + encKey, encIV := mtprotoDeriveKeys(mtprotoCipherPurposeClient, + req, + resp, + localAddr, + remoteAddr, + secret) + decKey, decIV := mtprotoDeriveKeys(mtprotoCipherPurposeServer, + req, + resp, + localAddr, + remoteAddr, + secret) - enc, _ := makeEncrypterDecrypter(encKey, encIV) - _, dec := makeEncrypterDecrypter(decKey, decIV) + enc, _ := mtprotoMakeEncrypterDecrypter(encKey, encIV) + _, dec := mtprotoMakeEncrypterDecrypter(decKey, decIV) - return NewBlockCipher(conn, enc, dec) + return newBlockCipher(parent, enc, dec) } -func deriveKeys(purpose cipherPurpose, req *rpc.NonceRequest, resp *rpc.NonceResponse, - client, remote *net.TCPAddr, secret []byte) ([]byte, []byte) { +func mtprotoDeriveKeys(purpose mtprotoCipherPurpose, + req *rpc.NonceRequest, + resp *rpc.NonceResponse, + client, remote *net.TCPAddr, + secret []byte) ([]byte, []byte) { message := bytes.Buffer{} message.Write(resp.Nonce) // nolint: gosec message.Write(req.Nonce) // nolint: gosec message.Write(req.CryptoTS) // nolint: gosec - clientIPv4 := emptyIP[:] - serverIPv4 := emptyIP[:] + clientIPv4 := mtprotoEmptyIP[:] + serverIPv4 := mtprotoEmptyIP[:] + if client.IP.To4() != nil { clientIPv4 = utils.ReverseBytes(client.IP.To4()) serverIPv4 = utils.ReverseBytes(remote.IP.To4()) } + message.Write(serverIPv4) // nolint: gosec var port [2]byte + binary.LittleEndian.PutUint16(port[:], uint16(client.Port)) message.Write(port[:]) // nolint: gosec switch purpose { - case cipherPurposeClient: + case mtprotoCipherPurposeClient: message.WriteString("CLIENT") // nolint: gosec - case cipherPurposeServer: + case mtprotoCipherPurposeServer: message.WriteString("SERVER") // nolint: gosec default: panic("Unexpected cipher purpose") @@ -76,6 +93,7 @@ func deriveKeys(purpose cipherPurpose, req *rpc.NonceRequest, resp *rpc.NonceRes message.Write(client.IP.To16()) // nolint: gosec message.Write(remote.IP.To16()) // nolint: gosec } + message.Write(req.Nonce) // nolint: gosec data := message.Bytes() @@ -88,7 +106,7 @@ func deriveKeys(purpose cipherPurpose, req *rpc.NonceRequest, resp *rpc.NonceRes return key, iv[:] } -func makeEncrypterDecrypter(key, iv []byte) (cipher.BlockMode, cipher.BlockMode) { +func mtprotoMakeEncrypterDecrypter(key, iv []byte) (cipher.BlockMode, cipher.BlockMode) { block, err := aes.NewCipher(key) if err != nil { panic(err) diff --git a/wrappers/stream/obfuscated2.go b/wrappers/stream/obfuscated2.go new file mode 100644 index 0000000..0368a2b --- /dev/null +++ b/wrappers/stream/obfuscated2.go @@ -0,0 +1,85 @@ +package stream + +import ( + "crypto/cipher" + "fmt" + "net" + "time" + + "go.uber.org/zap" + + "mtg/conntypes" +) + +type wrapperObfuscated2 struct { + encryptor cipher.Stream + decryptor cipher.Stream + parent conntypes.StreamReadWriteCloser +} + +func (w *wrapperObfuscated2) ReadTimeout(p []byte, timeout time.Duration) (int, error) { + n, err := w.parent.ReadTimeout(p, timeout) + if err != nil { + return 0, fmt.Errorf("cannot read stream ciphered data: %w", err) + } + + w.decryptor.XORKeyStream(p, p[:n]) + + return n, nil +} + +func (w *wrapperObfuscated2) Read(p []byte) (int, error) { + n, err := w.parent.Read(p) + if err != nil { + return n, err + } + + w.decryptor.XORKeyStream(p, p[:n]) + + return n, nil +} + +func (w *wrapperObfuscated2) WriteTimeout(p []byte, timeout time.Duration) (int, error) { + buf := make([]byte, len(p)) + copy(buf, p) + w.encryptor.XORKeyStream(buf, buf) + + return w.parent.WriteTimeout(buf, timeout) +} + +func (w *wrapperObfuscated2) Write(p []byte) (int, error) { + buf := make([]byte, len(p)) + copy(buf, p) + w.encryptor.XORKeyStream(buf, buf) + + return w.parent.Write(buf) +} + +func (w *wrapperObfuscated2) Conn() net.Conn { + return w.parent.Conn() +} + +func (w *wrapperObfuscated2) Logger() *zap.SugaredLogger { + return w.parent.Logger().Named("obfuscated2") +} + +func (w *wrapperObfuscated2) LocalAddr() *net.TCPAddr { + return w.parent.LocalAddr() +} + +func (w *wrapperObfuscated2) RemoteAddr() *net.TCPAddr { + return w.parent.RemoteAddr() +} + +func (w *wrapperObfuscated2) Close() error { + return w.parent.Close() +} + +func NewObfuscated2(socket conntypes.StreamReadWriteCloser, + encryptor, decryptor cipher.Stream) conntypes.StreamReadWriteCloser { + return &wrapperObfuscated2{ + parent: socket, + encryptor: encryptor, + decryptor: decryptor, + } +} diff --git a/wrappers/stream/rewind.go b/wrappers/stream/rewind.go new file mode 100644 index 0000000..1472012 --- /dev/null +++ b/wrappers/stream/rewind.go @@ -0,0 +1,104 @@ +package stream + +import ( + "bytes" + "io" + "net" + "sync" + "time" + + "go.uber.org/zap" + + "mtg/conntypes" +) + +type ReadWriteCloseRewinder interface { + conntypes.StreamReadWriteCloser + Rewind() +} + +type wrapperRewind struct { + parent conntypes.StreamReadWriteCloser + buf bytes.Buffer + mutex sync.Mutex + rewinded bool +} + +func (w *wrapperRewind) Write(p []byte) (int, error) { + return w.parent.Write(p) +} + +func (w *wrapperRewind) WriteTimeout(p []byte, timeout time.Duration) (int, error) { + return w.parent.WriteTimeout(p, timeout) +} + +func (w *wrapperRewind) Read(p []byte) (int, error) { + w.mutex.Lock() + defer w.mutex.Unlock() + + if w.rewinded { + if n, err := w.buf.Read(p); err != io.EOF { + return n, err + } + } + + n, err := w.parent.Read(p) + + if !w.rewinded { + w.buf.Write(p[:n]) + } + + return n, err +} + +func (w *wrapperRewind) ReadTimeout(p []byte, timeout time.Duration) (int, error) { + w.mutex.Lock() + defer w.mutex.Unlock() + + if w.rewinded { + if n, err := w.buf.Read(p); err != io.EOF { + return n, err + } + } + + n, err := w.parent.ReadTimeout(p, timeout) + + if !w.rewinded { + w.buf.Write(p[:n]) + } + + return n, err +} + +func (w *wrapperRewind) Conn() net.Conn { + return w.parent.Conn() +} + +func (w *wrapperRewind) Logger() *zap.SugaredLogger { + return w.parent.Logger().Named("rewinded") +} + +func (w *wrapperRewind) LocalAddr() *net.TCPAddr { + return w.parent.LocalAddr() +} + +func (w *wrapperRewind) RemoteAddr() *net.TCPAddr { + return w.parent.RemoteAddr() +} + +func (w *wrapperRewind) Close() error { + w.buf.Reset() + return w.parent.Close() +} + +func (w *wrapperRewind) Rewind() { + w.mutex.Lock() + w.rewinded = true + w.mutex.Unlock() +} + +func NewRewind(parent conntypes.StreamReadWriteCloser) ReadWriteCloseRewinder { + return &wrapperRewind{ + parent: parent, + } +} diff --git a/wrappers/stream/stats_telegram.go b/wrappers/stream/stats_telegram.go new file mode 100644 index 0000000..3eceafa --- /dev/null +++ b/wrappers/stream/stats_telegram.go @@ -0,0 +1,72 @@ +package stream + +import ( + "net" + "sync" + "time" + + "go.uber.org/zap" + + "mtg/conntypes" + "mtg/stats" +) + +type wrapperTelegramStats struct { + parent conntypes.StreamReadWriteCloser + dc conntypes.DC + once sync.Once +} + +func (w *wrapperTelegramStats) Write(p []byte) (int, error) { + return w.parent.Write(p) +} + +func (w *wrapperTelegramStats) WriteTimeout(p []byte, timeout time.Duration) (int, error) { + return w.parent.WriteTimeout(p, timeout) +} + +func (w *wrapperTelegramStats) Read(p []byte) (int, error) { + return w.parent.Read(p) +} + +func (w *wrapperTelegramStats) ReadTimeout(p []byte, timeout time.Duration) (int, error) { + return w.parent.ReadTimeout(p, timeout) +} + +func (w *wrapperTelegramStats) Conn() net.Conn { + return w.parent.Conn() +} + +func (w *wrapperTelegramStats) Logger() *zap.SugaredLogger { + return w.parent.Logger().Named("stats-telegram") +} + +func (w *wrapperTelegramStats) LocalAddr() *net.TCPAddr { + return w.parent.LocalAddr() +} + +func (w *wrapperTelegramStats) RemoteAddr() *net.TCPAddr { + return w.parent.RemoteAddr() +} + +func (w *wrapperTelegramStats) Close() error { + var err error + + w.once.Do(func() { + err = w.parent.Close() + stats.Stats.TelegramDisconnected(w.dc, w.RemoteAddr()) + }) + + return err +} + +func NewTelegramStats(dc conntypes.DC, parent conntypes.StreamReadWriteCloser) conntypes.StreamReadWriteCloser { + conn := &wrapperTelegramStats{ + parent: parent, + dc: dc, + } + + stats.Stats.TelegramConnected(dc, parent.RemoteAddr()) + + return conn +} diff --git a/wrappers/stream/stats_traffic.go b/wrappers/stream/stats_traffic.go new file mode 100644 index 0000000..90ed926 --- /dev/null +++ b/wrappers/stream/stats_traffic.go @@ -0,0 +1,67 @@ +package stream + +import ( + "net" + "time" + + "go.uber.org/zap" + + "mtg/conntypes" + "mtg/stats" +) + +type wrapperTrafficStats struct { + parent conntypes.StreamReadWriteCloser +} + +func (w *wrapperTrafficStats) Write(p []byte) (int, error) { + n, err := w.parent.Write(p) + stats.Stats.EgressTraffic(n) + + return n, err +} + +func (w *wrapperTrafficStats) WriteTimeout(p []byte, timeout time.Duration) (int, error) { + n, err := w.parent.WriteTimeout(p, timeout) + stats.Stats.EgressTraffic(n) + + return n, err +} + +func (w *wrapperTrafficStats) Read(p []byte) (int, error) { + n, err := w.parent.Read(p) + stats.Stats.IngressTraffic(n) + + return n, err +} + +func (w *wrapperTrafficStats) ReadTimeout(p []byte, timeout time.Duration) (int, error) { + n, err := w.parent.ReadTimeout(p, timeout) + stats.Stats.IngressTraffic(n) + + return n, err +} + +func (w *wrapperTrafficStats) Conn() net.Conn { + return w.parent.Conn() +} + +func (w *wrapperTrafficStats) Logger() *zap.SugaredLogger { + return w.parent.Logger().Named("stats-traffic") +} + +func (w *wrapperTrafficStats) LocalAddr() *net.TCPAddr { + return w.parent.LocalAddr() +} + +func (w *wrapperTrafficStats) RemoteAddr() *net.TCPAddr { + return w.parent.RemoteAddr() +} + +func (w *wrapperTrafficStats) Close() error { + return w.parent.Close() +} + +func NewTrafficStats(parent conntypes.StreamReadWriteCloser) conntypes.StreamReadWriteCloser { + return &wrapperTrafficStats{parent} +} diff --git a/wrappers/stream/timeout.go b/wrappers/stream/timeout.go new file mode 100644 index 0000000..ce35675 --- /dev/null +++ b/wrappers/stream/timeout.go @@ -0,0 +1,61 @@ +package stream + +import ( + "net" + "time" + + "go.uber.org/zap" + + "mtg/conntypes" +) + +const ( + timeoutRead = 2 * time.Minute + timeoutWrite = 2 * time.Minute +) + +type wrapperTimeout struct { + parent conntypes.StreamReadWriteCloser +} + +func (w *wrapperTimeout) WriteTimeout(p []byte, timeout time.Duration) (int, error) { + return w.parent.WriteTimeout(p, timeout) +} + +func (w *wrapperTimeout) Write(p []byte) (int, error) { + return w.parent.WriteTimeout(p, timeoutWrite) +} + +func (w *wrapperTimeout) ReadTimeout(p []byte, timeout time.Duration) (int, error) { + return w.parent.ReadTimeout(p, timeout) +} + +func (w *wrapperTimeout) Read(p []byte) (int, error) { + return w.parent.ReadTimeout(p, timeoutRead) +} + +func (w *wrapperTimeout) Close() error { + return w.parent.Close() +} + +func (w *wrapperTimeout) Conn() net.Conn { + return w.parent.Conn() +} + +func (w *wrapperTimeout) Logger() *zap.SugaredLogger { + return w.parent.Logger().Named("timeout") +} + +func (w *wrapperTimeout) LocalAddr() *net.TCPAddr { + return w.parent.LocalAddr() +} + +func (w *wrapperTimeout) RemoteAddr() *net.TCPAddr { + return w.parent.RemoteAddr() +} + +func NewTimeout(parent conntypes.StreamReadWriteCloser) conntypes.StreamReadWriteCloser { + return &wrapperTimeout{ + parent: parent, + } +} diff --git a/wrappers/streamcipher.go b/wrappers/streamcipher.go deleted file mode 100644 index 5c0b137..0000000 --- a/wrappers/streamcipher.go +++ /dev/null @@ -1,73 +0,0 @@ -package wrappers - -import ( - "bytes" - "crypto/cipher" - "net" - - "github.com/juju/errors" - "go.uber.org/zap" -) - -// StreamCipher is a wrapper which encrypts/decrypts stream with AES-CTR -// (as a part of obfuscated2 protocol). -type StreamCipher struct { - encryptor cipher.Stream - decryptor cipher.Stream - conn StreamReadWriteCloser - logger *zap.SugaredLogger -} - -func (s *StreamCipher) Read(p []byte) (int, error) { - n, err := s.conn.Read(p) - if err != nil { - return 0, errors.Annotate(err, "Cannot read stream ciphered data") - } - s.decryptor.XORKeyStream(p, p[:n]) - - return n, nil -} - -func (s *StreamCipher) Write(p []byte) (int, error) { - buf := streamCipherBufferPool.Get().(*bytes.Buffer) - defer streamCipherBufferPool.Put(buf) - - buf.Reset() - buf.Grow(len(p)) - buf.Write(p) // nolint: gosec - - data := buf.Bytes() - s.encryptor.XORKeyStream(data, data) - - return s.conn.Write(data) -} - -// Logger returns an instance of the logger for this wrapper. -func (s *StreamCipher) Logger() *zap.SugaredLogger { - return s.logger -} - -// LocalAddr returns local address of the underlying net.Conn. -func (s *StreamCipher) LocalAddr() *net.TCPAddr { - return s.conn.LocalAddr() -} - -// RemoteAddr returns remote address of the underlying net.Conn. -func (s *StreamCipher) RemoteAddr() *net.TCPAddr { - return s.conn.RemoteAddr() -} - -// Close closes underlying net.Conn instance. -func (s *StreamCipher) Close() error { - return s.conn.Close() -} - -// NewStreamCipher creates new stream cipher wrapper. -func NewStreamCipher(conn StreamReadWriteCloser, encryptor, decryptor cipher.Stream) StreamReadWriteCloser { - return &StreamCipher{ - conn: conn, - logger: conn.Logger().Named("stream-cipher"), - encryptor: encryptor, - decryptor: decryptor, - } -} diff --git a/wrappers/streamcipher_pool.go b/wrappers/streamcipher_pool.go deleted file mode 100644 index 45253c6..0000000 --- a/wrappers/streamcipher_pool.go +++ /dev/null @@ -1,14 +0,0 @@ -package wrappers - -import ( - "bytes" - "sync" -) - -var ( - streamCipherBufferPool = sync.Pool{ - New: func() interface{} { - return &bytes.Buffer{} - }, - } -) diff --git a/wrappers/wrap.go b/wrappers/wrap.go deleted file mode 100644 index 5e9581b..0000000 --- a/wrappers/wrap.go +++ /dev/null @@ -1,111 +0,0 @@ -package wrappers - -import ( - "io" - "net" - - "go.uber.org/zap" -) - -// Wrap is a base interface for all wrappers in this package. -type Wrap interface { - Logger() *zap.SugaredLogger - LocalAddr() *net.TCPAddr - RemoteAddr() *net.TCPAddr -} - -// Writer is a base interface for writers of this package. -type Writer interface { - io.Writer - Wrap -} - -// Closer is a base interface for wrappers of this package which can -// close connections. -type Closer interface { - io.Closer - Wrap -} - -// WriteCloser is a base interface for wrappers of this package which -// can write to and close connections. -type WriteCloser interface { - io.Closer - Writer -} - -// StreamReader is a base interface for wrappers which can read from the -// stream. -type StreamReader interface { - io.Reader - Wrap -} - -// StreamReadCloser is a base interface for wrappers which can read from -// and close the connections. -type StreamReadCloser interface { - io.Closer - StreamReader -} - -// StreamReadWriter is a base interface for wrappers which can read from -// and write to the connections. -type StreamReadWriter interface { - io.Writer - StreamReader -} - -// StreamWriteCloser is a base interface for wrappers which can write to -// and close the connections. -type StreamWriteCloser interface { - io.WriteCloser - Wrap -} - -// StreamReadWriteCloser is a base interface for stream processors. -type StreamReadWriteCloser interface { - io.Closer - StreamReadWriter -} - -// PacketReader is a base interface for wrappers which reads 'packets'. -// packets are atoms so you either get a packet or you get an error You -// cannot resume reading from packet. -type PacketReader interface { - Read() ([]byte, error) - Wrap -} - -// PacketWriter is a base interface for wrappers which can write packets. -type PacketWriter interface { - io.Writer - Wrap -} - -// PacketReadWriter is a base interface for wrappers which can read from -// and write packets. -type PacketReadWriter interface { - io.Writer - PacketReader -} - -// PacketReadCloser is a base interface for wrappers which can read -// packets and close the connection. -type PacketReadCloser interface { - io.Closer - PacketReader -} - -// PacketWriteCloser is a base interface for wrappers which can write -// packets and close the connection. -type PacketWriteCloser interface { - io.Writer - io.Closer - Wrap -} - -// PacketReadWriteCloser is a base interface for packet processors. -type PacketReadWriteCloser interface { - io.Closer - PacketReadWriter -}