Merge pull request #102 from 9seconds/next

v1.0
This commit is contained in:
Sergey Arkhipov
2019-11-11 17:55:06 +03:00
committed by GitHub
128 changed files with 5172 additions and 3734 deletions
+1 -3
View File
@@ -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:
+1 -1
View File
@@ -1,7 +1,7 @@
###############################################################################
# BUILD STAGE
FROM golang:1.12-alpine
FROM golang:1.13-alpine
RUN set -x \
&& apk --no-cache --update add \
+1 -5
View File
@@ -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
+112 -149
View File
@@ -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 <secret>
$ mtg run <secret>
```
How to run the tool with ADTag:
```console
mtg <secret> <adtag>
$ mtg run <secret> <adtag>
```
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.
+23 -24
View File
@@ -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...)
}
-9
View File
@@ -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)
}
+20
View File
@@ -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)
})
}
+26
View File
@@ -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)))
}
}
+103
View File
@@ -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
}
+43
View File
@@ -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)
}
}
-15
View File
@@ -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)
-63
View File
@@ -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
}
-42
View File
@@ -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
}
+223 -197
View File
@@ -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
}
+39 -13
View File
@@ -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
+36 -3
View File
@@ -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)
+6
View File
@@ -0,0 +1,6 @@
package conntypes
type ConnectionAcks struct {
Simple bool
Quick bool
}
+5
View File
@@ -0,0 +1,5 @@
package conntypes
type DC int16
const DCDefaultIdx DC = 1
+24
View File
@@ -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
}
+3
View File
@@ -0,0 +1,3 @@
package conntypes
type Packet []byte
+20
View File
@@ -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
)
+27
View File
@@ -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
}
}
+14
View File
@@ -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
}
+41
View File
@@ -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
}
+51
View File
@@ -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
}
+56
View File
@@ -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
}
+91
View File
@@ -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)
}
}
}
}
+140
View File
@@ -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{}
}
+30
View File
@@ -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,
}
)
+50
View File
@@ -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)
})
}
+11 -27
View File
@@ -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
)
+65 -34
View File
@@ -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=
+163
View File
@@ -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
}
+70
View File
@@ -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()
})
}
}
+40
View File
@@ -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
}
+24
View File
@@ -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,
}
})
}
+7
View File
@@ -0,0 +1,7 @@
package hub
import "mtg/protocol"
type Interface interface {
Register(*protocol.TelegramRequest) (*ProxyConn, error)
}
+81
View File
@@ -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
}
+77
View File
@@ -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,
}
}
+104 -182
View File
@@ -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)
}
-87
View File
@@ -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")
}
+102
View File
@@ -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
}
+3 -24
View File
@@ -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...)...)...)
+7 -8
View File
@@ -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{
+8 -10
View File
@@ -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
+8 -8
View File
@@ -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{
+27 -20
View File
@@ -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")
}
-105
View File
@@ -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
}
+53
View File
@@ -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)
}
+5 -3
View File
@@ -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
+113
View File
@@ -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{}
}
+28 -95
View File
@@ -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
}
}
-106
View File
@@ -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
}
-81
View File
@@ -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)
}
-97
View File
@@ -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)
}
+68
View File
@@ -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
}
}
+12
View File
@@ -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
+18
View File
@@ -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
}
+49
View File
@@ -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)
}
}
+68
View File
@@ -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
}
}
}
+60 -140
View File
@@ -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())
}
-25
View File
@@ -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
}
-7
View File
@@ -1,7 +0,0 @@
// +build windows
package rlimit
func Set() (err error) {
return
}
-35
View File
@@ -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")"
-76
View File
@@ -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
}
-28
View File
@@ -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
}
+50
View File
@@ -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
}
+57
View File
@@ -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()
}
}
-91
View File
@@ -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
}
-40
View File
@@ -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)
}
}
+30 -153
View File
@@ -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
}
+146
View File
@@ -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
}
+111
View File
@@ -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
}
-77
View File
@@ -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
}
+109
View File
@@ -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
}
+40
View File
@@ -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
}
+24
View File
@@ -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
}
+65
View File
@@ -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 ""
}
-52
View File
@@ -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
}
+12 -50
View File
@@ -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)
}
+42
View File
@@ -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
})
}
+8
View File
@@ -0,0 +1,8 @@
package telegram
import "mtg/conntypes"
type Telegram interface {
Dial(conntypes.DC, conntypes.ConnectionProtocol) (conntypes.StreamReadWriteCloser, error)
Secret() []byte
}
+48 -111
View File
@@ -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)
}
-191
View File
@@ -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)
}
-68
View File
@@ -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 ""
}
+86
View File
@@ -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
}
+79
View File
@@ -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)
}
+37
View File
@@ -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()
}
+85
View File
@@ -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
}
+91
View File
@@ -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
})
}
+26
View File
@@ -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
}
-21
View File
@@ -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
}
+21
View File
@@ -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
}
+1 -1
View File
@@ -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]
+24
View File
@@ -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
}
+7
View File
@@ -0,0 +1,7 @@
// +build windows
package utils
func SetLimits() error {
return nil
}
+25
View File
@@ -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
}
+23
View File
@@ -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
}
+11
View File
@@ -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)
}

Some files were not shown because too many files have changed in this diff Show More