REPOSITORY / ScuroNeko/mtg

Compare commits

DIFF REPOSITORY

Compare commits

..
3 Commits
263 changed files with 5932 additions and 13307 deletions
-4
View File
@@ -1,4 +0,0 @@
---
fixes:
- "github.com/9seconds/mtg/v2/::"
+1 -7
View File
@@ -1,9 +1,3 @@
version.go
vendor/
tags
.github/
.bin/
*.md
mtg
.golangci.toml
.gitignore
run.sh
-131
View File
@@ -1,131 +0,0 @@
---
name: CI
on:
push:
tags:
- v*
branches:
- master
- stable
- v1
release:
types:
- published
- released
pull_request:
types:
- opened
- edited
- reopened
- synchronize
- ready_for_review
jobs:
test:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 5
strategy:
matrix:
go_version:
- ^1.16
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: recursive
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go_version }}
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-
- name: Run tests
run: go test -coverprofile=./coverage.txt -covermode=atomic -v ./...
- name: Collect coverage
uses: codecov/codecov-action@v1
with:
file: ./coverage.txt
lint:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: recursive
- name: Run linter
uses: golangci/golangci-lint-action@v2
with:
version: v1.39.0
docker:
name: Docker
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: recursive
- name: Get Docker meta
id: docker_meta
uses: crazy-max/ghaction-docker-meta@v1
with:
images: nineseconds/mtg,ghcr.io/9seconds/mtg
tag-semver: "{{version}},{{major}},{{major}}.{{minor}}"
- name: Setup QEMU
uses: docker/setup-qemu-action@v1
- name: Setup BuildX
uses: docker/setup-buildx-action@v1
- name: Setup cache
uses: actions/cache@v2
with:
path: /tmp/buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to GHCR.io
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GH_PAT }}
- name: Build and push
uses: docker/build-push-action@v2
with:
pull: true
context: .
platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v7,linux/arm/v6
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}
cache-from: type=local,src=/tmp/buildx-cache
cache-to: type=local,dest=/tmp/buildx-cache
-69
View File
@@ -1,69 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches:
- master
- stable
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '24 20 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+1 -2
View File
@@ -7,6 +7,5 @@
*.out
mtg
vendor/
version.go
ccbuilds/
.bin/
coverage.txt
+2 -1
View File
@@ -3,10 +3,11 @@ concurrency = 4
deadline = "2m"
tests = true
skip-dirs = ["vendor"]
skip-files = ["version.go"]
[output]
format = "colored-line-number"
[linters]
enable-all = true
disable = ["gochecknoglobals", "gas", "goerr113", "exhaustivestruct"]
disable = ["gochecknoglobals"]
+19
View File
@@ -0,0 +1,19 @@
---
language: go
sudo: false
dist: trusty
go:
- 1.13.x
- master
before_script: make prepare
script:
- make all
- make lint
matrix:
allow_failures:
- go: master
+14 -9
View File
@@ -1,7 +1,7 @@
###############################################################################
# BUILD STAGE
FROM golang:1.16-alpine AS build
FROM golang:1.13-alpine
RUN set -x \
&& apk --no-cache --update add \
@@ -9,13 +9,15 @@ RUN set -x \
ca-certificates \
curl \
git \
make
make \
upx
COPY . /app
WORKDIR /app
COPY . /go/src/github.com/9seconds/mtg/
RUN set -x \
&& make -j 4 static
&& cd /go/src/github.com/9seconds/mtg \
&& make -j 4 static \
&& upx --ultra-brute -qq ./mtg
###############################################################################
@@ -24,8 +26,11 @@ RUN set -x \
FROM scratch
ENTRYPOINT ["/mtg"]
CMD ["run", "/config.toml"]
ENV MTG_IP=0.0.0.0 \
MTG_PORT=3128 \
MTG_STATS_IP=0.0.0.0 \
MTG_STATS_PORT=3129
EXPOSE 3128 3129
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=build /app/mtg /mtg
COPY --from=build /app/example.config.toml /config.toml
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=0 /go/src/github.com/9seconds/mtg/mtg /mtg
+28 -61
View File
@@ -4,104 +4,71 @@ 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.39.0
GOLANGCI_LINT_VERSION := v1.21.0
VERSION_GO := $(shell go version)
VERSION_DATE := $(shell date -Ru)
VERSION_TAG := $(shell git describe --tags --always)
COMMON_BUILD_FLAGS := -trimpath -mod=readonly -ldflags="-extldflags '-static' -s -w -X 'main.version=$(VERSION_TAG) ($(VERSION_GO)) [$(VERSION_DATE)]'"
COMMON_BUILD_FLAGS := -ldflags="-s -w -X 'main.version=$(VERSION_TAG) ($(VERSION_GO)) [$(VERSION_DATE)]'"
GOBIN := $(ROOT_DIR)/.bin
GOTOOL := env "GOBIN=$(GOBIN)" "PATH=$(ROOT_DIR)/.bin:$(PATH)"
MOD_ON := env GO111MODULE=on
MOD_OFF := env GO111MODULE=auto
# -----------------------------------------------------------------------------
.PHONY: all
all: build
$(APP_NAME):
@$(MOD_ON) go build $(COMMON_BUILD_FLAGS) -o "$(APP_NAME)"
.PHONY: build
build:
@go build $(COMMON_BUILD_FLAGS) -o "$(APP_NAME)"
$(APP_NAME): build
.PHONY: static
static:
@env CGO_ENABLED=0 GOOS=linux go build \
$(COMMON_BUILD_FLAGS) \
-tags netgo \
-a \
-o "$(APP_NAME)"
static-$(APP_NAME):
@$(MOD_ON) env CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo $(COMMON_BUILD_FLAGS) -o "$(APP_NAME)"
$(APP_NAME)-%: GOOS=$(shell echo -n "$@" | sed 's?$(APP_NAME)-??' | cut -f1 -d-)
$(APP_NAME)-%: GOARCH=$(shell echo -n "$@" | sed 's?$(APP_NAME)-??' | cut -f2 -d-)
$(APP_NAME)-%: ccbuilds
@env "GOOS=$(GOOS)" "GOARCH=$(GOARCH)" \
@$(MOD_ON) env "GOOS=$(GOOS)" "GOARCH=$(GOARCH)" \
go build \
$(COMMON_BUILD_FLAGS) \
-tags netgo \
-a \
-o "./ccbuilds/$(APP_NAME)-$(GOOS)-$(GOARCH)"
.PHONY: ccbuilds
ccbuilds:
@rm -rf ./ccbuilds && mkdir -p ./ccbuilds
vendor: go.mod go.sum
@$(MOD_ON) go mod vendor
.PHONY: fmt
fmt:
@$(GOTOOL) gofumpt -w -s -extra "$(ROOT_DIR)"
# -----------------------------------------------------------------------------
.PHONY: test
test:
@go test -v ./...
.PHONY: all
all: $(APP_NAME)
.PHONY: citest
citest:
@go test -coverprofile=coverage.txt -covermode=atomic -parallel 2 -race -v ./...
.PHONY: static
static: static-$(APP_NAME)
.PHONY: crosscompile
crosscompile: $(CC_BINARIES)
.PHONY: crosscompile-dir
crosscompile-dir:
@rm -rf "$(CC_DIR)" && mkdir -p "$(CC_DIR)"
.PHONY: lint
lint: vendor
@$(MOD_OFF) golangci-lint run
.PHONY: clean
clean:
@git clean -xfd && \
git reset --hard >/dev/null && \
git submodule foreach --recursive sh -c 'git clean -xfd && git reset --hard' >/dev/null
.PHONY: lint
lint:
@$(GOTOOL) golangci-lint run
.PHONY: docker
docker:
@docker build --pull -t "$(IMAGE_NAME)" "$(ROOT_DIR)"
.PHONY: doc
doc:
@$(GOTOOL) godoc -http 0.0.0.0:10000
.PHONY: prepare
prepare: install-lint
.PHONY: install-tools
install-tools: install-tools-lint install-tools-godoc install-tools-gofumpt
.PHONY: install-tools-lint
install-tools-lint:
@mkdir -p "$(GOBIN)" || true && \
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh \
| bash -s -- -b "$(GOBIN)" "$(GOLANGCI_LINT_VERSION)"
.PHONY: install-tools-godoc
install-tools-godoc:
@mkdir -p "$(GOBIN)" || true && \
$(GOTOOL) go get -u golang.org/x/tools/cmd/godoc
.PHONY: install-tools-gofumpt
install-tools-gofumpt:
@mkdir -p "$(GOBIN)" || true && \
$(GOTOOL) go get -u mvdan.cc/gofumpt
.PHONY: update-deps
update-deps:
@go get -u && go mod tidy
.PHONY: install-lint
install-lint:
@curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh \
| $(MOD_OFF) bash -s -- -b $(GOPATH)/bin $(GOLANGCI_LINT_VERSION)
+213 -275
View File
@@ -1,16 +1,14 @@
# mtg
Highly-opionated (ex-bullshit-free) MTPROTO proxy for
[Telegram](https://telegram.org/).
Bullshit-free MTPROTO proxy for Telegram
[![CI](https://github.com/9seconds/mtg/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/9seconds/mtg/actions/workflows/ci.yaml)
[![codecov](https://codecov.io/gh/9seconds/mtg/branch/master/graph/badge.svg?token=JfdDyGVpT4)](https://codecov.io/gh/9seconds/mtg)
[![Go Reference](https://pkg.go.dev/badge/github.com/9seconds/mtg.svg)](https://pkg.go.dev/github.com/9seconds/mtg/v2)
[![Build Status](https://travis-ci.org/9seconds/mtg.svg?branch=master)](https://travis-ci.org/9seconds/mtg)
[![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/)
**If you use v1.0 or upgrade broke you proxy, please read the chapter
[Version 2](#version-2)**
**Please see a guide on upgrading to 1.0 at the end of this README.**
## Rationale
# Rationale
There are several available proxies for Telegram MTPROTO available. Here
are the most notable:
@@ -19,322 +17,262 @@ are the most notable:
* [Python](https://github.com/alexbers/mtprotoproxy)
* [Erlang](https://github.com/seriyps/mtproto_proxy)
You can use any of these. They work great and all implementations have
feature parity now. This includes support of adtag, replay attack
protection, domain fronting, faketls, and so on. mtg has a similar
goal: to give a possibility to connect to Telegram in a restricted,
censored environment. But it does it slightly differently in details
that probably matter.
Almost all of them follow the way how official proxy was built. This
includes support of multiple secrets, support of promoted channels, etc.
* **Resource-efficient**
It has to be resource-efficient. It does not mean that you will see
the smallest memory usage. It means that it will try to use allocated
resources in zero-waste mode, reusing as much memory as possible and
so on.
mtg is an implementation in golang which is intended to be:
* **Lightweight**
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](https://shadowsocks.org): promoted channels is a strange
way of doing business I suppose. I think the only viable way is to
have a proxy that can be restored anywhere easily.
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.
* **A single secret**
I think that multiple secrets solve no problems and just complex
I think that multiple secrets solve no problems and just complexify
software. I also believe that in the case of throwout proxies, this
the feature is a useless luxury.
* **No adtag support**
Please read [Version 2](#version-2) chapter.
feature is a useless luxury.
* **Minimum docker image size**
Official image is less than 3.5 megabytes. Literally.
* **No management WebUI**
This is an implementation of a simple lightweight proxy. I won't do that.
* **Proxy chaining**
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 way is more robust.
mtg has the support of [SOCKS5](https://en.wikipedia.org/wiki/SOCKS)
proxies. So, in theory, you can run this proxy as a frontend
and route traffic via [v2ray](https://www.v2ray.com/),
[Gost](https://docs.ginuerzh.xyz/gost/),
[Trojan](https://trojan-gfw.github.io/trojan/), or any other project
you like.
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.
* **Native blocklist support**
To get promoted channel, please contact
[@MTProxybot](https://t.me/MTProxybot) and provide generated adtag as a
second parameter.
Previously, this was delegated to the [FireHOL](https://firehol.org/)
project or similar ones which track attacks and publish a list of
potentially dangerous IPs. mtg has native support of such blocklists.
* **Can be used as a library**
# Source code organization
mtg v2 was redesigned in a way so it can be embedded into your
software (written in Golang) with a minimum effort + you can replace
some parts with those you want.
There are 2 main branches:
### Version 2
1. `master` branch contains potentially unstable features
2. `stable` branch contains stable version. Usually you want to use this branch.
If you use version 1.x before, you are probably noticed some major
backward non-compatible details:
1. Configuration file
2. Removed support of adtag
For the configuration file, please check out the full example in this
repository. It has a lot of comments and most of the options are
optional. We do have only `secret` and `bind-to` sections mandatory.
Other sections in the example configuration file are filled with default
values.
Adtag support was removed completely. This was done to debloat mtg and
keep it simple and obvious. Hopefully, this goal is achieved and the
source code is clean and straightforward enough.
I always was quite skeptical about adtag. In my POV, a proxy as a fat
big connectivity point for hundreds of clients is an illusion. If you
work in a censored environment, the first thing that authority does is
IP blocking. For us, it means, those big proxies that can benefit from
having a pinned channel are going to be blocked in a minute.
Proxy has to be intimate. It has to be shared within a small group as
a family or maybe your college friends. It has to have a small number
of connections and never publicly announced its presence. It has to fly
under the radar. If the proxy is detected, you need to be able to give
a rebirth on a new IP address as soon as possible. I do no think that
having some special channel for such a use case makes any sense.
But other details like replay attack protection, domain fronting,
accurate FakeTLS implementation, IP blacklisting, and proxy
chaining matter here. If you work in censored perimeter like
[GFW](https://en.wikipedia.org/wiki/Great_Firewall)-protected
country, you probably want to have an MTPROTO proxy as
a frontend that transports traffic via cloaked tunnels
made by [Trojan](https://trojan-gfw.github.io/trojan/),
[Shadowsocks](https://shadowsocks.org), [v2ray](https://www.v2ray.com/),
or [Gost](https://docs.ginuerzh.xyz/gost/). That's why you have to have
the support of chaining as a first-class citizen.
Yes, this is possible and doable with optional adtag support. But the
truth is that the MTPROTO proxy for Telegram is just a thing that either
work as a normal client (direct mode) or doing some RPC calls in [TL
language](https://core.telegram.org/mtproto/TL) (adtag support). I
understand the intention of the developers and I understand that they
were under high pressure fighting with [RKN](https://rkn.gov.ru/) and
doing TON after that. Nothing is ideal. But for the proxy, it means that
source code is full of complex non-trivial code which is required only
to support a feature that we barely need.
So, to have a reasonable MTPROTO proxy, adtag support was removed. This
is a rare chance in my career where software v2 debloats a previous
version. It feels so good :)
### Version 1 and 2
I do continue to support both versions 1 and 2. But in a different mode.
Version 1 is now officially in maintenance mode. It means that I won't
make any new features or improvements there. You can consider a feature
freeze there. No bugs are going to be fixed there except for critical
ones. PRs are welcome though. The goal is to keep it working. It will
get some periodical updates like updates to the new Golang version of
dependencies version bump, but that's mostly it.
**If you want to have mtg with _adtag support_, please use version 1**.
Version 2 is going to have all my love, active support, bug fixing, etc.
It is under active development and maintenance.
This project has several main branches
1. [`master`](https://github.com/9seconds/mtg/tree/master) branch
contains a bleeding edge. It may potentially have some features
which will break your source code.
2. [`stable`](https://github.com/9seconds/mtg/tree/stable) branch contains
dumps of a master branch when we consider it 'stable'. This is a
branch you probably want to pick.
3. [`v2`](https://github.com/9seconds/mtg/tree/v2) has a development
of the v2.x version. In theory, it is the same as `master` but this
will change when we have v3.x.
4. [`v1`](https://github.com/9seconds/mtg/tree/v1) has a version 1.x.
## Getting started
### Download a tool
#### Download binaries
Binaries can be downloaded from the release page. Also, you can download
docker image.
For the current version, please download like
# How to build
```console
docker pull nineseconds/mtg:2
make
```
For version 1:
If you want to build for another platform:
```console
docker pull nineseconds/mtg:1
make crosscompile
```
You may also check both [Docker
Hub](https://hub.docker.com/r/nineseconds/mtg/tags) and [Github
Registry](https://github.com/users/9seconds/packages/container/package/mtg).
Please do not choose `latest` or `stable` if you want to avoid
surprises. Always choose some version tag.
Also, if you have `go` installed, you can always download this tool with `go get`:
```console
go get github.com/9seconds/mtg/v2
```
#### Build from sources
```console
git clone https://github.com:9seconds/mtg.git
cd mtg
make static
```
or for the docker image:
If you want to build Docker image (called `mtg`):
```console
make docker
```
### Generate secret
# Docker image
If you already have a secret in Base64 format or that, which starts with `ee`,
you can skip this chapter. Otherwise:
Docker follows the same policy as the source code organization:
- `latest` mirrors the master branch
- `stable` mirrors the stable branch
- tags are for tagged releases
```console
$ mtg generate-secret google.com
7ibaERuTSGPH1RdztfYnN4tnb29nbGUuY29t
docker pull nineseconds/mtg:latest
```
or
```console
$ mtg generate-secret --hex google.com
ee473ce5d4958eb5f968c87680a23854a0676f6f676c652e636f6d
docker pull nineseconds/mtg:stable
```
This secret is a keystone for a proxy and your password for a client.
You need to keep it secured.
We recommend choosing a hostname wisely. Here we have a _google.com_
but in reality, all providers can easily detect that this is not a
Google. Google has a list of networks it officially uses and your IP
address won't probably belong to it. It is a great idea to hide behind
some domain that has some relation to this IP address.
For example, you've bought a VPS from [Digital
Ocean](https://www.digitalocean.com/). Then it might be a good idea to
generate a secret for _digitalocean.com_ then.
### Prepare a configuration file
Please checkout an example configuration file. All options except of
`secret` and `bind-to` are optional. You can safely have this minimal
configuration file:
```toml
secret = "ee473ce5d4958eb5f968c87680a23854a0676f6f676c652e636f6d"
bind-to = "0.0.0.0:443"
```
This is enough to run the whole application. All other
options already have sensible defaults for the app at almost any scale.
Oh, the configuration is done in [TOML format](https://toml.io/en/).
### Run a proxy
Put a binary and a config into your webserver. Just for example,
a binary goes to `/usr/local/bin/mtg` and configuration to `/etc/mtg.toml`.
Now you can create a systemd unit:
```console
$ cat /etc/systemd/system/mtg.service
[Unit]
Description=mtg
[Service]
ExecStart=/usr/local/bin/mtg run /etc/mtg.toml
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
$ sudo systemctl daemon-reload
$ sudo systemctl enable mtg
$ sudo systemctl start mtg
docker pull nineseconds/mtg:0.10
```
or you can run a docker image
# Ansible role
You can find unofficial Ansible role for mtg here: https://github.com/rlex/ansible-role-mtg
Also, there is another project on Ansible Galaxy: https://galaxy.ansible.com/ivansible/lin_mtproxy
# Configuration
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
docker run -d -v /etc/mtg.toml:/config.toml -p 443:3128 --restart=unless-stopped nineseconds/mtg:2
$ mtg generate-secret simple
52a493bdfb90eea55739eabff2d92a14
```
where _443_ is a host port (a port you want to connect to from a
client), and _3128_ is the one you have in your config in the `bind-to`
section.
### Access a proxy
Now you can generate some useful links:
```console
$ mtg access /etc/mtg.toml
{
"ipv4": {
"ip": "x.y.z.a",
"port": 3128,
"tg_url": "tg://proxy?...",
"tg_qrcode": "https://api.qrserver.com/v1/create-qr-code?data...",
"tme_url": "https://t.me/proxy?...",
"tme_qrcode": "https://api.qrserver.com/v1/create-qr-code?data..."
},
"secret": {
"hex": "...",
"base64": "..."
}
}
$ mtg generate-secret secured
ddf05fb7acb549be047a7c585116581418
```
## Metrics
```console
$ mtg generate-secret -c google.com tls
ee852380f362a09343efb4690c4e17862e676f6f676c652e636f6d
```
Out of the box, mtg works with
[statsd](https://github.com/statsd/statsd) and
[Prometheus](https://prometheus.io/). Please check configuration file
example to get how to set this integration up.
Or, if you prefer docker:
Here goes a list of metrics with their types but without a prefix.
```console
$ docker run --rm nineseconds/mtg generate-secret tls -c bing.com
eedf71035a8ed48a623d8e83e66aec4d0562696e672e636f6d
```
| Name | Type | Tags | Description |
|-----------------------------|---------|----------------------------------|--------------------------------------------------------------------------------------------|
| client_connections | gauge | `ip_family` | Count of processing client connections. |
| telegram_connections | gauge | `telegram_ip`, `dc` | Count of connections to Telegram servers. |
| domain_fronting_connections | gauge | `ip_family` | Count of connections to fronting domain. |
| telegram_traffic | counter | `telegram_ip`, `dc`, `direction` | Count of bytes, transmitted to/from Telegram. |
| domain_fronting_traffic | counter | `direction` | Count of bytes, transmitted to/from fronting domain. |
| domain_fronting | counter | | Count of domain fronting events. |
| concurrency_limited | counter | | Count of events, when client connection was rejected due to concurrency limit. |
| ip_blocklisted | counter | | Count of events when client connection was rejected because IP was found in the blacklist. |
| replay_attacks | counter | | Count of detected replay attacks. |
## Antireplay cache
Tag meaning:
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.
| Name | Values | Description |
|-------------|----------------------------|-----------------------------------------------|
| ip_family | `ipv4`, `ipv6` | A version of the IP protocol. |
| dc | | A number of the Telegram DC for a connection. |
| telegram_ip | | IP address of the Telegram server. |
| direction | `to_client`, `from_client` | A direction of the traffic flow. |
## 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
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_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_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
read buffer.
Unfortunately, MTPROTO proxy protocol does not allow us to use splice
or any other neat tricks how to eliminate the need of copying data into
userspace.
# How to run the tool
Now run the tool:
```console
$ mtg run <secret>
```
How to run the tool with ADTag:
```console
$ mtg run <secret> <adtag>
```
This tool will listen on port 3128 by default with the given secret.
# oneliner to run this proxy
Please ensure that docker is installed. After that just execute
```console
curl -sfL --compressed https://raw.githubusercontent.com/9seconds/mtg/master/run.sh | bash
```
# statsd integration
mtg provides an integration with statsd, you can enable it with command
line interface. To enable it, you have to provide IP address of statsd
service.
Out of the box, mtg supports 2 additional dialects: [InfluxDB](https://www.influxdata.com/blog/getting-started-with-sending-statsd-metrics-to-telegraf-influxdb/)
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` | number | The number of active connections. |
| `telegram_connections` | number | The number of active telegram connections. |
| `crashes` | number | An amount of crashes in client handlers. |
| `traffic.egress` | bytes | Traffic from the start of application. |
| `replay_attacks` | number | The number of prevented replay attacks. |
All metrics are prefixed with given prefix. Default prefix is `mtg`.
Also, metrics provide tags (ipv4/ipv6, dc indexes etc).
# Prometheus integration
[Prometheus](https://prometheus.io) integration comes out of
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.
-24
View File
@@ -1,24 +0,0 @@
# Security Policy
## Supported Versions
We support 2 tracks: 2.x and 1.x
| Version | Supported |
| ------- | ------------------ |
| 2.x | :white_check_mark: |
| 1.x | :interrobang: |
| < 1.0 | :x: |
1.x has adtag support. We do not plan any active development there but we guarantee:
1. Regular dependency updates
2. Updates for Golang versions
3. Security vulnerability fixes
4. Merging small PRs
2.x is in active development and have a full support.
## Reporting a Vulnerability
If you have found a vulnerability, please report about it to nineseconds@yandex.ru, telegram @9seconds or here in issues
+36
View File
@@ -0,0 +1,36 @@
package antireplay
import "github.com/VictoriaMetrics/fastcache"
var (
prefixObfuscated2 = []byte{0x00}
prefixTLS = []byte{0x01}
)
type cache struct {
data *fastcache.Cache
}
func (c *cache) AddObfuscated2(data []byte) {
c.data.Set(keyObfuscated2(data), nil)
}
func (c *cache) AddTLS(data []byte) {
c.data.Set(keyTLS(data), nil)
}
func (c *cache) HasObfuscated2(data []byte) bool {
return c.data.Has(keyObfuscated2(data))
}
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...)
}
+16 -13
View File
@@ -1,17 +1,20 @@
// Antireplay package has cache implementations that are effective
// against replay attacks.
//
// To understand more about replay attacks, please read documentation
// for mtglib.AntiReplayCache interface. This package has a list of some
// implementations of this interface.
package antireplay
const (
// DefaultStableBloomFilterMaxSize is a recommended byte size for a
// stable bloom filter.
DefaultStableBloomFilterMaxSize = 1024 * 1024 // 1MiB
import (
"sync"
// DefaultStableBloomFilterErrorRate is a recommended default error
// rate for a stable bloom filter.
DefaultStableBloomFilterErrorRate = 0.001
"github.com/VictoriaMetrics/fastcache"
"github.com/9seconds/mtg/config"
)
var (
Cache cache
initOnce sync.Once
)
func Init() {
initOnce.Do(func() {
Cache.data = fastcache.New(config.C.AntiReplayMaxSize)
})
}
-14
View File
@@ -1,14 +0,0 @@
package antireplay
import "github.com/9seconds/mtg/v2/mtglib"
type noop struct{}
func (n noop) SeenBefore(_ []byte) bool { return false }
// NewNoop returns an implementation that does nothing. A corresponding
// method always returns false, so this cache accepts everything you
// pass to it.
func NewNoop() mtglib.AntiReplayCache {
return noop{}
}
-26
View File
@@ -1,26 +0,0 @@
package antireplay_test
import (
"testing"
"github.com/9seconds/mtg/v2/antireplay"
"github.com/stretchr/testify/suite"
)
type NoopTestSuite struct {
suite.Suite
}
func (suite *NoopTestSuite) TestOp() {
filter := antireplay.NewNoop()
suite.False(filter.SeenBefore([]byte{1, 2, 3}))
suite.False(filter.SeenBefore([]byte{4, 5, 6}))
suite.False(filter.SeenBefore([]byte{1, 2, 3}))
suite.False(filter.SeenBefore([]byte{4, 5, 6}))
}
func TestNoop(t *testing.T) {
t.Parallel()
suite.Run(t, &NoopTestSuite{})
}
-51
View File
@@ -1,51 +0,0 @@
package antireplay
import (
"sync"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/OneOfOne/xxhash"
boom "github.com/tylertreat/BoomFilters"
)
type stableBloomFilter struct {
filter boom.StableBloomFilter
mutex sync.Mutex
}
func (s *stableBloomFilter) SeenBefore(digest []byte) bool {
s.mutex.Lock()
defer s.mutex.Unlock()
return s.filter.TestAndAdd(digest)
}
// NewStableBloomFilter returns an implementation of AntiReplayCache
// based on stable bloom filter.
//
// http://webdocs.cs.ualberta.ca/~drafiei/papers/DupDet06Sigmod.pdf
//
// The basic idea of a stable bloom filter is quite simple: each time
// when you set a new element, you randomly reset P elements. There is a
// hardcore math which proves that if you choose this P correctly, you
// can maintain the same error rate for a stream of elements.
//
// byteSize is the number of bytes you want to give to a bloom filter.
// errorRate is desired false-positive error rate. If you want to use
// default values, please pass 0 for byteSize and <0 for errorRate.
func NewStableBloomFilter(byteSize uint, errorRate float64) mtglib.AntiReplayCache {
if byteSize == 0 {
byteSize = DefaultStableBloomFilterMaxSize
}
if errorRate < 0 {
errorRate = DefaultStableBloomFilterErrorRate
}
sf := boom.NewDefaultStableBloomFilter(byteSize*8, errorRate) // nolint: gomnd
sf.SetHash(xxhash.New64())
return &stableBloomFilter{
filter: *sf,
}
}
-26
View File
@@ -1,26 +0,0 @@
package antireplay_test
import (
"testing"
"github.com/9seconds/mtg/v2/antireplay"
"github.com/stretchr/testify/suite"
)
type StableBloomFilterTestSuite struct {
suite.Suite
}
func (suite *StableBloomFilterTestSuite) TestOp() {
filter := antireplay.NewStableBloomFilter(500, 0.001)
suite.False(filter.SeenBefore([]byte{1, 2, 3}))
suite.False(filter.SeenBefore([]byte{4, 5, 6}))
suite.True(filter.SeenBefore([]byte{1, 2, 3}))
suite.True(filter.SeenBefore([]byte{4, 5, 6}))
}
func TestStableBloomFilter(t *testing.T) {
t.Parallel()
suite.Run(t, &StableBloomFilterTestSuite{})
}
+26
View File
@@ -0,0 +1,26 @@
package cli
import (
"crypto/rand"
"encoding/hex"
"github.com/9seconds/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)))
}
}
+102
View File
@@ -0,0 +1,102 @@
package cli
import (
"net"
"os"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/9seconds/mtg/antireplay"
"github.com/9seconds/mtg/config"
"github.com/9seconds/mtg/faketls"
"github.com/9seconds/mtg/hub"
"github.com/9seconds/mtg/ntp"
"github.com/9seconds/mtg/obfuscated2"
"github.com/9seconds/mtg/proxy"
"github.com/9seconds/mtg/stats"
"github.com/9seconds/mtg/telegram"
"github.com/9seconds/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)
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)
}
}
+235
View File
@@ -0,0 +1,235 @@
package config
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net"
"github.com/alecthomas/units"
statsd "github.com/smira/go-statsd"
"go.uber.org/zap"
)
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
OptionTypeStatsdTagsFormat
OptionTypeStatsdTags
OptionTypeWriteBufferSize
OptionTypeReadBufferSize
OptionTypeCloakPort
OptionTypeAntiReplayMaxSize
OptionTypeMultiplexPerConnection
OptionTypeSecret
OptionTypeAdtag
)
type Config struct {
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"`
StatsdTagsFormat *statsd.TagFormat `json:"statsd_tags_format"`
StatsNamespace string `json:"stats_namespace"`
CloakHost string `json:"cloak_host"`
StatsdTags map[string]string `json:"statsd_tags"`
WriteBuffer int `json:"write_buffer"`
ReadBuffer int `json:"read_buffer"`
CloakPort int `json:"cloak_port"`
AntiReplayMaxSize int `json:"anti_replay_max_size"`
MultiplexPerConnection int `json:"multiplex_per_connection"`
Debug bool `json:"debug"`
Verbose bool `json:"verbose"`
SecretMode SecretMode `json:"secret_mode"`
Secret []byte `json:"secret"`
AdTag []byte `json:"adtag"`
}
type Opt struct {
Option OptionType
Value interface{}
}
var C = Config{}
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 OptionTypeStatsdTagsFormat:
value := opt.Value.(string)
switch value {
case "datadog":
C.StatsdTagsFormat = statsd.TagFormatDatadog
case "influxdb":
C.StatsdTagsFormat = statsd.TagFormatInfluxDB
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:
return fmt.Errorf("unknown tag %v", opt.Option)
}
}
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 != "" {
if _, err := net.LookupHost(C.CloakHost); err != nil {
zap.S().Warnw("Cannot resolve address of host", "hostname", C.CloakHost, "error", err)
}
}
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
}
+78
View File
@@ -0,0 +1,78 @@
package config
import (
"context"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"strings"
"time"
)
const (
ifconfigAddress = "https://ifconfig.co/ip"
ifconfigTimeout = 10 * time.Second
)
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(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(ctx context.Context, network string) (net.IP, error) {
dialer := &net.Dialer{FallbackDelay: -1}
client := &http.Client{
Jar: nil,
Timeout: ifconfigTimeout,
Transport: &http.Transport{
DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, addr)
},
},
}
req, err := http.NewRequest("GET", ifconfigAddress, nil)
if err != nil {
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, fmt.Errorf("cannot read response body: %w", err)
}
respData := strings.TrimSpace(string(respDataBytes))
ip := net.ParseIP(respData)
if ip == nil {
return nil, fmt.Errorf("ifconfig.co returns incorrect IP %s", respData)
}
return ip, nil
}
+91
View File
@@ -0,0 +1,91 @@
package config
import (
"encoding/hex"
"net"
"net/url"
"strconv"
)
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.IP.String())
values.Set("port", strconv.Itoa(addr.Port))
values.Set("secret", secret)
urls.TG = makeTGURL(values)
urls.TMe = makeTMeURL(values)
urls.TGQRCode = makeQRCodeURL(urls.TG)
urls.TMeQRCode = makeQRCodeURL(urls.TG)
return
}
func makeTGURL(values url.Values) string {
tgURL := url.URL{
Scheme: "tg",
Host: "proxy",
RawQuery: values.Encode(),
}
return tgURL.String()
}
func makeTMeURL(values url.Values) string {
tMeURL := url.URL{
Scheme: "https",
Host: "t.me",
Path: "proxy",
RawQuery: values.Encode(),
}
return tMeURL.String()
}
func makeQRCodeURL(data string) string {
qr := url.URL{
Scheme: "https",
Host: "api.qrserver.com",
Path: "v1/create-qr-code",
}
values := url.Values{}
values.Set("qzone", "4")
values.Set("format", "svg")
values.Set("data", data)
qr.RawQuery = values.Encode()
return qr.String()
}
+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
}
-108
View File
@@ -1,108 +0,0 @@
package events
import (
"context"
"math/rand"
"runtime"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/OneOfOne/xxhash"
)
// EventStream is a default implementation of the mtglib.EventStream
// interface.
//
// EventStream manages a set of goroutines, observers. Main
// responsibility of the event stream is to route an event to relevant
// observer based on some hash so each observer will have all events
// which belong to some stream id.
//
// Thus, EventStream can spawn many observers.
type EventStream struct {
ctx context.Context
ctxCancel context.CancelFunc
chans []chan mtglib.Event
}
// Send starts delivering of the message to observer with respect to a
// given context If context is closed, message could be not delivered.
func (e EventStream) Send(ctx context.Context, evt mtglib.Event) {
var chanNo uint32
if streamID := evt.StreamID(); streamID != "" {
chanNo = xxhash.ChecksumString32(streamID)
} else {
chanNo = rand.Uint32()
}
select {
case <-ctx.Done():
case <-e.ctx.Done():
case e.chans[int(chanNo)%len(e.chans)] <- evt:
}
}
// Shutdown stops an event stream pipeline.
func (e EventStream) Shutdown() {
e.ctxCancel()
}
// NewEventStream builds a new default event stream.
//
// If you give an empty array of observers, then NoopObserver is going
// to be used. If you give many observers, then they will process a
// message concurrently.
func NewEventStream(observerFactories []ObserverFactory) EventStream {
if len(observerFactories) == 0 {
observerFactories = append(observerFactories, NewNoopObserver)
}
ctx, cancel := context.WithCancel(context.Background())
rv := EventStream{
ctx: ctx,
ctxCancel: cancel,
chans: make([]chan mtglib.Event, runtime.NumCPU()),
}
for i := 0; i < runtime.NumCPU(); i++ {
rv.chans[i] = make(chan mtglib.Event, 1)
if len(observerFactories) == 1 {
go eventStreamProcessor(ctx, rv.chans[i], observerFactories[0]())
} else {
go eventStreamProcessor(ctx, rv.chans[i], newMultiObserver(observerFactories))
}
}
return rv
}
func eventStreamProcessor(ctx context.Context, eventChan <-chan mtglib.Event, observer Observer) { // nolint: cyclop
defer observer.Shutdown()
for {
select {
case <-ctx.Done():
return
case evt := <-eventChan:
switch typedEvt := evt.(type) {
case mtglib.EventTraffic:
observer.EventTraffic(typedEvt)
case mtglib.EventStart:
observer.EventStart(typedEvt)
case mtglib.EventFinish:
observer.EventFinish(typedEvt)
case mtglib.EventConnectedToDC:
observer.EventConnectedToDC(typedEvt)
case mtglib.EventDomainFronting:
observer.EventDomainFronting(typedEvt)
case mtglib.EventIPBlocklisted:
observer.EventIPBlocklisted(typedEvt)
case mtglib.EventConcurrencyLimited:
observer.EventConcurrencyLimited(typedEvt)
case mtglib.EventReplayAttack:
observer.EventReplayAttack(typedEvt)
}
}
}
}
-220
View File
@@ -1,220 +0,0 @@
package events_test
import (
"context"
"net"
"testing"
"time"
"github.com/9seconds/mtg/v2/events"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type EventStreamTestSuite struct {
suite.Suite
ctx context.Context
ctxCancel context.CancelFunc
observerMock1 *ObserverMock
observerMock2 *ObserverMock
stream events.EventStream
}
func (suite *EventStreamTestSuite) SetupTest() {
suite.ctx, suite.ctxCancel = context.WithCancel(context.Background())
suite.observerMock1 = &ObserverMock{}
suite.observerMock2 = &ObserverMock{}
suite.observerMock1.On("Shutdown")
suite.observerMock2.On("Shutdown")
factories := make([]events.ObserverFactory, 2)
factories[0] = func() events.Observer { return suite.observerMock1 }
factories[1] = func() events.Observer { return suite.observerMock2 }
suite.stream = events.NewEventStream(factories)
}
func (suite *EventStreamTestSuite) TestEventStart() {
evt := mtglib.NewEventStart("connID", net.ParseIP("10.0.0.1"))
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
v.
On("EventStart", mock.Anything).
Once().
Run(func(args mock.Arguments) {
caught, ok := args.Get(0).(mtglib.EventStart)
suite.True(ok)
suite.Equal(evt.RemoteIP.String(), caught.RemoteIP.String())
suite.Equal(evt.StreamID(), caught.StreamID())
suite.Equal(evt.Timestamp(), caught.Timestamp())
})
}
suite.stream.Send(suite.ctx, evt)
time.Sleep(100 * time.Millisecond)
}
func (suite *EventStreamTestSuite) TestEventConnectedToDC() {
evt := mtglib.NewEventConnectedToDC("connID", net.ParseIP("10.0.0.1"), 3)
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
v.
On("EventConnectedToDC", mock.Anything).
Once().
Run(func(args mock.Arguments) {
caught, ok := args.Get(0).(mtglib.EventConnectedToDC)
suite.True(ok)
suite.Equal(evt.RemoteIP.String(), caught.RemoteIP.String())
suite.Equal(evt.StreamID(), caught.StreamID())
suite.Equal(evt.DC, caught.DC)
suite.Equal(evt.Timestamp(), caught.Timestamp())
})
}
suite.stream.Send(suite.ctx, evt)
time.Sleep(100 * time.Millisecond)
}
func (suite *EventStreamTestSuite) TestEventDomainFronting() {
evt := mtglib.NewEventDomainFronting("connID")
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
v.
On("EventDomainFronting", mock.Anything).
Once().
Run(func(args mock.Arguments) {
caught, ok := args.Get(0).(mtglib.EventDomainFronting)
suite.True(ok)
suite.Equal(evt.StreamID(), caught.StreamID())
suite.Equal(evt.Timestamp(), caught.Timestamp())
})
}
suite.stream.Send(suite.ctx, evt)
time.Sleep(100 * time.Millisecond)
}
func (suite *EventStreamTestSuite) TestEventTraffic() {
evt := mtglib.NewEventTraffic("connID", 1024, true)
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
v.
On("EventTraffic", mock.Anything).
Once().
Run(func(args mock.Arguments) {
caught, ok := args.Get(0).(mtglib.EventTraffic)
suite.True(ok)
suite.Equal(evt.StreamID(), caught.StreamID())
suite.Equal(evt.Timestamp(), caught.Timestamp())
suite.Equal(evt.Traffic, caught.Traffic)
suite.Equal(evt.IsRead, caught.IsRead)
})
}
suite.stream.Send(suite.ctx, evt)
time.Sleep(100 * time.Millisecond)
}
func (suite *EventStreamTestSuite) TestEventFinish() {
evt := mtglib.NewEventFinish("connID")
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
v.
On("EventFinish", mock.Anything).
Once().
Run(func(args mock.Arguments) {
caught, ok := args.Get(0).(mtglib.EventFinish)
suite.True(ok)
suite.Equal(evt.StreamID(), caught.StreamID())
suite.Equal(evt.Timestamp(), caught.Timestamp())
})
}
suite.stream.Send(suite.ctx, evt)
time.Sleep(100 * time.Millisecond)
}
func (suite *EventStreamTestSuite) TestEventConcurrencyLimited() {
evt := mtglib.NewEventConcurrencyLimited()
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
v.
On("EventConcurrencyLimited", mock.Anything).
Once().
Run(func(args mock.Arguments) {
caught, ok := args.Get(0).(mtglib.EventConcurrencyLimited)
suite.True(ok)
suite.Equal(evt.Timestamp(), caught.Timestamp())
suite.Empty(evt.StreamID())
})
}
suite.stream.Send(suite.ctx, evt)
time.Sleep(100 * time.Millisecond)
}
func (suite *EventStreamTestSuite) TestEventIPBlocklisted() {
evt := mtglib.NewEventIPBlocklisted(net.ParseIP("10.0.0.10"))
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
v.
On("EventIPBlocklisted", mock.Anything).
Once().
Run(func(args mock.Arguments) {
caught, ok := args.Get(0).(mtglib.EventIPBlocklisted)
suite.True(ok)
suite.Equal(evt.StreamID(), caught.StreamID())
suite.Equal(evt.Timestamp(), caught.Timestamp())
suite.Equal(evt.RemoteIP.String(), caught.RemoteIP.String())
})
}
suite.stream.Send(suite.ctx, evt)
time.Sleep(100 * time.Millisecond)
}
func (suite *EventStreamTestSuite) TestEventReplayAttack() {
evt := mtglib.NewEventReplayAttack("CONNID")
for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
v.
On("EventReplayAttack", mock.Anything).
Once().
Run(func(args mock.Arguments) {
caught, ok := args.Get(0).(mtglib.EventReplayAttack)
suite.True(ok)
suite.Equal(evt.StreamID(), caught.StreamID())
suite.Equal(evt.Timestamp(), caught.Timestamp())
})
}
suite.stream.Send(suite.ctx, evt)
time.Sleep(100 * time.Millisecond)
}
func (suite *EventStreamTestSuite) TearDownTest() {
suite.stream.Shutdown()
suite.ctxCancel()
time.Sleep(100 * time.Millisecond)
suite.observerMock1.AssertExpectations(suite.T())
suite.observerMock2.AssertExpectations(suite.T())
}
func TestEventStream(t *testing.T) {
t.Parallel()
suite.Run(t, &EventStreamTestSuite{})
}
-69
View File
@@ -1,69 +0,0 @@
// Events has a default implementations of EventStream for mtglib.
//
// Please see documentation for mtglib.EventStream interface to get an
// idea of such an abstraction. This package has implementations for the
// default event stream.
//
// Default event stream has a list of its own concepts. First, all it
// does is a routing of messages to known observers. It takes an event,
// defines its type and pass this message to a method of the observer.
//
// There might be many observers, but default event stream has a
// guarantee though. It uses StreamID as a sharding key and guarantees
// that a message with the same StreamID will be devlivered to the same
// observer instance. So, each producer is guarateed to get all relevant
// messages related to the same session. It is not possible that it will
// get EventFinish if it has not seen EventStart for that session yet.
package events
import "github.com/9seconds/mtg/v2/mtglib"
// Observer is an instance that listens for the incoming events.
//
// As it is said in the package description, the default event stream
// guarantees that all events with the same StreamID are going to be
// routed to the same instance of the observer. So, there is no need
// to synchronize information about streams between many observers
// instances, they can have their local storage.
type Observer interface {
// EventStart reacts on incoming mtglib.EventStart event.
EventStart(mtglib.EventStart)
// EventFinish reacts on incoming mtglib.EventFinish event.
EventFinish(mtglib.EventFinish)
// EventConnectedToDC reacts on incoming mtglib.EventConnectedToDC
// event.
EventConnectedToDC(mtglib.EventConnectedToDC)
// EventDomainFronting reacts on incoming mtglib.EventDomainFronting
// event.
EventDomainFronting(mtglib.EventDomainFronting)
// EventTraffic reacts on incoming mtglib.EventTraffic event.
EventTraffic(mtglib.EventTraffic)
// EventConcurrencyLimited reacts on incoming
// mtglib.EventConcurrencyLimited event.
EventConcurrencyLimited(mtglib.EventConcurrencyLimited)
// EventIPBlocklisted reacts on incoming mtglib.EventIPBlocklisted event.
EventIPBlocklisted(mtglib.EventIPBlocklisted)
// EventReplayAttack reacts on incoming mtglib.EventReplayAttack event.
EventReplayAttack(mtglib.EventReplayAttack)
// Shutdown stop observer. Default event stream guarantees:
// 1. If shutdown is executed, it is executed only once
// 2. Observer won't receieve any new message after this
// function call.
Shutdown()
}
// ObserverFactory creates a new instance of the observer.
//
// Default event stream creates a small set of goroutines to manage
// incoming messages. Each message is routed to an appropriate observer
// based on a sharding key, stream id. So, it is possible that an
// instance of mtg will have many observer instances, not a single one.
type ObserverFactory func() Observer
-46
View File
@@ -1,46 +0,0 @@
package events_test
import (
"github.com/9seconds/mtg/v2/mtglib"
"github.com/stretchr/testify/mock"
)
type ObserverMock struct {
mock.Mock
}
func (o *ObserverMock) EventStart(evt mtglib.EventStart) {
o.Called(evt)
}
func (o *ObserverMock) EventConnectedToDC(evt mtglib.EventConnectedToDC) {
o.Called(evt)
}
func (o *ObserverMock) EventDomainFronting(evt mtglib.EventDomainFronting) {
o.Called(evt)
}
func (o *ObserverMock) EventTraffic(evt mtglib.EventTraffic) {
o.Called(evt)
}
func (o *ObserverMock) EventFinish(evt mtglib.EventFinish) {
o.Called(evt)
}
func (o *ObserverMock) EventConcurrencyLimited(evt mtglib.EventConcurrencyLimited) {
o.Called(evt)
}
func (o *ObserverMock) EventIPBlocklisted(evt mtglib.EventIPBlocklisted) {
o.Called(evt)
}
func (o *ObserverMock) EventReplayAttack(evt mtglib.EventReplayAttack) {
o.Called(evt)
}
func (o *ObserverMock) Shutdown() {
o.Called()
}
-149
View File
@@ -1,149 +0,0 @@
package events
import (
"sync"
"github.com/9seconds/mtg/v2/mtglib"
)
type multiObserver struct {
observers []Observer
}
func (m multiObserver) EventStart(evt mtglib.EventStart) {
wg := &sync.WaitGroup{}
wg.Add(len(m.observers))
for _, v := range m.observers {
go func(obs Observer) {
defer wg.Done()
obs.EventStart(evt)
}(v)
}
wg.Wait()
}
func (m multiObserver) EventConnectedToDC(evt mtglib.EventConnectedToDC) {
wg := &sync.WaitGroup{}
wg.Add(len(m.observers))
for _, v := range m.observers {
go func(obs Observer) {
defer wg.Done()
obs.EventConnectedToDC(evt)
}(v)
}
wg.Wait()
}
func (m multiObserver) EventDomainFronting(evt mtglib.EventDomainFronting) {
wg := &sync.WaitGroup{}
wg.Add(len(m.observers))
for _, v := range m.observers {
go func(obs Observer) {
defer wg.Done()
obs.EventDomainFronting(evt)
}(v)
}
wg.Wait()
}
func (m multiObserver) EventTraffic(evt mtglib.EventTraffic) {
wg := &sync.WaitGroup{}
wg.Add(len(m.observers))
for _, v := range m.observers {
go func(obs Observer) {
defer wg.Done()
obs.EventTraffic(evt)
}(v)
}
wg.Wait()
}
func (m multiObserver) EventFinish(evt mtglib.EventFinish) {
wg := &sync.WaitGroup{}
wg.Add(len(m.observers))
for _, v := range m.observers {
go func(obs Observer) {
defer wg.Done()
obs.EventFinish(evt)
}(v)
}
wg.Wait()
}
func (m multiObserver) EventConcurrencyLimited(evt mtglib.EventConcurrencyLimited) {
wg := &sync.WaitGroup{}
wg.Add(len(m.observers))
for _, v := range m.observers {
go func(obs Observer) {
defer wg.Done()
obs.EventConcurrencyLimited(evt)
}(v)
}
wg.Wait()
}
func (m multiObserver) EventIPBlocklisted(evt mtglib.EventIPBlocklisted) {
wg := &sync.WaitGroup{}
wg.Add(len(m.observers))
for _, v := range m.observers {
go func(obs Observer) {
defer wg.Done()
obs.EventIPBlocklisted(evt)
}(v)
}
wg.Wait()
}
func (m multiObserver) EventReplayAttack(evt mtglib.EventReplayAttack) {
wg := &sync.WaitGroup{}
wg.Add(len(m.observers))
for _, v := range m.observers {
go func(obs Observer) {
defer wg.Done()
obs.EventReplayAttack(evt)
}(v)
}
wg.Wait()
}
func (m multiObserver) Shutdown() {
for _, v := range m.observers {
v.Shutdown()
}
}
func newMultiObserver(factories []ObserverFactory) Observer {
observers := make([]Observer, len(factories))
for i, v := range factories {
observers[i] = v()
}
return multiObserver{
observers: observers,
}
}
-33
View File
@@ -1,33 +0,0 @@
package events
import (
"context"
"github.com/9seconds/mtg/v2/mtglib"
)
type noop struct{}
func (n noop) Send(ctx context.Context, evt mtglib.Event) {}
// NewNoopStream creates a stream which discards each message.
func NewNoopStream() mtglib.EventStream {
return noop{}
}
type noopObserver struct{}
func (n noopObserver) EventStart(_ mtglib.EventStart) {}
func (n noopObserver) EventConnectedToDC(_ mtglib.EventConnectedToDC) {}
func (n noopObserver) EventDomainFronting(_ mtglib.EventDomainFronting) {}
func (n noopObserver) EventTraffic(_ mtglib.EventTraffic) {}
func (n noopObserver) EventFinish(_ mtglib.EventFinish) {}
func (n noopObserver) EventConcurrencyLimited(_ mtglib.EventConcurrencyLimited) {}
func (n noopObserver) EventIPBlocklisted(_ mtglib.EventIPBlocklisted) {}
func (n noopObserver) EventReplayAttack(_ mtglib.EventReplayAttack) {}
func (n noopObserver) Shutdown() {}
// NewNoopObserver creates an observer which discards each message.
func NewNoopObserver() Observer {
return noopObserver{}
}
-78
View File
@@ -1,78 +0,0 @@
package events_test
import (
"context"
"net"
"testing"
"github.com/9seconds/mtg/v2/events"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/stretchr/testify/suite"
)
type NoopTestSuite struct {
suite.Suite
testData map[string]mtglib.Event
ctx context.Context
}
func (suite *NoopTestSuite) SetupSuite() {
suite.testData = map[string]mtglib.Event{
"start": mtglib.NewEventStart("connID", net.ParseIP("127.0.0.1")),
"connected-to-dc": mtglib.NewEventConnectedToDC("connID", net.ParseIP("127.1.0.1"), 2),
"domain-fronting": mtglib.NewEventDomainFronting("connID"),
"traffic": mtglib.NewEventTraffic("connID", 1000, true),
"finish": mtglib.NewEventFinish("connID"),
"concurrency-limited": mtglib.NewEventConcurrencyLimited(),
"ip-blacklisted": mtglib.NewEventIPBlocklisted(net.ParseIP("10.0.0.10")),
"replay-attack": mtglib.NewEventReplayAttack("connID"),
}
suite.ctx = context.Background()
}
func (suite *NoopTestSuite) TestStream() {
stream := events.NewNoopStream()
for name, v := range suite.testData {
value := v
suite.T().Run(name, func(t *testing.T) {
stream.Send(suite.ctx, value)
})
}
}
func (suite *NoopTestSuite) TestObserver() {
observer := events.NewNoopObserver()
for name, v := range suite.testData {
value := v
suite.T().Run(name, func(t *testing.T) {
switch typedEvt := value.(type) {
case mtglib.EventStart:
observer.EventStart(typedEvt)
case mtglib.EventConnectedToDC:
observer.EventConnectedToDC(typedEvt)
case mtglib.EventDomainFronting:
observer.EventDomainFronting(typedEvt)
case mtglib.EventFinish:
observer.EventFinish(typedEvt)
case mtglib.EventConcurrencyLimited:
observer.EventConcurrencyLimited(typedEvt)
case mtglib.EventIPBlocklisted:
observer.EventIPBlocklisted(typedEvt)
case mtglib.EventReplayAttack:
observer.EventReplayAttack(typedEvt)
}
})
}
observer.Shutdown()
}
func TestNoop(t *testing.T) {
t.Parallel()
suite.Run(t, &NoopTestSuite{})
}
-186
View File
@@ -1,186 +0,0 @@
# This is an example of the configuration file for mtg. You actually can
# run mtg with it. It starts a proxy on all interfaces with a secret
# ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d
#
# It has all possible options with default values. So, a real world
# configuration file should contain only those options you are going to
# use. You do not need to enumerate all of them. In other words, each
# option here has a default value. If you comment a key-value pair, it
# should not make any effect.
#
# stats is the only exception.
# Debug starts application in debug mode. It starts to be quite verbose
# in output. Actually, the idea is that you run it in debug mode only if
# you have any issue.
debug = true
# A secret. Please remember that mtg supports only FakeTLS mode, legacy
# simple and secured mode are prohibited. For you it means that secret
# should either be base64-encoded or starts with ee.
secret = "ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d"
# Host:port pair to run proxy on.
bind-to = "0.0.0.0:3128"
# Defines how many concurrent connections are allowed to this proxy.
# All other incoming connections are going to be dropped.
concurrency = 8192
# A size of user-space buffer for TCP to use. Since we do 2 connections,
# then we have tcp-buffer * (4 + 2) per each connection: read/write for
# each connection + 2 copy buffers to pump the data between sockets.
tcp-buffer = "4kb"
# Sometimes you want to enforce mtg to use some types of
# IP connectivity to Telegram. We have 4 modes:
# - prefer-ipv6:
# We can use both ipv4 and ipv6 but ipv6 has a preference
# - prefer-ipv4:
# We can use both ipv4 and ipv6 but ipv4 has a preference
# - only-ipv6:
# Only ipv6 connectivity is used
# - only-ipv4:
# Only ipv4 connectivity is used
prefer-ip = "prefer-ipv6"
# FakeTLS uses domain fronting protection. So it needs to know a port to
# access.
domain-fronting-port = 443
# FakeTLS can compare timestamps to prevent probes. Each message has
# encrypted timestamp. So, mtg can compare this timestamp and decide if
# we need to proceed with connection or not.
#
# Sometimes time can be skewed so we accept all messages within a
# time range of this parameter.
tolerate-time-skewness = "5s"
# network defines different network-related settings
[network]
# please be aware that mtg needs to do some external requests. For
# example, if you do not pass public ips, it will request your public ip
# address from some external service.
#
# As for 2.0, if you set a public-ip on your own, mtg won't issue any
# network requests except of those required for Telegram.
#
# so, in order of doing them, it needs to do DNS lookup. mtg ignores DNS
# resolver of the operating system and uses DOH instead. This is a host
# it has to access.
#
# By default we use Quad9.
doh-ip = "9.9.9.9"
# mtg can work via proxies (for now, we support only socks5). Proxy
# configuration is done via list. So, you can specify many proxies
# there.
#
# Actually, if you supply an empty list, then no proxies are going to be
# used. If you supply a single proxy, then mtg will use it exclusively.
# If you supply >= 2, then mtg will load balance between them.
#
# If you add an empty string here, this is an equivalent of 'plain network',
# with no proxy usage.
#
# Proxy configuration is done via ordinary URI schema:
#
# socks5://user:password@host:port?open_threshold=5&half_open_timeout=1m&reset_failures_timeout=10s
#
# Only socks5 proxy is used. user/password is optional. As you can
# see, you can specify some parameters in GET query. These parameters
# configure circuit breaker.
#
# open_threshold means a number of errors which should happen so we stop
# use a proxy.
#
# half_open_timeout means a time period (in Golang duration notation)
# after which we can retry with this proxy
#
# reset_failures_timeout means a time period when we flush out errors
# when circuit breaker in closed state.
#
# Please see https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker
# on details about circuit breakers.
proxies = [
# "socks5://user:password@host:port?open_threshold=5&half_open_timeout=1m&reset_failures_timeout=10s"
]
# network timeouts define different settings for timeouts. tcp timeout
# define a global timeout on establishing of network connections. idle
# means a timeout on pumping data between sockset when nothing is
# happening.
#
# please be noticed that handshakes have no timeouts intentionally. You can
# find a reasoning here:
# https://www.ndss-symposium.org/wp-content/uploads/2020/02/23087-paper.pdf
[network.timeout]
tcp = "5s"
http = "10s"
idle = "1m"
# Some countries do active probing on Telegram connections. This technique
# allows to protect from such effort.
#
# mtg has a cache of some connection fingerprints. Actually, first bytes
# of each connection. So, it stores them in some in-memory LRU+TTL cache.
# You can configure this cache here.
[defense.anti-replay]
# You can enable/disable this feature.
enabled = true
# max size of such a cache. Please be aware that this number is
# approximate we try hard to store data quite dense but it is possible
# that we can go over this limit for 10-20% under some conditions and
# architectures.
max-size = "1mib"
# we use stable bloom filters for anti-replay cache. This helps
# to maintain a desired error ratio.
error-rate = 0.001
# You can protect proxies by using different blocklists. If client has
# ip from the given range, we do not try to do a proper handshake. We
# actually route it to fronting domain. So, this client will never ever
# have a chance to use mtg to access Telegram.
#
# Please remember that blocklists are initialized in async way. So,
# when you start a proxy, blocklists are empty, they are populated and
# processed in backgrounds. An error in any URL is ignored.
[defense.blocklist]
# You can enable/disable this feature.
enabled = true
# This is a limiter for concurrency. In order to protect website
# from overloading, we download files in this number of threads.
download-concurrency = 2
# A list of URLs in FireHOL format (https://iplists.firehol.org/)
# You can provider links here (starts with https:// or http://) or
# path to a local file, but in this case it should be absolute.
urls = [
# "https://iplists.firehol.org/files/firehol_level1.netset",
# "/local.file"
]
# How often do we need to update a blocklist set.
update-each = "24h"
# statsd statistics integration.
[stats.statsd]
# enabled/disabled
enabled = false
# host:port for UDP endpoint of statsd
address = "127.0.0.1:8888"
# prefix of metric for statsd
metric-prefix = "mtg"
# tag format to use
# supported values are 'datadog', 'influxdb' and 'graphite'
# default format is graphite.
tag-format = "datadog"
# prometheus metrics integration.
[stats.prometheus]
# enabled/disabled
enabled = true
# host:port where to start http server for endpoint
bind-to = "127.0.0.1:3129"
# prefix of http path
http-path = "/"
# prefix for metrics for prometheus
metric-prefix = "mtg"
+134
View File
@@ -0,0 +1,134 @@
package faketls
import (
"bufio"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"strconv"
"sync"
"time"
"github.com/9seconds/mtg/antireplay"
"github.com/9seconds/mtg/config"
"github.com/9seconds/mtg/conntypes"
"github.com/9seconds/mtg/obfuscated2"
"github.com/9seconds/mtg/protocol"
"github.com/9seconds/mtg/stats"
"github.com/9seconds/mtg/tlstypes"
"github.com/9seconds/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[:])
serverHello := tlstypes.NewServerHello(clientHello)
serverHelloPacket := serverHello.WelcomePacket()
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,
}
)
+16 -27
View File
@@ -1,31 +1,20 @@
module github.com/9seconds/mtg/v2
module github.com/9seconds/mtg
go 1.16
go 1.13
require (
github.com/OneOfOne/xxhash v1.2.8
github.com/alecthomas/kong v0.2.16
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6
github.com/d4l3k/messagediff v1.2.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/gotd/td v0.34.0
github.com/jarcoal/httpmock v1.0.8
github.com/kentik/patricia v0.0.0-20201202224819-f9447a6e25f1
github.com/libp2p/go-reuseport v0.0.2
github.com/mccutchen/go-httpbin v1.1.1
github.com/panjf2000/ants/v2 v2.4.4
github.com/pelletier/go-toml v1.9.0
github.com/prometheus/client_golang v1.10.0
github.com/prometheus/common v0.23.0 // indirect
github.com/rs/zerolog v1.21.0
github.com/smira/go-statsd v1.3.2
github.com/stretchr/objx v0.3.0 // indirect
github.com/stretchr/testify v1.7.0
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6
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/v2 v2.1.1 // indirect
github.com/prometheus/client_golang v1.2.1
github.com/prometheus/procfs v0.0.7 // indirect
github.com/smira/go-statsd v1.3.1
go.uber.org/multierr v1.4.0 // indirect
go.uber.org/zap v1.13.0
golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f
golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914 // indirect
golang.org/x/sys v0.0.0-20191119060738-e882bf8e40c2
golang.org/x/tools v0.0.0-20191118222007-07fc4c7f2b98 // indirect
gopkg.in/alecthomas/kingpin.v2 v2.2.6
)
+72 -430
View File
@@ -1,536 +1,178 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/PuerkitoBio/goquery v1.6.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/alecthomas/kong v0.2.16 h1:F232CiYSn54Tnl1sJGTeHmx4vJDNLVP2b9yCVMOQwHQ=
github.com/alecthomas/kong v0.2.16/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
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=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
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/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/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4=
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=
github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=
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=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc=
github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
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/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
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-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
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-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
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/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
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/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gotd/getdoc v0.6.2/go.mod h1:6Ul8cyaq+Aw0gVyuU+TKZPCmNjdlylzSIWVxjzI5t/U=
github.com/gotd/ige v0.1.5 h1:qITql4hZpqPM/DSeO5IVlxzXJxrs4ZZiKRwPif2K76U=
github.com/gotd/ige v0.1.5/go.mod h1:LbvqjUuGELVuHcKjfJZ2NPNzegICQU4eqrPAkXy0nTs=
github.com/gotd/neo v0.1.2 h1:hayFfKNSH5RP/L+KKKnsKMdDNCdhSfzVuo5TF4wvpa8=
github.com/gotd/neo v0.1.2/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
github.com/gotd/td v0.34.0 h1:BLIdpv2sxbCuWI2fyXCECRQuLJdnYk3VMZDRKNog/E8=
github.com/gotd/td v0.34.0/go.mod h1:4s/7cuEscdvZIecM/pVi5L3nTK+djSoVLAhgfk40OE4=
github.com/gotd/tl v0.4.0/go.mod h1:CMIcjPWFS4qxxJ+1Ce7U/ilbtPrkoVo/t8uhN5Y/D7c=
github.com/gotd/xor v0.1.0/go.mod h1:ZTmdgqf6SOHder8/MFp9CNkXIadzID5lIiaZxRZICH0=
github.com/gotd/xor v0.1.1 h1:LSPEeuf7noTo4fi4PrEsAaWXOSwjsY2e+IINPiR+c7s=
github.com/gotd/xor v0.1.1/go.mod h1:ZTmdgqf6SOHder8/MFp9CNkXIadzID5lIiaZxRZICH0=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k=
github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
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/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/k0kubun/pp v2.4.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=
github.com/kentik/patricia v0.0.0-20201202224819-f9447a6e25f1 h1:D7qhJP3R49ZjUzpzKQ6B2H3lgejPs6DTO5gRomhhOpE=
github.com/kentik/patricia v0.0.0-20201202224819-f9447a6e25f1/go.mod h1:2OfLA+0esiUJpwMjrH39pEk79cb8MvGTBS9YlZpejJ4=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
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/konsorten/go-windows-terminal-sequences v1.0.3/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=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/libp2p/go-reuseport v0.0.2 h1:XSG94b1FJfGA01BUrT82imejHQyTxO4jEWqheyCXYvU=
github.com/libp2p/go-reuseport v0.0.2/go.mod h1:SPD+5RwGC7rcnzngoYC86GjPzjSywuQyMVAheVBD9nQ=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mccutchen/go-httpbin v1.1.1 h1:aEws49HEJEyXHLDnshQVswfUlCVoS8g6h9YaDyaW7RE=
github.com/mccutchen/go-httpbin v1.1.1/go.mod h1:fhpOYavp5g2K74XDl/ao2y4KvhqVtKlkg1e+0UaQv7I=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/panjf2000/ants/v2 v2.4.4 h1:kebk2KSiXHGeiYS6b+w2RqNN5+IKoqlBNd7cuC7MvQI=
github.com/panjf2000/ants/v2 v2.4.4/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.9.0 h1:NOd0BRdOKpPf0SxkL3HxSQOG7rNh+4kl6PHcBPFs7Q0=
github.com/pelletier/go-toml v1.9.0/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.10.0 h1:/o0BDeWzLWXNZ+4q5gXltUvaMpJqckTa+jTNoB+z4cg=
github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU=
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-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
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.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
github.com/prometheus/common v0.23.0 h1:GXWvPYuTUenIa+BhOq/x+L/QZzCqASkVRny5KTlPDGM=
github.com/prometheus/common v0.23.0/go.mod h1:H6QK/N6XVT42whUeIdI3dp36w49c+/iMDk7UAI2qm7Q=
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.0-20190117184657-bf6a532e95b1/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.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/quasilyte/go-ruleguard/dsl v0.3.2/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
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/prometheus/procfs v0.0.7 h1:RS5GAlMbnkWkhs4+bPocMTmGjYkuCY5djjqEDdXOhcQ=
github.com/prometheus/procfs v0.0.7/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.21.0 h1:Q3vdXlfLNT+OftyBHsU0Y445MD+8m8axjKgf2si0QcM=
github.com/rs/zerolog v1.21.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
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/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smira/go-statsd v1.3.2 h1:1EeuzxNZ/TD9apbTOFSM9nulqfcsQFmT4u1A2DREabI=
github.com/smira/go-statsd v1.3.2/go.mod h1:1srXJ9/pbnN04G8f4F1jUzsGOnwkPKXciyqpewGlkC4=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/smira/go-statsd v1.3.1 h1:JalGiHNdK7GqVAPpg7j0Kwp2jZrz/fCg/B4ZuNuBY2w=
github.com/smira/go-statsd v1.3.1/go.mod h1:1srXJ9/pbnN04G8f4F1jUzsGOnwkPKXciyqpewGlkC4=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/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/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.1.5-0.20170809224252-890a5c3458b4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
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=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43 h1:QEePdg0ty2r0t1+qwfZmQ4OOl/MB2UXIeJSpIZv56lg=
github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43/go.mod h1:OYRfF6eb5wY9VRFkXJH8FFBi3plw2v+giaIu7P054pM=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
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/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg=
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.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/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-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e h1:8foAy0aoO5GkqCvAEJ4VC4P3zksTg4X4aJCDpZzmgQI=
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f h1:kz4KIr+xcPUsI3VMoqWfPMvtnJ6MGfiVwsWSVzphMO4=
golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f/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/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/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-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125 h1:Ugb8sMTWuWRC3+sz5WeN/4kejDx9BvIwnPUiJBjJE+8=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914 h1:MlY3mEfbnWGmUi4rtHOtNnnnN4UJRGSyLPx+DXA5Sq4=
golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914/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-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/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-20181122145206-62eef0e2fa9b/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6 h1:cdsMqa2nXzqlgs183pHxtvoVwU7CyzaCTAUOg94af4c=
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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-20191119060738-e882bf8e40c2 h1:wAW1U21MfVN0sUipAD8952TBjGXMRHFKQugDlQ9RwwE=
golang.org/x/sys v0.0.0-20191119060738-e882bf8e40c2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.0.0-20191118222007-07fc4c7f2b98 h1:tZwpOHmF1OEL9wJGSgBALnhFg/8VKjQTtctCX51GLNI=
golang.org/x/tools v0.0.0-20191118222007-07fc4c7f2b98/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
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/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/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
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=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
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=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
+163
View File
@@ -0,0 +1,163 @@
package hub
import (
"fmt"
"math/rand"
"sync"
"go.uber.org/zap"
"github.com/9seconds/mtg/conntypes"
"github.com/9seconds/mtg/mtproto"
"github.com/9seconds/mtg/mtproto/rpc"
"github.com/9seconds/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
}
+69
View File
@@ -0,0 +1,69 @@
package hub
import (
"fmt"
"sort"
"github.com/9seconds/mtg/config"
)
type connectionList struct {
connections []*connection
}
func (c *connectionList) get(conn *ProxyConn) (*connection, error) {
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)
if prevLen == 0 {
return
}
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"
"github.com/9seconds/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 "github.com/9seconds/mtg/protocol"
type Interface interface {
Register(*protocol.TelegramRequest) (*ProxyConn, error)
}
+90
View File
@@ -0,0 +1,90 @@
package hub
import (
"context"
"time"
"github.com/9seconds/mtg/conntypes"
"github.com/9seconds/mtg/protocol"
)
const muxGCEvery = time.Minute
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() {
gcTicker := time.NewTicker(muxGCEvery)
defer gcTicker.Stop()
for {
select {
case <-m.ctx.Done():
for _, v := range m.clients {
v.Close()
}
return
case <-gcTicker.C:
m.connections.gc()
case req := <-m.channelNew:
m.connections.gc()
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
}
+79
View File
@@ -0,0 +1,79 @@
package hub
import (
"sync"
"time"
"github.com/9seconds/mtg/conntypes"
"github.com/9seconds/mtg/mtproto/rpc"
"github.com/9seconds/mtg/protocol"
)
const (
proxyConnWriteTimeout = 2 * time.Minute
proxyConnReadTimeout = 2 * time.Minute
proxyConnBackpressureAfter = 10
)
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, proxyConnBackpressureAfter),
channelDone: make(chan struct{}),
channelClosed: channelClosed,
req: req,
}
}
-192
View File
@@ -1,192 +0,0 @@
package cli
import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
)
type accessResponse struct {
IPv4 *accessResponseURLs `json:"ipv4,omitempty"`
IPv6 *accessResponseURLs `json:"ipv6,omitempty"`
Secret struct {
Hex string `json:"hex"`
Base64 string `json:"base64"`
} `json:"secret"`
}
type accessResponseURLs struct {
IP net.IP `json:"ip"`
Port uint `json:"port"`
TgURL string `json:"tg_url"`
TgQrCode string `json:"tg_qrcode"`
TmeURL string `json:"tme_url"`
TmeQrCode string `json:"tme_qrcode"`
}
type Access struct {
base
PublicIPv4 net.IP `kong:"help='Public IPv4 address for proxy. By default it is resolved via remote website',name='ipv4',short='i'"` // nolint: lll
PublicIPv6 net.IP `kong:"help='Public IPv6 address for proxy. By default it is resolved via remote website',name='ipv6',short='I'"` // nolint: lll
Port uint `kong:"help='Port number. Default port is taken from configuration file, bind-to parameter',type:'uint',short='p'"` // nolint: lll
Hex bool `kong:"help='Print secret in hex encoding.',short='x'"`
}
func (c *Access) Run(cli *CLI, version string) error {
if err := c.ReadConfig(version); err != nil {
return fmt.Errorf("cannot init config: %w", err)
}
return c.Execute(cli)
}
func (c *Access) Execute(cli *CLI) error {
resp := &accessResponse{}
resp.Secret.Base64 = c.Config.Secret.Base64()
resp.Secret.Hex = c.Config.Secret.Hex()
wg := &sync.WaitGroup{}
wg.Add(2) // nolint: gomnd
go func() {
defer wg.Done()
ip := cli.Access.PublicIPv4
if ip == nil {
ip = c.getIP("tcp4")
}
if ip != nil {
ip = ip.To4()
}
resp.IPv4 = c.makeURLs(ip, cli)
}()
go func() {
defer wg.Done()
ip := cli.Access.PublicIPv6
if ip == nil {
ip = c.getIP("tcp6")
}
if ip != nil {
ip = ip.To16()
}
resp.IPv6 = c.makeURLs(ip, cli)
}()
wg.Wait()
encoder := json.NewEncoder(os.Stdout)
encoder.SetEscapeHTML(false)
encoder.SetIndent("", " ")
if err := encoder.Encode(resp); err != nil {
return fmt.Errorf("cannot dump access json: %w", err)
}
return nil
}
func (c *Access) getIP(protocol string) net.IP {
client := c.Network.MakeHTTPClient(func(ctx context.Context, network, address string) (net.Conn, error) {
return c.Network.DialContext(ctx, protocol, address) // nolint: wrapcheck
})
req, err := http.NewRequest(http.MethodGet, "https://ifconfig.co", nil) // nolint: noctx
if err != nil {
panic(err)
}
req.Header.Add("Accept", "text/plain")
resp, err := client.Do(req)
if err != nil {
return nil
}
if resp.StatusCode != http.StatusOK {
return nil
}
defer func() {
io.Copy(io.Discard, resp.Body) // nolint: errcheck
resp.Body.Close()
}()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil
}
return net.ParseIP(strings.TrimSpace(string(data)))
}
func (c *Access) makeURLs(ip net.IP, cli *CLI) *accessResponseURLs {
if ip == nil {
return nil
}
portNo := cli.Access.Port
if portNo == 0 {
portNo = c.Config.BindTo.PortValue(0)
}
values := url.Values{}
values.Set("server", ip.String())
values.Set("port", strconv.Itoa(int(portNo)))
if cli.Access.Hex {
values.Set("secret", c.Config.Secret.Hex())
} else {
values.Set("secret", c.Config.Secret.Base64())
}
urlQuery := values.Encode()
rv := &accessResponseURLs{
IP: ip,
Port: portNo,
TgURL: (&url.URL{
Scheme: "tg",
Host: "proxy",
RawQuery: urlQuery,
}).String(),
TmeURL: (&url.URL{
Scheme: "https",
Host: "t.me",
Path: "proxy",
RawQuery: urlQuery,
}).String(),
}
rv.TgQrCode = c.makeQRCode(rv.TgURL)
rv.TmeQrCode = c.makeQRCode(rv.TmeURL)
return rv
}
func (c *Access) makeQRCode(data string) string {
values := url.Values{}
values.Set("qzone", "4")
values.Set("format", "svg")
values.Set("data", data)
return (&url.URL{
Scheme: "https",
Host: "api.qrserver.com",
Path: "v1/create-qr-code",
RawQuery: values.Encode(),
}).String()
}
-197
View File
@@ -1,197 +0,0 @@
package cli_test
import (
"net"
"net/http"
"testing"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/9seconds/mtg/v2/internal/testlib"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/suite"
"github.com/xeipuuv/gojsonschema"
)
var accressResponseJSONSchema = func() *gojsonschema.Schema {
schema, err := gojsonschema.NewSchema(gojsonschema.NewStringLoader(`
{
"type": "object",
"required": ["secret"],
"additionalProperties": true,
"properties": {
"secret": {
"type": "object",
"required": [
"hex",
"base64"
],
"additionalProperties": false,
"properties": {
"hex": {
"type": "string",
"minLength": 34
},
"base64": {
"type": "string",
"minLength": 10
}
}
},
"ipv4": {
"$ref": "#/definitions/ip"
},
"ipv6": {
"$ref": "#/definitions/ip"
}
},
"definitions": {
"ip": {
"type": "object",
"required": [
"ip",
"port",
"tg_url",
"tg_qrcode",
"tme_url",
"tme_qrcode"
],
"additionalProperties": false,
"properties": {
"ip": {
"type": "string",
"minLength": 1,
"anyOf": [
{
"format": "ipv4"
},
{
"format": "ipv6"
}
]
},
"port": {
"type": "integer",
"multipleOf": 1.0,
"exclusiveMinimum": 0,
"exclusiveMaximum": 65536
},
"tg_url": {
"type": "string",
"minLength": 1,
"format": "uri"
},
"tg_qrcode": {
"type": "string",
"minLength": 1,
"format": "uri"
},
"tme_url": {
"type": "string",
"minLength": 1,
"format": "uri"
},
"tme_qrcode": {
"type": "string",
"minLength": 1,
"format": "uri"
}
}
}
}
}
`))
if err != nil {
panic(err)
}
return schema
}()
type AccessTestSuite struct {
CommonTestSuite
}
func (suite *AccessTestSuite) SetupTest() {
suite.CommonTestSuite.SetupTest()
suite.cli.Access.Config = &config.Config{}
suite.cli.Access.Config.Secret = mtglib.GenerateSecret("google.com")
suite.cli.Access.Network = suite.networkMock
suite.NoError(
suite.cli.Access.Config.BindTo.UnmarshalText([]byte("0.0.0.0:80")))
}
func (suite *AccessTestSuite) TestGenerateNoCalls() {
suite.cli.Access.PublicIPv4 = net.ParseIP("10.0.0.10")
suite.cli.Access.PublicIPv6 = net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")
output := testlib.CaptureStdout(func() {
suite.NoError(suite.cli.Access.Execute(suite.cli))
})
validated, err := accressResponseJSONSchema.Validate(
gojsonschema.NewStringLoader(output))
suite.NoError(err)
suite.Empty(validated.Errors())
suite.True(validated.Valid())
suite.Contains(output, "10.0.0.10")
suite.Contains(output, "2001:db8:85a3::8a2e:370:7334")
suite.Contains(output, "ipv4")
suite.Contains(output, "ipv6")
suite.Contains(output, suite.cli.Access.Config.Secret.Base64())
suite.Contains(output, suite.cli.Access.Config.Secret.Hex())
}
func (suite *AccessTestSuite) TestGenerateIPv4Call() {
suite.cli.Access.PublicIPv6 = net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")
httpmock.RegisterResponder(http.MethodGet, "https://ifconfig.co",
httpmock.NewStringResponder(http.StatusOK, "10.11.12.13"))
output := testlib.CaptureStdout(func() {
suite.NoError(suite.cli.Access.Execute(suite.cli))
})
validated, err := accressResponseJSONSchema.Validate(
gojsonschema.NewStringLoader(output))
suite.NoError(err)
suite.Empty(validated.Errors())
suite.True(validated.Valid())
suite.Contains(output, "10.11.12.13")
suite.Contains(output, "2001:db8:85a3::8a2e:370:7334")
suite.Contains(output, "ipv4")
suite.Contains(output, "ipv6")
suite.Contains(output, suite.cli.Access.Config.Secret.Base64())
suite.Contains(output, suite.cli.Access.Config.Secret.Hex())
}
func (suite *AccessTestSuite) TestIPv4CallFail() {
suite.cli.Access.PublicIPv6 = net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")
httpmock.RegisterResponder(http.MethodGet, "https://ifconfig.co",
httpmock.NewStringResponder(http.StatusForbidden, ""))
output := testlib.CaptureStdout(func() {
suite.NoError(suite.cli.Access.Execute(suite.cli))
})
validated, err := accressResponseJSONSchema.Validate(
gojsonschema.NewStringLoader(output))
suite.NoError(err)
suite.Empty(validated.Errors())
suite.True(validated.Valid())
suite.Contains(output, "2001:db8:85a3::8a2e:370:7334")
suite.NotContains(output, "ipv4")
suite.Contains(output, "ipv6")
suite.Contains(output, suite.cli.Access.Config.Secret.Base64())
suite.Contains(output, suite.cli.Access.Config.Secret.Hex())
}
func TestAccess(t *testing.T) { // nolint: paralleltest
suite.Run(t, &AccessTestSuite{})
}
-81
View File
@@ -1,81 +0,0 @@
package cli
import (
"fmt"
"net"
"net/url"
"os"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/9seconds/mtg/v2/network"
)
type base struct {
ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` // nolint: lll
Network mtglib.Network `kong:"-"`
Config *config.Config `kong:"-"`
}
func (b *base) ReadConfig(version string) error {
content, err := os.ReadFile(b.ConfigPath)
if err != nil {
return fmt.Errorf("cannot read config file: %w", err)
}
conf, err := config.Parse(content)
if err != nil {
return fmt.Errorf("cannot parse config: %w", err)
}
ntw, err := b.makeNetwork(conf, version)
if err != nil {
return fmt.Errorf("cannot build a network: %w", err)
}
b.Config = conf
b.Network = ntw
return nil
}
func (b *base) makeNetwork(conf *config.Config, version string) (mtglib.Network, error) {
tcpTimeout := conf.Network.Timeout.TCP.Value(network.DefaultTimeout)
httpTimeout := conf.Network.Timeout.HTTP.Value(network.DefaultHTTPTimeout)
dohIP := conf.Network.DOHIP.Value(net.ParseIP(network.DefaultDOHHostname)).String()
bufferSize := conf.TCPBuffer.Value(network.DefaultBufferSize)
userAgent := "mtg/" + version
baseDialer, err := network.NewDefaultDialer(tcpTimeout, int(bufferSize))
if err != nil {
return nil, fmt.Errorf("cannot build a default dialer: %w", err)
}
proxyURLs := make([]*url.URL, 0, len(conf.Network.Proxies))
for _, v := range conf.Network.Proxies {
if value := v.Value(nil); value != nil {
proxyURLs = append(proxyURLs, v.Value(nil))
}
}
switch len(proxyURLs) {
case 0:
return network.NewNetwork(baseDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck
case 1:
socksDialer, err := network.NewSocks5Dialer(baseDialer, proxyURLs[0])
if err != nil {
return nil, fmt.Errorf("cannot build socks5 dialer: %w", err)
}
return network.NewNetwork(socksDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck
}
socksDialer, err := network.NewLoadBalancedSocks5Dialer(baseDialer, proxyURLs)
if err != nil {
return nil, fmt.Errorf("cannot build socks5 dialer: %w", err)
}
return network.NewNetwork(socksDialer, userAgent, dohIP, httpTimeout) // nolint: wrapcheck
}
-33
View File
@@ -1,33 +0,0 @@
package cli
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/suite"
)
type BaseTestSuite struct {
suite.Suite
b base
}
func (suite *BaseTestSuite) SetupTest() {
suite.b = base{}
}
func (suite *BaseTestSuite) TestReadConfigNok() {
suite.b.ConfigPath = filepath.Join("testdata", "unknown")
suite.Error(suite.b.ReadConfig("dev"))
}
func (suite *BaseTestSuite) TestReadConfig() {
suite.b.ConfigPath = filepath.Join("testdata", "minimal.toml")
suite.NoError(suite.b.ReadConfig("dev"))
}
func TestBase(t *testing.T) {
t.Parallel()
suite.Run(t, &BaseTestSuite{})
}
-10
View File
@@ -1,10 +0,0 @@
package cli
import "github.com/alecthomas/kong"
type CLI struct {
GenerateSecret GenerateSecret `kong:"cmd,help='Generate new proxy secret'"`
Access Access `kong:"cmd,help='Print access information.'"`
Run Proxy `kong:"cmd,help='Run proxy.'"`
Version kong.VersionFlag `kong:"help='Print version.',short='v'"`
}
-24
View File
@@ -1,24 +0,0 @@
package cli
import (
"fmt"
"github.com/9seconds/mtg/v2/mtglib"
)
type GenerateSecret struct {
HostName string `kong:"arg,required,help='Hostname to use for domain fronting.',name='hostname'"`
Hex bool `kong:"help='Print secret in hex encoding.',short='x'"`
}
func (c *GenerateSecret) Run(cli *CLI, _ string) error {
secret := mtglib.GenerateSecret(cli.GenerateSecret.HostName)
if cli.GenerateSecret.Hex {
fmt.Println(secret.Hex()) // nolint: forbidigo
} else {
fmt.Println(secret.Base64()) // nolint: forbidigo
}
return nil
}
-51
View File
@@ -1,51 +0,0 @@
package cli_test
import (
"strings"
"testing"
"github.com/9seconds/mtg/v2/internal/testlib"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/stretchr/testify/suite"
)
type GenerateSecretTestSuite struct {
CommonTestSuite
}
func (suite *GenerateSecretTestSuite) SetupTest() {
suite.CommonTestSuite.SetupTest()
suite.cli.GenerateSecret.HostName = "google.com"
}
func (suite *GenerateSecretTestSuite) TestDefault() {
output := testlib.CaptureStdout(func() {
suite.NoError(suite.cli.GenerateSecret.Run(suite.cli, "dev"))
})
suite.True(strings.HasPrefix(output, "7"))
secret, err := mtglib.ParseSecret(output)
suite.NoError(err)
suite.True(secret.Valid())
suite.Equal("google.com", secret.Host)
}
func (suite *GenerateSecretTestSuite) TestHex() {
suite.cli.GenerateSecret.Hex = true
output := testlib.CaptureStdout(func() {
suite.NoError(suite.cli.GenerateSecret.Run(suite.cli, "dev"))
})
suite.True(strings.HasPrefix(output, "ee"))
secret, err := mtglib.ParseSecret(output)
suite.NoError(err)
suite.True(secret.Valid())
suite.Equal("google.com", secret.Host)
}
func TestGenerateSecret(t *testing.T) {
t.Parallel()
suite.Run(t, &GenerateSecretTestSuite{})
}
-37
View File
@@ -1,37 +0,0 @@
package cli_test
import (
"net/http"
"github.com/9seconds/mtg/v2/internal/cli"
"github.com/9seconds/mtg/v2/internal/testlib"
"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type CommonTestSuite struct {
suite.Suite
cli *cli.CLI
networkMock *testlib.MtglibNetworkMock
httpClient *http.Client
}
func (suite *CommonTestSuite) SetupTest() {
suite.networkMock = &testlib.MtglibNetworkMock{}
suite.httpClient = &http.Client{}
suite.cli = &cli.CLI{}
httpmock.ActivateNonDefault(suite.httpClient)
suite.networkMock.
On("MakeHTTPClient", mock.Anything).
Maybe().
Return(suite.httpClient)
}
func (suite *CommonTestSuite) TearDownTest() {
suite.networkMock.AssertExpectations(suite.T())
httpmock.DeactivateAndReset()
}
-167
View File
@@ -1,167 +0,0 @@
package cli
import (
"fmt"
"net"
"os"
"github.com/9seconds/mtg/v2/antireplay"
"github.com/9seconds/mtg/v2/events"
"github.com/9seconds/mtg/v2/internal/utils"
"github.com/9seconds/mtg/v2/ipblocklist"
"github.com/9seconds/mtg/v2/logger"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/9seconds/mtg/v2/stats"
"github.com/rs/zerolog"
)
type Proxy struct {
base
}
func (c *Proxy) Run(cli *CLI, version string) error {
if err := c.ReadConfig(version); err != nil {
return fmt.Errorf("cannot init config: %w", err)
}
return c.Execute()
}
func (c *Proxy) Execute() error {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
zerolog.TimestampFieldName = "timestamp"
zerolog.LevelFieldName = "level"
if c.Config.Debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
} else {
zerolog.SetGlobalLevel(zerolog.WarnLevel)
}
ctx := utils.RootContext()
opts := mtglib.ProxyOpts{
Logger: logger.NewZeroLogger(zerolog.New(os.Stdout).With().Timestamp().Logger()),
Network: c.Network,
AntiReplayCache: antireplay.NewNoop(),
IPBlocklist: ipblocklist.NewNoop(),
EventStream: events.NewNoopStream(),
Secret: c.Config.Secret,
BufferSize: c.Config.TCPBuffer.Value(mtglib.DefaultBufferSize),
DomainFrontingPort: c.Config.DomainFrontingPort.Value(mtglib.DefaultDomainFrontingPort),
IdleTimeout: c.Config.Network.Timeout.Idle.Value(mtglib.DefaultIdleTimeout),
PreferIP: c.Config.PreferIP.Value(mtglib.DefaultPreferIP),
}
opts.Logger.BindStr("configuration", c.Config.String()).Debug("configuration")
c.setupAntiReplayCache(&opts)
if err := c.setupIPBlocklist(&opts); err != nil {
return fmt.Errorf("cannot setup ipblocklist: %w", err)
}
if err := c.setupEventStream(&opts); err != nil {
return fmt.Errorf("cannot setup event stream: %w", err)
}
proxy, err := mtglib.NewProxy(opts)
if err != nil {
return fmt.Errorf("cannot create a proxy: %w", err)
}
listener, err := net.Listen("tcp", c.Config.BindTo.String())
if err != nil {
return fmt.Errorf("cannot start proxy: %w", err)
}
go proxy.Serve(listener) // nolint: errcheck
<-ctx.Done()
listener.Close()
proxy.Shutdown()
return nil
}
func (c *Proxy) setupAntiReplayCache(opts *mtglib.ProxyOpts) {
if !c.Config.Defense.AntiReplay.Enabled {
return
}
opts.AntiReplayCache = antireplay.NewStableBloomFilter(
c.Config.Defense.AntiReplay.MaxSize.Value(antireplay.DefaultStableBloomFilterMaxSize),
c.Config.Defense.AntiReplay.ErrorRate.Value(antireplay.DefaultStableBloomFilterErrorRate),
)
}
func (c *Proxy) setupIPBlocklist(opts *mtglib.ProxyOpts) error {
if !c.Config.Defense.Blocklist.Enabled {
return nil
}
remoteURLs := []string{}
localFiles := []string{}
for _, v := range c.Config.Defense.Blocklist.URLs {
if v.IsRemote() {
remoteURLs = append(remoteURLs, v.String())
} else {
localFiles = append(localFiles, v.String())
}
}
firehol, err := ipblocklist.NewFirehol(opts.Logger.Named("ipblockist"),
c.Network,
c.Config.Defense.Blocklist.DownloadConcurrency,
remoteURLs,
localFiles)
if err != nil {
return err // nolint: wrapcheck
}
go firehol.Run(c.Config.Defense.Blocklist.UpdateEach.Value(ipblocklist.DefaultFireholUpdateEach))
opts.IPBlocklist = firehol
return nil
}
func (c *Proxy) setupEventStream(opts *mtglib.ProxyOpts) error {
factories := make([]events.ObserverFactory, 0, 2)
if c.Config.Stats.StatsD.Enabled {
statsdFactory, err := stats.NewStatsd(
c.Config.Stats.StatsD.Address.String(),
opts.Logger.Named("statsd"),
c.Config.Stats.StatsD.MetricPrefix.Value(stats.DefaultStatsdMetricPrefix),
c.Config.Stats.StatsD.TagFormat.Value(stats.DefaultStatsdTagFormat))
if err != nil {
return fmt.Errorf("cannot build statsd observer: %w", err)
}
factories = append(factories, statsdFactory.Make)
}
if c.Config.Stats.Prometheus.Enabled {
prometheus := stats.NewPrometheus(
c.Config.Stats.Prometheus.MetricPrefix.Value(stats.DefaultMetricPrefix),
c.Config.Stats.Prometheus.HTTPPath.Value("/"),
)
listener, err := net.Listen("tcp", c.Config.Stats.Prometheus.BindTo.String())
if err != nil {
return fmt.Errorf("cannot start a listener for prometheus: %w", err)
}
go prometheus.Serve(listener) // nolint: errcheck
factories = append(factories, prometheus.Make)
}
if len(factories) > 0 {
opts.EventStream = events.NewEventStream(factories)
}
return nil
}
-2
View File
@@ -1,2 +0,0 @@
secret = "7mqFMMq3P2Tvvt_rPx5qhmFnb29nbGUuY29t"
bind-to = "0.0.0.0:80"
-157
View File
@@ -1,157 +0,0 @@
package config
import (
"bytes"
"encoding/json"
"fmt"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/pelletier/go-toml"
)
type Config struct {
Debug bool `json:"debug"`
Secret mtglib.Secret `json:"secret"`
BindTo TypeHostPort `json:"bind-to"`
TCPBuffer TypeBytes `json:"tcp-buffer"`
PreferIP TypePreferIP `json:"prefer-ip"`
DomainFrontingPort TypePort `json:"domain-fronting-port"`
TolerateTimeSkewness TypeDuration `json:"tolerate-time-skewness"`
Concurrency uint `json:"concurrency"`
Defense struct {
AntiReplay struct {
Enabled bool `json:"enabled"`
MaxSize TypeBytes `json:"max-size"`
ErrorRate TypeErrorRate `json:"error-rate"`
} `json:"anti-replay"`
Blocklist struct {
Enabled bool `json:"enabled"`
DownloadConcurrency uint `json:"download-concurrency"`
URLs []TypeBlocklistURI `json:"urls"`
UpdateEach TypeDuration `json:"update-each"`
} `json:"blocklist"`
} `json:"defense"`
Network struct {
Timeout struct {
TCP TypeDuration `json:"tcp"`
HTTP TypeDuration `json:"http"`
Idle TypeDuration `json:"idle"`
} `json:"timeout"`
DOHIP TypeIP `json:"doh-ip"`
Proxies []TypeURL `json:"proxies"`
} `json:"network"`
Stats struct {
StatsD struct {
Enabled bool `json:"enabled"`
Address TypeHostPort `json:"address"`
MetricPrefix TypeMetricPrefix `json:"metric-prefix"`
TagFormat TypeStatsdTagFormat `json:"tag-format"`
} `json:"statsd"`
Prometheus struct {
Enabled bool `json:"enabled"`
BindTo TypeHostPort `json:"bind-to"`
HTTPPath TypeHTTPPath `json:"http-path"`
MetricPrefix TypeMetricPrefix `json:"metric-prefix"`
} `json:"prometheus"`
} `json:"stats"`
}
func (c *Config) Validate() error {
if !c.Secret.Valid() {
return fmt.Errorf("invalid secret %s", c.Secret.String())
}
if len(c.BindTo.HostValue(nil)) == 0 || c.BindTo.PortValue(0) == 0 {
return fmt.Errorf("incorrect bind-to parameter %s", c.BindTo.String())
}
return nil
}
func (c *Config) String() string {
buf := &bytes.Buffer{}
encoder := json.NewEncoder(buf)
encoder.SetEscapeHTML(false)
if err := encoder.Encode(c); err != nil {
panic(err)
}
return buf.String()
}
type configRaw struct {
Debug bool `toml:"debug" json:"debug,omitempty"`
Secret string `toml:"secret" json:"secret"`
BindTo string `toml:"bind-to" json:"bind-to"`
TCPBuffer string `toml:"tcp-buffer" json:"tcp-buffer,omitempty"`
PreferIP string `toml:"prefer-ip" json:"prefer-ip,omitempty"`
DomainFrontingPort uint `toml:"domain-fronting-port" json:"domain-fronting-port,omitempty"`
TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerate-time-skewness,omitempty"`
Concurrency uint `toml:"concurrency" json:"concurrency,omitempty"`
Defense struct {
AntiReplay struct {
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
MaxSize string `toml:"max-size" json:"max-size,omitempty"`
ErrorRate float64 `toml:"error-rate" json:"error-rate,omitempty"`
} `toml:"anti-replay" json:"anti-replay,omitempty"`
Blocklist struct {
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
DownloadConcurrency uint `toml:"download-concurrency" json:"download-concurrency,omitempty"`
URLs []string `toml:"urls" json:"urls,omitempty"`
UpdateEach string `toml:"update-each" json:"update-each,omitempty"`
} `toml:"blocklist" json:"blocklist,omitempty"`
} `toml:"defense" json:"defense,omitempty"`
Network struct {
Timeout struct {
TCP string `toml:"tcp" json:"tcp,omitempty"`
HTTP string `toml:"http" json:"http,omitempty"`
Idle string `toml:"idle" json:"idle,omitempty"`
} `toml:"timeout" json:"timeout,omitempty"`
DOHIP string `toml:"doh-ip" json:"doh-ip,omitempty"`
Proxies []string `toml:"proxies" json:"proxies,omitempty"`
} `toml:"network" json:"network,omitempty"`
Stats struct {
StatsD struct {
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
Address string `toml:"address" json:"address,omitempty"`
MetricPrefix string `toml:"metric-prefix" json:"metric-prefix,omitempty"`
TagFormat string `toml:"tag-format" json:"tag-format,omitempty"`
} `toml:"statsd" json:"statsd,omitempty"`
Prometheus struct {
Enabled bool `toml:"enabled" json:"enabled,omitempty"`
BindTo string `toml:"bind-to" json:"bind-to,omitempty"`
HTTPPath string `toml:"http-path" json:"http-path,omitempty"`
MetricPrefix string `toml:"metric-prefix" json:"metric-prefix,omitempty"`
} `toml:"prometheus" json:"prometheus,omitempty"`
} `toml:"stats" json:"stats,omitempty"`
}
func Parse(rawData []byte) (*Config, error) {
rawConf := &configRaw{}
jsonBuf := &bytes.Buffer{}
conf := &Config{}
jsonEncoder := json.NewEncoder(jsonBuf)
jsonEncoder.SetEscapeHTML(false)
jsonEncoder.SetIndent("", "")
if err := toml.Unmarshal(rawData, rawConf); err != nil {
return nil, fmt.Errorf("cannot parse toml config: %w", err)
}
if err := jsonEncoder.Encode(rawConf); err != nil {
panic(err)
}
if err := json.NewDecoder(jsonBuf).Decode(conf); err != nil {
return nil, fmt.Errorf("cannot parse a config: %w", err)
}
if err := conf.Validate(); err != nil {
return nil, fmt.Errorf("cannot validate config: %w", err)
}
return conf, nil
}
-54
View File
@@ -1,54 +0,0 @@
package config_test
import (
"os"
"path/filepath"
"testing"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/stretchr/testify/suite"
)
type ConfigTestSuite struct {
suite.Suite
}
func (suite *ConfigTestSuite) ReadConfig(filename string) []byte {
data, err := os.ReadFile(filepath.Join("testdata", filename))
suite.NoError(err)
return data
}
func (suite *ConfigTestSuite) TestParseEmpty() {
_, err := config.Parse([]byte{})
suite.Error(err)
}
func (suite *ConfigTestSuite) TestParseBrokenToml() {
_, err := config.Parse(suite.ReadConfig("broken.toml"))
suite.Error(err)
}
func (suite *ConfigTestSuite) TestParseOnlySecret() {
_, err := config.Parse(suite.ReadConfig("only_secret.toml"))
suite.Error(err)
}
func (suite *ConfigTestSuite) TestParseMinimalConfig() {
conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
suite.NoError(err)
suite.Equal("7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t", conf.Secret.Base64())
suite.Equal("0.0.0.0:3128", conf.BindTo.String())
}
func (suite *ConfigTestSuite) TestString() {
conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
suite.NoError(err)
suite.NotEmpty(conf.String())
}
func TestConfig(t *testing.T) {
t.Parallel()
suite.Run(t, &ConfigTestSuite{})
}
-1
View File
@@ -1 +0,0 @@
s = sdfsdfds
-2
View File
@@ -1,2 +0,0 @@
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
bind-to = "0.0.0.0:3128"
-1
View File
@@ -1 +0,0 @@
secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
-68
View File
@@ -1,68 +0,0 @@
package config
import (
"fmt"
"net/url"
"os"
"path/filepath"
)
type TypeBlocklistURI struct {
value string
}
func (c *TypeBlocklistURI) UnmarshalText(data []byte) error {
if len(data) == 0 {
return nil
}
text := string(data)
if filepath.IsAbs(text) {
if _, err := os.Stat(text); os.IsNotExist(err) {
return fmt.Errorf("filepath %s does not exist", text)
}
c.value = text
return nil
}
parsedURL, err := url.Parse(text)
if err != nil {
return fmt.Errorf("incorrect url: %w", err)
}
switch parsedURL.Scheme {
case "http", "https": // nolint: goconst
default:
return fmt.Errorf("unknown schema %s", parsedURL.Scheme)
}
if parsedURL.Host == "" {
return fmt.Errorf("incorrect url %s", text)
}
c.value = parsedURL.String()
return nil
}
func (c TypeBlocklistURI) MarshalText() ([]byte, error) {
return []byte(c.value), nil
}
func (c TypeBlocklistURI) String() string {
return c.value
}
func (c TypeBlocklistURI) IsRemote() bool {
return !filepath.IsAbs(c.value)
}
func (c TypeBlocklistURI) Value(defaultValue string) string {
if c.value == "" {
return defaultValue
}
return c.value
}
-177
View File
@@ -1,177 +0,0 @@
package config_test
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"os"
"path/filepath"
"strconv"
"testing"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type typeBlocklistURITestStruct struct {
Value config.TypeBlocklistURI `json:"value"`
}
type TypeBlocklistURITestSuite struct {
suite.Suite
}
func (suite *TypeBlocklistURITestSuite) TestUnmarshalNil() {
typ := &config.TypeBlocklistURI{}
suite.NoError(typ.UnmarshalText(nil))
suite.Empty(typ.String())
}
func (suite *TypeBlocklistURITestSuite) TestUnknownSchema() {
typ := &config.TypeBlocklistURI{}
suite.Error(typ.UnmarshalText([]byte("gopher://lalala")))
}
func (suite *TypeBlocklistURITestSuite) TestEmptyHost() {
typ := &config.TypeBlocklistURI{}
suite.Error(typ.UnmarshalText([]byte("https:///path")))
}
func (suite *TypeBlocklistURITestSuite) TestIncorrectURL() {
typ := &config.TypeBlocklistURI{}
suite.Error(typ.UnmarshalText([]byte("h:/--")))
}
func (suite *TypeBlocklistURITestSuite) TestUnmarshalFail() {
rnd := make([]byte, 48)
rand.Read(rnd) // nolint: errcheck
unknownPath := base64.StdEncoding.EncodeToString(rnd)
testData := []string{
"1",
unknownPath,
"/" + unknownPath,
"http:/",
"gopher://lalalal",
}
for _, v := range testData {
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
assert.Error(t, json.Unmarshal(data, &typeBlocklistURITestStruct{}))
})
}
}
func (suite *TypeBlocklistURITestSuite) TestUnmarshalOk() {
dir, _ := os.Getwd()
dir, _ = filepath.Abs(dir)
testData := []string{
"http://lalala",
filepath.Join(dir, "config.go"),
"https://lalala",
}
for _, v := range testData {
value := v
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
testStruct := &typeBlocklistURITestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.EqualValues(t, value, testStruct.Value.Value(""))
})
}
}
func (suite *TypeBlocklistURITestSuite) TestMarshalOk() {
dir, _ := os.Getwd()
dir, _ = filepath.Abs(dir)
testData := []string{
"http://lalalal",
filepath.Join(dir, "config.go"),
}
for _, v := range testData {
name := v
data, err := json.Marshal(map[string]string{
"value": name,
})
suite.NoError(err)
suite.T().Run(name, func(t *testing.T) {
testStruct := &typeBlocklistURITestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.Equal(t, name, testStruct.Value.String())
marshalled, err := testStruct.Value.MarshalText()
assert.NoError(t, err)
assert.Equal(t, name, string(marshalled))
})
}
}
func (suite *TypeBlocklistURITestSuite) TestValue() {
testStruct := &typeBlocklistURITestStruct{}
suite.Equal("http://lalala", testStruct.Value.Value("http://lalala"))
data, err := json.Marshal(map[string]string{
"value": "http://blablabla",
})
suite.NoError(err)
suite.NoError(json.Unmarshal(data, testStruct))
suite.Equal("http://blablabla", testStruct.Value.Value(""))
}
func (suite *TypeBlocklistURITestSuite) TestIsRemote() {
dir, _ := os.Getwd()
dir, _ = filepath.Abs(dir)
testData := map[bool]string{
true: "http://lalalal",
false: filepath.Join(dir, "config.go"),
}
for k, v := range testData {
ok := k
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(strconv.FormatBool(ok), func(t *testing.T) {
testStruct := &typeBlocklistURITestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
if ok {
assert.True(t, testStruct.Value.IsRemote())
} else {
assert.False(t, testStruct.Value.IsRemote())
}
})
}
}
func TestTypeBlocklistURI(t *testing.T) {
t.Parallel()
suite.Run(t, &TypeBlocklistURITestSuite{})
}
-54
View File
@@ -1,54 +0,0 @@
package config
import (
"fmt"
"strings"
"github.com/alecthomas/units"
)
type TypeBytes struct {
value units.Base2Bytes
}
func (c *TypeBytes) UnmarshalText(data []byte) error {
if len(data) == 0 {
return nil
}
normalizedData := strings.ToUpper(string(data))
normalizedData = strings.ReplaceAll(normalizedData, "IB", "iB")
value, err := units.ParseBase2Bytes(normalizedData)
if err != nil {
return fmt.Errorf("incorrect bytes value: %w", err)
}
if value < 0 {
return fmt.Errorf("%d should be positive number", value)
}
c.value = value
return nil
}
func (c TypeBytes) MarshalText() ([]byte, error) {
return []byte(c.String()), nil
}
func (c TypeBytes) String() string {
if c.value == 0 {
return ""
}
return strings.ToLower(c.value.String())
}
func (c TypeBytes) Value(defaultValue uint) uint {
if c.value == 0 {
return defaultValue
}
return uint(c.value)
}
-120
View File
@@ -1,120 +0,0 @@
package config_test
import (
"encoding/json"
"testing"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type typeBytesTestStruct struct {
Value config.TypeBytes `json:"value"`
}
type TypeBytesTestSuite struct {
suite.Suite
}
func (suite *TypeBytesTestSuite) TestUnmarshalNil() {
typ := &config.TypeBytes{}
suite.NoError(typ.UnmarshalText(nil))
suite.Empty(typ.String())
}
func (suite *TypeBytesTestSuite) TestUnmarshalFail() {
testData := []string{
"1m",
"1",
"-1kb",
"-1kib",
"-1QB",
}
for _, v := range testData {
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
assert.Error(t, json.Unmarshal(data, &typeBytesTestStruct{}))
})
}
}
func (suite *TypeBytesTestSuite) TestUnmarshalOk() {
testData := map[string]uint{
"1b": 1,
"1kb": 1024,
"1kib": 1024,
"2mb": 2 * 1024 * 1024,
"2mib": 2 * 1024 * 1024,
}
for k, v := range testData {
value := v
data, err := json.Marshal(map[string]string{
"value": k,
})
suite.NoError(err)
suite.T().Run(k, func(t *testing.T) {
testStruct := &typeBytesTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.EqualValues(t, value, testStruct.Value.Value(0))
})
}
}
func (suite *TypeBytesTestSuite) TestMarshalOk() {
testData := []string{
"1b",
"1kib",
"2mib",
}
for _, v := range testData {
name := v
data, err := json.Marshal(map[string]string{
"value": name,
})
suite.NoError(err)
suite.T().Run(name, func(t *testing.T) {
testStruct := &typeBytesTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.Equal(t, name, testStruct.Value.String())
marshalled, err := testStruct.Value.MarshalText()
assert.NoError(t, err)
assert.Equal(t, name, string(marshalled))
})
}
}
func (suite *TypeBytesTestSuite) TestValue() {
testStruct := &typeBytesTestStruct{}
suite.EqualValues(0, testStruct.Value.Value(0))
suite.EqualValues(1, testStruct.Value.Value(1))
data, err := json.Marshal(map[string]string{
"value": "1kb",
})
suite.NoError(err)
suite.NoError(json.Unmarshal(data, testStruct))
suite.EqualValues(1024, testStruct.Value.Value(0))
suite.EqualValues(1024, testStruct.Value.Value(1))
}
func TestTypeBytes(t *testing.T) {
t.Parallel()
suite.Run(t, &TypeBytesTestSuite{})
}
-46
View File
@@ -1,46 +0,0 @@
package config
import (
"fmt"
"strings"
"time"
)
type TypeDuration struct {
value time.Duration
}
func (c *TypeDuration) UnmarshalText(data []byte) error {
if len(data) == 0 {
return nil
}
dur, err := time.ParseDuration(strings.ToLower(string(data)))
if err != nil {
return fmt.Errorf("incorrect duration: %w", err)
}
if dur < 0 {
return fmt.Errorf("%s should be positive duration", dur)
}
c.value = dur
return nil
}
func (c TypeDuration) MarshalText() ([]byte, error) {
return []byte(c.value.String()), nil
}
func (c TypeDuration) String() string {
return c.value.String()
}
func (c TypeDuration) Value(defaultValue time.Duration) time.Duration {
if c.value == 0 {
return defaultValue
}
return c.value
}
-118
View File
@@ -1,118 +0,0 @@
package config_test
import (
"encoding/json"
"testing"
"time"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type typeDurationTestStruct struct {
Value config.TypeDuration `json:"value"`
}
type TypeDurationTestSuite struct {
suite.Suite
}
func (suite *TypeDurationTestSuite) TestUnmarshalNil() {
typ := &config.TypeDuration{}
suite.NoError(typ.UnmarshalText(nil))
suite.EqualValues(0, typ.Value(0))
}
func (suite *TypeDurationTestSuite) TestUnmarshalFail() {
testData := []string{
"1t",
"1",
"-1s",
"-1h",
}
for _, v := range testData {
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
assert.Error(t, json.Unmarshal(data, &typeDurationTestStruct{}))
})
}
}
func (suite *TypeDurationTestSuite) TestUnmarshalOk() {
testData := map[string]time.Duration{
"1s": time.Second,
"1m": time.Minute,
"2h1s": 2*time.Hour + time.Second,
}
for k, v := range testData {
value := v
data, err := json.Marshal(map[string]string{
"value": k,
})
suite.NoError(err)
suite.T().Run(k, func(t *testing.T) {
testStruct := &typeDurationTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.Equal(t, value, testStruct.Value.Value(0))
})
}
}
func (suite *TypeDurationTestSuite) TestMarshalOk() {
testData := []string{
"1s",
"1m0s",
"2h0m1s",
}
for _, v := range testData {
name := v
data, err := json.Marshal(map[string]string{
"value": name,
})
suite.NoError(err)
suite.T().Run(name, func(t *testing.T) {
testStruct := &typeDurationTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.Equal(t, name, testStruct.Value.String())
marshalled, err := testStruct.Value.MarshalText()
assert.NoError(t, err)
assert.Equal(t, name, string(marshalled))
})
}
}
func (suite *TypeDurationTestSuite) TestValue() {
testStruct := &typeDurationTestStruct{}
suite.EqualValues(0, testStruct.Value.Value(0))
suite.Equal(time.Second, testStruct.Value.Value(time.Second))
data, err := json.Marshal(map[string]string{
"value": "1s",
})
suite.NoError(err)
suite.NoError(json.Unmarshal(data, testStruct))
suite.Equal(time.Second, testStruct.Value.Value(0))
suite.Equal(time.Second, testStruct.Value.Value(time.Minute))
}
func TestTypeDuration(t *testing.T) {
t.Parallel()
suite.Run(t, &TypeDurationTestSuite{})
}
-43
View File
@@ -1,43 +0,0 @@
package config
import (
"fmt"
"strconv"
)
const typeErrorRateIgnoreLess = 1e-8
type TypeErrorRate struct {
value float64
}
func (c *TypeErrorRate) UnmarshalJSON(data []byte) error {
value, err := strconv.ParseFloat(string(data), 64)
if err != nil {
return fmt.Errorf("incorrect float value: %w", err)
}
if value <= 0 || value >= 100 {
return fmt.Errorf("%f should be 0 < x < 100", value)
}
c.value = value
return nil
}
func (c *TypeErrorRate) MarshalText() ([]byte, error) {
return []byte(c.String()), nil
}
func (c TypeErrorRate) String() string {
return strconv.FormatFloat(c.value, 'f', -1, 64)
}
func (c TypeErrorRate) Value(defaultValue float64) float64 {
if c.value < typeErrorRateIgnoreLess {
return defaultValue
}
return c.value
}
-125
View File
@@ -1,125 +0,0 @@
package config_test
import (
"encoding/json"
"strconv"
"testing"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type typeErrorRateTestStruct struct {
Value config.TypeErrorRate `json:"value"`
}
type TypeErrorRateTestSuite struct {
suite.Suite
}
func (suite *TypeErrorRateTestSuite) TestUnmarshalFail() {
testData := []float64{
1000,
-100,
-0.0001,
}
for _, v := range testData {
data, err := json.Marshal(map[string]float64{
"value": v,
})
suite.NoError(err)
suite.T().Run(strconv.FormatFloat(v, 'f', -1, 64), func(t *testing.T) {
assert.Error(t, json.Unmarshal(data, &typeErrorRateTestStruct{}))
})
}
data, err := json.Marshal(map[string]string{
"value": "hello",
})
suite.NoError(err)
suite.Error(json.Unmarshal(data, &typeErrorRateTestStruct{}))
}
func (suite *TypeErrorRateTestSuite) TestUnmarshalOk() {
testData := []float64{
1,
55.5,
0.0001,
1e-6,
}
for _, v := range testData {
value := v
data, err := json.Marshal(map[string]float64{
"value": v,
})
suite.NoError(err)
suite.T().Run(strconv.FormatFloat(v, 'f', -1, 64), func(t *testing.T) {
testStruct := &typeErrorRateTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.InEpsilon(t, value, testStruct.Value.Value(0), 1e-10)
})
}
}
func (suite *TypeErrorRateTestSuite) TestMarshalOk() {
testData := []float64{
1,
55.5,
0.0001,
1e-6,
}
for _, v := range testData {
value := v
data, err := json.Marshal(map[string]float64{
"value": v,
})
suite.NoError(err)
suite.T().Run(strconv.FormatFloat(v, 'f', -1, 64), func(t *testing.T) {
testStruct := &typeErrorRateTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
parsed, err := strconv.ParseFloat(testStruct.Value.String(), 64)
assert.NoError(t, err)
assert.InEpsilon(t, value, parsed, 1e-10)
marshalled, err := testStruct.Value.MarshalText()
assert.NoError(t, err)
parsed, err = strconv.ParseFloat(string(marshalled), 64)
assert.NoError(t, err)
assert.InEpsilon(t, value, parsed, 1e-10)
})
}
}
func (suite *TypeErrorRateTestSuite) TestValue() {
testStruct := &typeErrorRateTestStruct{}
suite.InEpsilon(1, testStruct.Value.Value(1), 1e-10)
suite.InEpsilon(2, testStruct.Value.Value(2), 1e-10)
data, err := json.Marshal(map[string]float64{
"value": 1,
})
suite.NoError(err)
suite.NoError(json.Unmarshal(data, testStruct))
suite.InEpsilon(1, testStruct.Value.Value(2), 1e-10)
suite.InEpsilon(1, testStruct.Value.Value(3), 1e-10)
}
func TestTypeErrorRate(t *testing.T) {
t.Parallel()
suite.Run(t, &TypeErrorRateTestSuite{})
}
-67
View File
@@ -1,67 +0,0 @@
package config
import (
"fmt"
"net"
"strconv"
)
type TypeHostPort struct {
host TypeIP
port TypePort
}
func (c *TypeHostPort) UnmarshalText(data []byte) error {
if len(data) == 0 {
return nil
}
text := string(data)
host, port, err := net.SplitHostPort(text)
if err != nil {
return fmt.Errorf("incorrect host:port syntax: %w", err)
}
if port == "" {
return fmt.Errorf("port in %s host:port pair cannot be empty", text)
}
if err := c.port.UnmarshalJSON([]byte(port)); err != nil {
return fmt.Errorf("incorrect port in host:port: %w", err)
}
if err := c.host.UnmarshalText([]byte(host)); err != nil {
return fmt.Errorf("incorrect host: %w", err)
}
return nil
}
func (c TypeHostPort) MarshalText() ([]byte, error) {
return []byte(c.String()), nil
}
func (c TypeHostPort) String() string {
return c.Value(net.IP{}, 0)
}
func (c TypeHostPort) HostValue(defaultValue net.IP) net.IP {
return c.host.Value(defaultValue)
}
func (c TypeHostPort) PortValue(defaultValue uint) uint {
return c.port.Value(defaultValue)
}
func (c TypeHostPort) Value(defaultHostValue net.IP, defaultPortValue uint) string {
host := c.HostValue(defaultHostValue)
port := c.PortValue(defaultPortValue)
hostStr := ""
if len(host) > 0 {
hostStr = host.String()
}
return net.JoinHostPort(hostStr, strconv.Itoa(int(port)))
}
-115
View File
@@ -1,115 +0,0 @@
package config_test
import (
"encoding/json"
"net"
"testing"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type typeHostPortTestStruct struct {
Value config.TypeHostPort `json:"value"`
}
type TypeHostPortTestSuite struct {
suite.Suite
}
func (suite *TypeHostPortTestSuite) TestUnmarshalFail() {
testData := []string{
"10.0.0.10:aaa",
"10.0.0.10:",
":",
"xxx",
"xxx:80",
}
for _, v := range testData {
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
assert.Error(t, json.Unmarshal(data, &typeHostPortTestStruct{}))
})
}
}
func (suite *TypeHostPortTestSuite) TestUnmarshalOk() {
testData := []string{
"10.0.0.10:80",
"0.0.0.0:80",
":8000",
}
for _, v := range testData {
value := v
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
testStruct := &typeHostPortTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.EqualValues(t, value, testStruct.Value.Value(nil, 0))
})
}
}
func (suite *TypeHostPortTestSuite) TestMarshalOk() {
testData := []string{
"10.0.0.10:80",
"0.0.0.0:80",
":8000",
}
for _, v := range testData {
value := v
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
testStruct := &typeHostPortTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.Equal(t, value, testStruct.Value.String())
marshalled, err := testStruct.Value.MarshalText()
assert.NoError(t, err)
assert.Equal(t, value, string(marshalled))
})
}
}
func (suite *TypeHostPortTestSuite) TestValue() {
testStruct := &typeHostPortTestStruct{}
suite.EqualValues("127.0.0.1:80",
testStruct.Value.Value(net.ParseIP("127.0.0.1"), 80))
suite.EqualValues("127.1.0.1:80",
testStruct.Value.Value(net.ParseIP("127.1.0.1"), 80))
data, err := json.Marshal(map[string]string{
"value": "127.0.0.1:80",
})
suite.NoError(err)
suite.NoError(json.Unmarshal(data, testStruct))
suite.EqualValues("127.0.0.1:80", testStruct.Value.Value(nil, 0))
suite.EqualValues("127.0.0.1:80", testStruct.Value.Value(net.ParseIP("10.0.0.10"), 3000))
}
func TestTypeHostPort(t *testing.T) {
t.Parallel()
suite.Run(t, &TypeHostPortTestSuite{})
}
-35
View File
@@ -1,35 +0,0 @@
package config
import "strings"
type TypeHTTPPath struct {
value string
}
func (c *TypeHTTPPath) UnmarshalText(data []byte) error {
if len(data) > 0 {
c.value = "/" + strings.Trim(string(data), "/")
}
return nil
}
func (c TypeHTTPPath) MarshalText() ([]byte, error) {
return []byte(c.String()), nil
}
func (c TypeHTTPPath) String() string {
if c.value == "" {
return "/"
}
return c.value
}
func (c TypeHTTPPath) Value(defaultValue string) string {
if c.value == "" {
return defaultValue
}
return c.value
}
-91
View File
@@ -1,91 +0,0 @@
package config_test
import (
"encoding/json"
"testing"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type typeHTTPPathTestStruct struct {
Value config.TypeHTTPPath `json:"value"`
}
type TypeHTTPPathTestSuite struct {
suite.Suite
}
func (suite *TypeHTTPPathTestSuite) TestUnmarshal() {
testData := []string{
"/hello",
"hello",
"hello/",
"/hello/",
}
for _, v := range testData {
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
testStruct := &typeHTTPPathTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.Equal(t, "/hello", testStruct.Value.Value(""))
})
}
}
func (suite *TypeHTTPPathTestSuite) TestMarshalOk() {
testData := map[string]string{
"": "/",
"/hello": "/hello",
"/hello/": "/hello",
"hello/": "/hello",
"hello": "/hello",
}
for k, v := range testData {
toPass := k
compareWith := v
data, err := json.Marshal(map[string]string{
"value": toPass,
})
suite.NoError(err)
suite.T().Run(toPass, func(t *testing.T) {
testStruct := &typeHTTPPathTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.Equal(t, compareWith, testStruct.Value.String())
marshalled, err := testStruct.Value.MarshalText()
assert.NoError(t, err)
assert.Equal(t, compareWith, string(marshalled))
})
}
}
func (suite *TypeHTTPPathTestSuite) TestValue() {
testStruct := &typeHTTPPathTestStruct{}
suite.Equal("/hello", testStruct.Value.Value("/hello"))
data, err := json.Marshal(map[string]string{
"value": "/map",
})
suite.NoError(err)
suite.NoError(json.Unmarshal(data, testStruct))
suite.Equal("/map", testStruct.Value.Value("/hello"))
}
func TestTypeHTTPPath(t *testing.T) {
t.Parallel()
suite.Run(t, &TypeHTTPPathTestSuite{})
}
-45
View File
@@ -1,45 +0,0 @@
package config
import (
"fmt"
"net"
)
type TypeIP struct {
value net.IP
}
func (c *TypeIP) UnmarshalText(data []byte) error {
if len(data) == 0 {
return nil
}
ip := net.ParseIP(string(data))
if ip == nil {
return fmt.Errorf("incorrect ip address: %s", string(data))
}
c.value = ip
return nil
}
func (c *TypeIP) MarshalText() ([]byte, error) {
return []byte(c.String()), nil
}
func (c TypeIP) String() string {
if len(c.value) > 0 {
return c.value.String()
}
return ""
}
func (c TypeIP) Value(defaultValue net.IP) net.IP {
if c.value == nil {
return defaultValue
}
return c.value
}
-115
View File
@@ -1,115 +0,0 @@
package config_test
import (
"encoding/json"
"net"
"testing"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type typeIPTestStruct struct {
Value config.TypeIP `json:"value"`
}
type TypeIPTestSuite struct {
suite.Suite
}
func (suite *TypeIPTestSuite) TestUnmarshalFail() {
testData := []string{
"0.0.10",
"10.0.0.10:",
"xxx:80",
"2001:0db8:85a3:0000:0000:8a2e:4",
}
for _, v := range testData {
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
assert.Error(t, json.Unmarshal(data, &typeIPTestStruct{}))
})
}
}
func (suite *TypeIPTestSuite) TestUnmarshalOk() {
testData := []string{
"0.0.0.0",
"10.0.0.10",
"2001:0db8:85a3:0000:0000:8a2e:0370:7334",
}
for _, v := range testData {
value := v
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
testStruct := &typeIPTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.Equal(t,
net.ParseIP(value).String(),
testStruct.Value.Value(nil).String())
})
}
}
func (suite *TypeIPTestSuite) TestMarshalOk() {
testData := []string{
"0.0.0.0",
"10.0.0.10",
"2001:0db8:85a3:0000:0000:8a2e:0370:7334",
}
for _, v := range testData {
value := net.ParseIP(v).String()
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
testStruct := &typeIPTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.Equal(t, value, testStruct.Value.String())
marshalled, err := testStruct.Value.MarshalText()
assert.NoError(t, err)
assert.Equal(t, value, string(marshalled))
})
}
}
func (suite *TypeIPTestSuite) TestValue() {
testStruct := &typeIPTestStruct{}
suite.Empty(testStruct.Value.String())
suite.Nil(testStruct.Value.Value(nil))
suite.Equal("127.1.0.1", testStruct.Value.Value(net.ParseIP("127.1.0.1")).String())
data, err := json.Marshal(map[string]string{
"value": "127.0.0.1",
})
suite.NoError(err)
suite.NoError(json.Unmarshal(data, testStruct))
suite.Equal("127.0.0.1", testStruct.Value.Value(nil).String())
suite.Equal("127.0.0.1", testStruct.Value.Value(net.ParseIP("10.0.0.10")).String())
}
func TestTypeIP(t *testing.T) {
t.Parallel()
suite.Run(t, &TypeIPTestSuite{})
}
-41
View File
@@ -1,41 +0,0 @@
package config
import (
"fmt"
"regexp"
)
type TypeMetricPrefix struct {
value string
}
func (c *TypeMetricPrefix) UnmarshalText(data []byte) error {
if len(data) == 0 {
return nil
}
prefix := string(data)
if ok, err := regexp.MatchString("^[a-z0-9]+$", prefix); !ok || err != nil {
return fmt.Errorf("incorrect metric prefix: %s", prefix)
}
c.value = prefix
return nil
}
func (c TypeMetricPrefix) MarshalText() ([]byte, error) {
return []byte(c.String()), nil
}
func (c TypeMetricPrefix) String() string {
return c.value
}
func (c TypeMetricPrefix) Value(defaultValue string) string {
if c.value == "" {
return defaultValue
}
return c.value
}
-115
View File
@@ -1,115 +0,0 @@
package config_test
import (
"encoding/json"
"testing"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type typeMetricPrefixTestStruct struct {
Value config.TypeMetricPrefix `json:"value"`
}
type TypeMetricPrefixTestSuite struct {
suite.Suite
}
func (suite *TypeMetricPrefixTestSuite) TestUnmarshalNil() {
typ := &config.TypeMetricPrefix{}
suite.NoError(typ.UnmarshalText(nil))
suite.Empty(typ.String())
}
func (suite *TypeMetricPrefixTestSuite) TestUnmarshalFail() {
testData := []string{
"aaa.aaa",
"aaa-bbb",
"aaa:ccc",
"metric prefix",
}
for _, v := range testData {
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
assert.Error(t, json.Unmarshal(data, &typeMetricPrefixTestStruct{}))
})
}
}
func (suite *TypeMetricPrefixTestSuite) TestUnmarshalOk() {
testData := []string{
"mtg",
"mtg111",
}
for _, v := range testData {
value := v
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
testStruct := &typeMetricPrefixTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.Equal(t, value, testStruct.Value.Value(""))
})
}
}
func (suite *TypeMetricPrefixTestSuite) TestMarshalOk() {
testData := []string{
"mtg",
"mtg111",
}
for _, v := range testData {
value := v
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
testStruct := &typeMetricPrefixTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.Equal(t, value, testStruct.Value.String())
marshalled, err := testStruct.Value.MarshalText()
assert.NoError(t, err)
assert.Equal(t, value, string(marshalled))
})
}
}
func (suite *TypeMetricPrefixTestSuite) TestValue() {
testStruct := &typeMetricPrefixTestStruct{}
suite.Equal("mtg", testStruct.Value.Value("mtg"))
suite.Equal("vvv", testStruct.Value.Value("vvv"))
data, err := json.Marshal(map[string]string{
"value": "aaa",
})
suite.NoError(err)
suite.NoError(json.Unmarshal(data, testStruct))
suite.Equal("aaa", testStruct.Value.Value("mtg"))
suite.Equal("aaa", testStruct.Value.Value("vvv"))
}
func TestTypeMetricPrefix(t *testing.T) {
t.Parallel()
suite.Run(t, &TypeMetricPrefixTestSuite{})
}
-45
View File
@@ -1,45 +0,0 @@
package config
import (
"fmt"
"strconv"
)
type TypePort struct {
value uint
}
func (c *TypePort) UnmarshalJSON(data []byte) error {
if len(data) == 0 {
return nil
}
intValue, err := strconv.ParseUint(string(data), 10, 64)
if err != nil {
return fmt.Errorf("port number is not a number: %w", err)
}
if intValue == 0 || intValue >= 65536 {
return fmt.Errorf("port number should be 0 < portNo < 65536: %d", intValue)
}
c.value = uint(intValue)
return nil
}
func (c *TypePort) MarshalJSON() ([]byte, error) {
return []byte(c.String()), nil
}
func (c TypePort) String() string {
return strconv.Itoa(int(c.value))
}
func (c TypePort) Value(defaultValue uint) uint {
if c.value == 0 {
return defaultValue
}
return c.value
}
-117
View File
@@ -1,117 +0,0 @@
package config_test
import (
"encoding/json"
"strconv"
"testing"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type typePortTestStruct struct {
Value config.TypePort `json:"value"`
}
type TypePortTestSuite struct {
suite.Suite
}
func (suite *TypePortTestSuite) TestUnmarshalNil() {
typ := &config.TypePort{}
suite.NoError(typ.UnmarshalJSON(nil))
suite.Equal("0", typ.String())
}
func (suite *TypePortTestSuite) TestUnmarshalFail() {
testData := []int{
-1,
1_000_000,
}
for _, v := range testData {
data, err := json.Marshal(map[string]int{
"value": v,
})
suite.NoError(err)
suite.T().Run(strconv.Itoa(v), func(t *testing.T) {
assert.Error(t, json.Unmarshal(data, &typePortTestStruct{}))
})
}
}
func (suite *TypePortTestSuite) TestUnmarshalOk() {
testData := []int{
1,
1_000,
65535,
}
for _, v := range testData {
value := v
data, err := json.Marshal(map[string]int{
"value": v,
})
suite.NoError(err)
suite.T().Run(strconv.Itoa(v), func(t *testing.T) {
testStruct := &typePortTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.EqualValues(t, value, testStruct.Value.Value(0))
})
}
}
func (suite *TypePortTestSuite) TestMarshalOk() {
testData := map[string]int{
"1": 1,
"1000": 1000,
"65535": 65535,
}
for k, v := range testData {
name := k
value := v
data, err := json.Marshal(map[string]int{
"value": value,
})
suite.NoError(err)
suite.T().Run(name, func(t *testing.T) {
testStruct := &typePortTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.Equal(t, name, testStruct.Value.String())
marshalled, err := testStruct.Value.MarshalJSON()
assert.NoError(t, err)
assert.Equal(t, name, string(marshalled))
})
}
}
func (suite *TypePortTestSuite) TestValue() {
testStruct := &typePortTestStruct{}
suite.EqualValues(0, testStruct.Value.Value(0))
suite.EqualValues(1, testStruct.Value.Value(1))
data, err := json.Marshal(map[string]int{
"value": 5,
})
suite.NoError(err)
suite.NoError(json.Unmarshal(data, testStruct))
suite.EqualValues(5, testStruct.Value.Value(0))
suite.EqualValues(5, testStruct.Value.Value(1))
}
func TestTypePort(t *testing.T) {
t.Parallel()
suite.Run(t, &TypePortTestSuite{})
}
-50
View File
@@ -1,50 +0,0 @@
package config
import (
"fmt"
"strings"
)
const (
TypePreferIPPreferIPv4 = "prefer-ipv4"
TypePreferIPPreferIPv6 = "prefer-ipv6"
TypePreferOnlyIPv4 = "only-ipv4"
TypePreferOnlyIPv6 = "only-ipv6"
)
type TypePreferIP struct {
value string
}
func (c *TypePreferIP) UnmarshalText(data []byte) error {
if len(data) == 0 {
return nil
}
text := strings.ToLower(string(data))
switch text {
case TypePreferIPPreferIPv4, TypePreferIPPreferIPv6, TypePreferOnlyIPv4, TypePreferOnlyIPv6:
c.value = text
default:
return fmt.Errorf("incorrect prefer-ip value: %s", string(data))
}
return nil
}
func (c TypePreferIP) MarshalText() ([]byte, error) {
return []byte(c.value), nil
}
func (c *TypePreferIP) String() string {
return c.value
}
func (c *TypePreferIP) Value(defaultValue string) string {
if c.value == "" {
return defaultValue
}
return c.value
}
-142
View File
@@ -1,142 +0,0 @@
package config_test
import (
"encoding/json"
"strings"
"testing"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type typePreferIPTestStruct struct {
Value config.TypePreferIP `json:"value"`
}
type TypePreferIPTestSuite struct {
suite.Suite
}
func (suite *TypePreferIPTestSuite) TestUnmarshalNil() {
typ := &config.TypePreferIP{}
suite.NoError(typ.UnmarshalText(nil))
suite.Empty(typ.String())
}
func (suite *TypePreferIPTestSuite) TestUnmarshalFail() {
testData := []string{
"p",
"ipv4",
"onlyipv4",
"ipv6prefer",
}
for _, v := range testData {
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
assert.Error(t, json.Unmarshal(data, &typePreferIPTestStruct{}))
})
}
}
func (suite *TypePreferIPTestSuite) TestUnmarshalOk() {
testData := []string{
config.TypePreferIPPreferIPv4,
config.TypePreferIPPreferIPv6,
config.TypePreferOnlyIPv4,
config.TypePreferOnlyIPv6,
strings.ToUpper(config.TypePreferIPPreferIPv4),
strings.ToUpper(config.TypePreferIPPreferIPv6),
strings.ToUpper(config.TypePreferOnlyIPv4),
strings.ToUpper(config.TypePreferOnlyIPv6),
strings.ToLower(config.TypePreferIPPreferIPv4),
strings.ToLower(config.TypePreferIPPreferIPv6),
strings.ToLower(config.TypePreferOnlyIPv4),
strings.ToLower(config.TypePreferOnlyIPv6),
}
for _, v := range testData {
value := v
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
testStruct := &typePreferIPTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.EqualValues(t,
strings.ToLower(value),
testStruct.Value.Value(config.TypePreferIPPreferIPv4))
})
}
}
func (suite *TypePreferIPTestSuite) TestMarshalOk() {
testData := []string{
config.TypePreferIPPreferIPv4,
config.TypePreferIPPreferIPv6,
config.TypePreferOnlyIPv4,
config.TypePreferOnlyIPv6,
strings.ToUpper(config.TypePreferIPPreferIPv4),
strings.ToUpper(config.TypePreferIPPreferIPv6),
strings.ToUpper(config.TypePreferOnlyIPv4),
strings.ToUpper(config.TypePreferOnlyIPv6),
strings.ToLower(config.TypePreferIPPreferIPv4),
strings.ToLower(config.TypePreferIPPreferIPv6),
strings.ToLower(config.TypePreferOnlyIPv4),
strings.ToLower(config.TypePreferOnlyIPv6),
}
for _, v := range testData {
value := v
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
testStruct := &typePreferIPTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.Equal(t, strings.ToLower(value), testStruct.Value.String())
marshalled, err := testStruct.Value.MarshalText()
assert.NoError(t, err)
assert.Equal(t, strings.ToLower(value), string(marshalled))
})
}
}
func (suite *TypePreferIPTestSuite) TestValue() {
testStruct := &typePreferIPTestStruct{}
suite.EqualValues(config.TypePreferIPPreferIPv4,
testStruct.Value.Value(config.TypePreferIPPreferIPv4))
suite.EqualValues(config.TypePreferIPPreferIPv6,
testStruct.Value.Value(config.TypePreferIPPreferIPv6))
data, err := json.Marshal(map[string]string{
"value": config.TypePreferOnlyIPv4,
})
suite.NoError(err)
suite.NoError(json.Unmarshal(data, testStruct))
suite.EqualValues(config.TypePreferOnlyIPv4,
testStruct.Value.Value(config.TypePreferOnlyIPv6))
suite.EqualValues(config.TypePreferOnlyIPv4,
testStruct.Value.Value(config.TypePreferIPPreferIPv6))
}
func TestTypePreferIP(t *testing.T) {
t.Parallel()
suite.Run(t, &TypePreferIPTestSuite{})
}
-49
View File
@@ -1,49 +0,0 @@
package config
import (
"fmt"
"strings"
)
const (
TypeStatsdTagFormatInfluxdb = "influxdb"
TypeStatsdTagFormatDatadog = "datadog"
TypeStatsdTagFormatGraphite = "graphite"
)
type TypeStatsdTagFormat struct {
value string
}
func (c *TypeStatsdTagFormat) UnmarshalText(data []byte) error {
if len(data) == 0 {
return nil
}
text := strings.ToLower(string(data))
switch text {
case TypeStatsdTagFormatInfluxdb, TypeStatsdTagFormatDatadog, TypeStatsdTagFormatGraphite:
c.value = text
default:
return fmt.Errorf("incorrect tag format value: %s", string(data))
}
return nil
}
func (c TypeStatsdTagFormat) MarshalText() ([]byte, error) {
return []byte(c.value), nil
}
func (c *TypeStatsdTagFormat) String() string {
return c.value
}
func (c *TypeStatsdTagFormat) Value(defaultValue string) string {
if c.value == "" {
return defaultValue
}
return c.value
}
@@ -1,136 +0,0 @@
package config_test
import (
"encoding/json"
"strings"
"testing"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type typeStatsdTagFormatTestStruct struct {
Value config.TypeStatsdTagFormat `json:"value"`
}
type TypeStatsdTagFormatTestSuite struct {
suite.Suite
}
func (suite *TypeStatsdTagFormatTestSuite) TestUnmarshalNil() {
typ := &config.TypeStatsdTagFormat{}
suite.NoError(typ.UnmarshalText(nil))
suite.Equal("lalala", typ.Value("lalala"))
}
func (suite *TypeStatsdTagFormatTestSuite) TestUnmarshalFail() {
testData := []string{
"p",
"ipv4",
"onlyipv4",
"ipv6prefer",
}
for _, v := range testData {
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
assert.Error(t, json.Unmarshal(data, &typeStatsdTagFormatTestStruct{}))
})
}
}
func (suite *TypeStatsdTagFormatTestSuite) TestUnmarshalOk() {
testData := []string{
config.TypeStatsdTagFormatDatadog,
config.TypeStatsdTagFormatInfluxdb,
config.TypeStatsdTagFormatGraphite,
strings.ToUpper(config.TypeStatsdTagFormatDatadog),
strings.ToUpper(config.TypeStatsdTagFormatInfluxdb),
strings.ToUpper(config.TypeStatsdTagFormatGraphite),
strings.ToLower(config.TypeStatsdTagFormatDatadog),
strings.ToLower(config.TypeStatsdTagFormatInfluxdb),
strings.ToLower(config.TypeStatsdTagFormatGraphite),
}
for _, v := range testData {
value := v
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
testStruct := &typeStatsdTagFormatTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.EqualValues(t,
strings.ToLower(value),
testStruct.Value.Value(config.TypeStatsdTagFormatDatadog))
})
}
}
func (suite *TypeStatsdTagFormatTestSuite) TestMarshalOk() {
testData := []string{
config.TypeStatsdTagFormatDatadog,
config.TypeStatsdTagFormatInfluxdb,
config.TypeStatsdTagFormatGraphite,
strings.ToUpper(config.TypeStatsdTagFormatDatadog),
strings.ToUpper(config.TypeStatsdTagFormatInfluxdb),
strings.ToUpper(config.TypeStatsdTagFormatGraphite),
strings.ToLower(config.TypeStatsdTagFormatDatadog),
strings.ToLower(config.TypeStatsdTagFormatInfluxdb),
strings.ToLower(config.TypeStatsdTagFormatGraphite),
}
for _, v := range testData {
value := v
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
testStruct := &typeStatsdTagFormatTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.Equal(t, strings.ToLower(value), testStruct.Value.String())
marshalled, err := testStruct.Value.MarshalText()
assert.NoError(t, err)
assert.Equal(t, strings.ToLower(value), string(marshalled))
})
}
}
func (suite *TypeStatsdTagFormatTestSuite) TestValue() {
testStruct := &typePreferIPTestStruct{}
suite.EqualValues(config.TypePreferIPPreferIPv4,
testStruct.Value.Value(config.TypePreferIPPreferIPv4))
suite.EqualValues(config.TypePreferIPPreferIPv6,
testStruct.Value.Value(config.TypePreferIPPreferIPv6))
data, err := json.Marshal(map[string]string{
"value": config.TypePreferOnlyIPv4,
})
suite.NoError(err)
suite.NoError(json.Unmarshal(data, testStruct))
suite.EqualValues(config.TypePreferOnlyIPv4,
testStruct.Value.Value(config.TypePreferOnlyIPv6))
suite.EqualValues(config.TypePreferOnlyIPv4,
testStruct.Value.Value(config.TypePreferIPPreferIPv6))
}
func TestTypeStatsdTagFormat(t *testing.T) {
t.Parallel()
suite.Run(t, &TypeStatsdTagFormatTestSuite{})
}
-71
View File
@@ -1,71 +0,0 @@
package config
import (
"fmt"
"net"
"net/url"
)
type TypeURL struct {
value *url.URL
}
func (c *TypeURL) UnmarshalText(data []byte) error { // nolint: cyclop
if len(data) == 0 {
return nil
}
value, err := url.Parse(string(data))
if err != nil {
return fmt.Errorf("incorrect URL: %w", err)
}
switch value.Scheme {
case "http", "https", "socks5":
case "":
return fmt.Errorf("url %s has to have a schema", value)
default:
return fmt.Errorf("unsupported schema %s", value.Scheme)
}
if value.Host == "" {
return fmt.Errorf("url %s has to have a host", value)
}
if _, _, err := net.SplitHostPort(value.Host); err != nil {
switch value.Scheme {
case "http":
value.Host = net.JoinHostPort(value.Host, "80")
case "https":
value.Host = net.JoinHostPort(value.Host, "443")
case "socks5":
value.Host = net.JoinHostPort(value.Host, "1080")
default:
return fmt.Errorf("cannot set a default port for %s", value)
}
}
c.value = value
return nil
}
func (c *TypeURL) MarshalText() ([]byte, error) {
return []byte(c.String()), nil
}
func (c TypeURL) String() string {
if c.value == nil {
return ""
}
return c.value.String()
}
func (c TypeURL) Value(defaultValue *url.URL) *url.URL {
if c.value == nil {
return defaultValue
}
return c.value
}
-107
View File
@@ -1,107 +0,0 @@
package config_test
import (
"encoding/json"
"net/url"
"testing"
"github.com/9seconds/mtg/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type typeURLTestStruct struct {
Value config.TypeURL `json:"value"`
}
type TypeURLTestSuite struct {
suite.Suite
}
func (suite *TypeURLTestSuite) TestUnmarshalNil() {
u, _ := url.Parse("https://google.com")
typ := &config.TypeURL{}
suite.NoError(typ.UnmarshalText(nil))
suite.Empty(typ.String())
suite.Equal("https://google.com", typ.Value(u).String())
}
func (suite *TypeURLTestSuite) TestUnmarshalFail() {
testData := []string{
"http:/aaa.com",
"ipv4",
"111",
"://111",
"http://aaa.com:xxx",
"gopher://aaa.com:888",
"gopher://aaa.com",
}
for _, v := range testData {
data, err := json.Marshal(map[string]string{
"value": v,
})
suite.NoError(err)
suite.T().Run(v, func(t *testing.T) {
assert.Error(t, json.Unmarshal(data, &typeURLTestStruct{}))
})
}
}
func (suite *TypeURLTestSuite) TestUnmarshalOk() {
testData := map[string]string{
"https://10.0.0.10:80": "https://10.0.0.10:80",
"https://10.0.0.10:443": "https://10.0.0.10",
"http://10.0.0.10:8": "http://10.0.0.10:8",
"http://10.0.0.10:80": "http://10.0.0.10",
"socks5://10.0.0.10:1080": "socks5://10.0.0.10",
"socks5://10.0.0.10:888": "socks5://10.0.0.10:888",
}
for k, v := range testData {
expected := k
actual := v
data, err := json.Marshal(map[string]string{
"value": actual,
})
suite.NoError(err)
suite.T().Run(actual, func(t *testing.T) {
testStruct := &typeURLTestStruct{}
assert.NoError(t, json.Unmarshal(data, testStruct))
assert.Equal(t, expected, testStruct.Value.Value(nil).String())
marshalled, err := testStruct.Value.MarshalText()
assert.NoError(t, err)
assert.Equal(t, expected, string(marshalled))
})
}
}
func (suite *TypeURLTestSuite) TestValue() {
testStruct := &typeURLTestStruct{}
u1, _ := url.Parse("https://10.0.0.10:80")
u2, _ := url.Parse("https://10.1.0.10:80")
suite.Equal("https://10.0.0.10:80", testStruct.Value.Value(u1).String())
suite.Equal("https://10.1.0.10:80", testStruct.Value.Value(u2).String())
data, err := json.Marshal(map[string]string{
"value": "http://127.0.0.1:80",
})
suite.NoError(err)
suite.NoError(json.Unmarshal(data, testStruct))
suite.Equal("http://127.0.0.1:80", testStruct.Value.Value(u1).String())
suite.Equal("http://127.0.0.1:80", testStruct.Value.Value(u2).String())
}
func TestTypeURL(t *testing.T) {
t.Parallel()
suite.Run(t, &TypeURLTestSuite{})
}
-42
View File
@@ -1,42 +0,0 @@
package testlib
import (
"bytes"
"io"
"os"
"strings"
)
func CaptureStdout(callback func()) string {
return captureOutput(&os.Stdout, callback)
}
func CaptureStderr(callback func()) string {
return captureOutput(&os.Stderr, callback)
}
func captureOutput(filefp **os.File, callback func()) string {
oldFp := *filefp
defer func() {
*filefp = oldFp
}()
reader, writer, _ := os.Pipe()
buf := &bytes.Buffer{}
closeChan := make(chan bool)
go func() {
io.Copy(buf, reader) // nolint: errcheck
close(closeChan)
}()
*filefp = writer
callback()
writer.Close()
<-closeChan
return strings.TrimSpace(buf.String())
}
-7
View File
@@ -1,7 +0,0 @@
package testlib
import "github.com/stretchr/testify/mock"
type EventsObserverMock struct {
mock.Mock
}
@@ -1,11 +0,0 @@
package testlib
import "github.com/stretchr/testify/mock"
type MtglibAntiReplayCacheMock struct {
mock.Mock
}
func (m *MtglibAntiReplayCacheMock) SeenBefore(data []byte) bool {
return m.Called(data).Bool(0)
}
-30
View File
@@ -1,30 +0,0 @@
package testlib
import (
"context"
"net"
"net/http"
"github.com/stretchr/testify/mock"
)
type MtglibNetworkMock struct {
mock.Mock
}
func (m *MtglibNetworkMock) Dial(network, address string) (net.Conn, error) {
args := m.Called(network, address)
return args.Get(0).(net.Conn), args.Error(1) // nolint: wrapcheck
}
func (m *MtglibNetworkMock) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
args := m.Called(ctx, network, address)
return args.Get(0).(net.Conn), args.Error(1) // nolint: wrapcheck
}
func (m *MtglibNetworkMock) MakeHTTPClient(dialFunc func(ctx context.Context,
network, address string) (net.Conn, error)) *http.Client {
return m.Called(dialFunc).Get(0).(*http.Client)
}
-48
View File
@@ -1,48 +0,0 @@
package testlib
import (
"net"
"time"
"github.com/stretchr/testify/mock"
)
type NetConnMock struct {
mock.Mock
}
func (n *NetConnMock) Read(b []byte) (int, error) {
args := n.Called(b)
return args.Int(0), args.Error(1)
}
func (n *NetConnMock) Write(b []byte) (int, error) {
args := n.Called(b)
return args.Int(0), args.Error(1)
}
func (n *NetConnMock) Close() error {
return n.Called().Error(0) // nolint: wrapcheck
}
func (n *NetConnMock) LocalAddr() net.Addr {
return n.Called().Get(0).(net.Addr)
}
func (n *NetConnMock) RemoteAddr() net.Addr {
return n.Called().Get(0).(net.Addr)
}
func (n *NetConnMock) SetDeadline(t time.Time) error {
return n.Called(t).Error(0) // nolint: wrapcheck
}
func (n *NetConnMock) SetReadDeadline(t time.Time) error {
return n.Called(t).Error(0) // nolint: wrapcheck
}
func (n *NetConnMock) SetWriteDeadline(t time.Time) error {
return n.Called(t).Error(0) // nolint: wrapcheck
}
-371
View File
@@ -1,371 +0,0 @@
package ipblocklist
import (
"bufio"
"context"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"sync"
"time"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/kentik/patricia"
"github.com/kentik/patricia/bool_tree"
"github.com/panjf2000/ants/v2"
)
const (
fireholIPv4DefaultCIDR = 32
fireholIPv6DefaultCIDR = 128
)
var fireholRegexpComment = regexp.MustCompile(`\s*#.*?$`)
// Firehol is IPBlocklist which uses lists from FireHOL:
// https://iplists.firehol.org/
//
// It can use both local files and remote URLs. This is not necessary
// that blocklists should be taken from this website, we expect only
// compatible formats here.
//
// Example of the format:
//
// # this is a comment
// # to ignore
// 127.0.0.1 # you can specify an IP
// 10.0.0.0/8 # or cidr
type Firehol struct {
ctx context.Context
ctxCancel context.CancelFunc
logger mtglib.Logger
rwMutex sync.RWMutex
remoteURLs []string
localFiles []string
httpClient *http.Client
workerPool *ants.Pool
treeV4 *bool_tree.TreeV4
treeV6 *bool_tree.TreeV6
}
// Shutdown stop a background update process.
func (f *Firehol) Shutdown() {
f.ctxCancel()
}
// Contains is given IP list can be found in FireHOL blocklists.
func (f *Firehol) Contains(ip net.IP) bool {
if ip == nil {
return true
}
f.rwMutex.RLock()
defer f.rwMutex.RUnlock()
if ip4 := ip.To4(); ip4 != nil {
return f.containsIPv4(ip4)
}
return f.containsIPv6(ip.To16())
}
// Run starts a background update process.
//
// This is a blocking method so you probably want to run it in a
// goroutine.
func (f *Firehol) Run(updateEach time.Duration) {
if updateEach == 0 {
updateEach = DefaultFireholUpdateEach
}
ticker := time.NewTicker(updateEach)
defer func() {
ticker.Stop()
select {
case <-ticker.C:
default:
}
}()
if err := f.update(); err != nil {
f.logger.WarningError("cannot update blocklist", err)
} else {
f.logger.Info("blocklist was updated")
}
for {
select {
case <-f.ctx.Done():
return
case <-ticker.C:
if err := f.update(); err != nil {
f.logger.WarningError("cannot update blocklist", err)
} else {
f.logger.Info("blocklist was updated")
}
}
}
}
func (f *Firehol) containsIPv4(addr net.IP) bool {
ip := patricia.NewIPv4AddressFromBytes(addr, 32)
if ok, _, err := f.treeV4.FindDeepestTag(ip); ok && err == nil {
return true
}
return false
}
func (f *Firehol) containsIPv6(addr net.IP) bool {
ip := patricia.NewIPv6Address(addr, 128)
if ok, _, err := f.treeV6.FindDeepestTag(ip); ok && err == nil {
return true
}
return false
}
func (f *Firehol) update() error { // nolint: funlen, cyclop
ctx, cancel := context.WithCancel(f.ctx)
defer cancel()
wg := &sync.WaitGroup{}
wg.Add(len(f.remoteURLs) + len(f.localFiles))
treeMutex := &sync.Mutex{}
v4tree := bool_tree.NewTreeV4()
v6tree := bool_tree.NewTreeV6()
errorChan := make(chan error, 1)
defer close(errorChan)
for _, v := range f.localFiles {
go func(filename string) {
defer wg.Done()
if err := f.updateLocalFile(ctx, filename, treeMutex, v4tree, v6tree); err != nil {
cancel()
f.logger.BindStr("filename", filename).WarningError("cannot update", err)
select {
case errorChan <- err:
default:
}
}
}(v)
}
for _, v := range f.remoteURLs {
value := v
f.workerPool.Submit(func() { // nolint: errcheck
defer wg.Done()
if err := f.updateRemoteURL(ctx, value, treeMutex, v4tree, v6tree); err != nil {
cancel()
f.logger.BindStr("url", value).WarningError("cannot update", err)
select {
case errorChan <- err:
default:
}
}
})
}
wg.Wait()
select {
case err := <-errorChan:
return fmt.Errorf("cannot update trees: %w", err)
default:
}
f.rwMutex.Lock()
defer f.rwMutex.Unlock()
f.treeV4 = v4tree
f.treeV6 = v6tree
return nil
}
func (f *Firehol) updateLocalFile(ctx context.Context, filename string,
mutex sync.Locker,
v4tree *bool_tree.TreeV4, v6tree *bool_tree.TreeV6) error {
filefp, err := os.Open(filename)
if err != nil {
return fmt.Errorf("cannot open file: %w", err)
}
go func(ctx context.Context, closer io.Closer) {
<-ctx.Done()
closer.Close()
}(ctx, filefp)
defer filefp.Close()
return f.updateTrees(mutex, filefp, v4tree, v6tree)
}
func (f *Firehol) updateRemoteURL(ctx context.Context, url string,
mutex sync.Locker,
v4tree *bool_tree.TreeV4, v6tree *bool_tree.TreeV6) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("cannot build a request: %w", err)
}
resp, err := f.httpClient.Do(req) // nolint: bodyclose
if err != nil {
return fmt.Errorf("cannot request a remote URL %s: %w", url, err)
}
go func(ctx context.Context, closer io.Closer) {
<-ctx.Done()
closer.Close()
}(ctx, resp.Body)
defer func(rc io.ReadCloser) {
io.Copy(io.Discard, rc) // nolint: errcheck
rc.Close()
}(resp.Body)
return f.updateTrees(mutex, resp.Body, v4tree, v6tree)
}
func (f *Firehol) updateTrees(mutex sync.Locker,
reader io.Reader,
v4tree *bool_tree.TreeV4,
v6tree *bool_tree.TreeV6) error {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
text := scanner.Text()
text = fireholRegexpComment.ReplaceAllLiteralString(text, "")
text = strings.TrimSpace(text)
if text == "" {
continue
}
ip, cidr, err := f.updateParseLine(text)
if err != nil {
return fmt.Errorf("cannot parse a line: %w", err)
}
if err := f.updateAddToTrees(ip, cidr, mutex, v4tree, v6tree); err != nil {
return fmt.Errorf("cannot add a node to the tree: %w", err)
}
}
if scanner.Err() != nil {
return fmt.Errorf("cannot parse a response: %w", scanner.Err())
}
return nil
}
func (f *Firehol) updateParseLine(text string) (net.IP, uint, error) {
_, ipnet, err := net.ParseCIDR(text)
if err != nil {
ipaddr := net.ParseIP(text)
if ipaddr == nil {
return nil, 0, fmt.Errorf("incorrect ip address %s", text)
}
ip4 := ipaddr.To4()
if ip4 != nil {
return ip4, fireholIPv4DefaultCIDR, nil
}
return ipaddr.To16(), fireholIPv6DefaultCIDR, nil
}
ones, _ := ipnet.Mask.Size()
return ipnet.IP, uint(ones), nil
}
func (f *Firehol) updateAddToTrees(ip net.IP, cidr uint,
mutex sync.Locker,
v4tree *bool_tree.TreeV4, v6tree *bool_tree.TreeV6) error {
mutex.Lock()
defer mutex.Unlock()
if ip.To4() != nil {
addr := patricia.NewIPv4AddressFromBytes(ip, cidr)
if _, _, err := v4tree.Set(addr, true); err != nil {
return err // nolint: wrapcheck
}
} else {
addr := patricia.NewIPv6Address(ip, cidr)
if _, _, err := v6tree.Set(addr, true); err != nil {
return err // nolint: wrapcheck
}
}
return nil
}
// NewFirehol creates a new instance of FireHOL IP blocklist.
//
// This method does not start an update process so please execute Run
// when it is necessary.
func NewFirehol(logger mtglib.Logger, network mtglib.Network,
downloadConcurrency uint,
remoteURLs []string,
localFiles []string) (*Firehol, error) {
for _, v := range remoteURLs {
parsed, err := url.Parse(v)
if err != nil {
return nil, fmt.Errorf("incorrect url %s: %w", v, err)
}
switch parsed.Scheme {
case "http", "https":
default:
return nil, fmt.Errorf("unsupported url %s", v)
}
}
for _, v := range localFiles {
if stat, err := os.Stat(v); os.IsNotExist(err) || stat.IsDir() || stat.Mode().Perm()&0o400 == 0 {
return nil, fmt.Errorf("%s is not a readable file", v)
}
}
if downloadConcurrency == 0 {
downloadConcurrency = DefaultFireholDownloadConcurrency
}
workerPool, _ := ants.NewPool(int(downloadConcurrency))
ctx, cancel := context.WithCancel(context.Background())
return &Firehol{
ctx: ctx,
ctxCancel: cancel,
logger: logger.Named("firehol"),
httpClient: network.MakeHTTPClient(nil),
treeV4: bool_tree.NewTreeV4(),
treeV6: bool_tree.NewTreeV6(),
workerPool: workerPool,
remoteURLs: remoteURLs,
localFiles: localFiles,
}, nil
}
-148
View File
@@ -1,148 +0,0 @@
package ipblocklist_test
import (
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
"github.com/9seconds/mtg/v2/internal/testlib"
"github.com/9seconds/mtg/v2/ipblocklist"
"github.com/9seconds/mtg/v2/logger"
"github.com/9seconds/mtg/v2/network"
"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type FireholTestSuite struct {
suite.Suite
networkMock *testlib.MtglibNetworkMock
httpServer *httptest.Server
}
func (suite *FireholTestSuite) SetupSuite() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
filefp, err := os.Open(filepath.Join("testdata", "remote_ipset.ipset"))
if err != nil {
panic(err)
}
defer filefp.Close()
io.Copy(w, filefp) // nolint: errcheck
})
suite.httpServer = httptest.NewServer(mux)
}
func (suite *FireholTestSuite) SetupTest() {
httpClient := &http.Client{}
suite.networkMock = &testlib.MtglibNetworkMock{}
httpmock.ActivateNonDefault(httpClient)
suite.networkMock.
On("MakeHTTPClient", mock.Anything).
Maybe().
Return(httpClient)
}
func (suite *FireholTestSuite) TearDownTest() {
suite.networkMock.AssertExpectations(suite.T())
httpmock.DeactivateAndReset()
}
func (suite *FireholTestSuite) TearDownSuite() {
suite.httpServer.Close()
}
func (suite *FireholTestSuite) TestLocalFail() {
blocklist, err := ipblocklist.NewFirehol(logger.NewNoopLogger(),
suite.networkMock, 2,
nil, []string{filepath.Join("testdata", "broken_ipset.ipset")})
suite.NoError(err)
go blocklist.Run(time.Hour)
time.Sleep(500 * time.Millisecond)
suite.False(blocklist.Contains(net.ParseIP("10.0.0.10")))
suite.False(blocklist.Contains(net.ParseIP("127.0.0.1")))
blocklist.Shutdown()
time.Sleep(500 * time.Millisecond)
}
func (suite *FireholTestSuite) TestLocalOk() {
blocklist, err := ipblocklist.NewFirehol(logger.NewNoopLogger(),
suite.networkMock, 2,
nil, []string{filepath.Join("testdata", "good_ipset.ipset")})
suite.NoError(err)
go blocklist.Run(time.Hour)
time.Sleep(500 * time.Millisecond)
suite.True(blocklist.Contains(net.ParseIP("10.0.0.10")))
suite.False(blocklist.Contains(net.ParseIP("127.0.0.1")))
blocklist.Shutdown()
time.Sleep(500 * time.Millisecond)
}
func (suite *FireholTestSuite) TestRemoteFail() {
blocklist, err := ipblocklist.NewFirehol(logger.NewNoopLogger(),
suite.networkMock, 2,
[]string{"https://google.com"}, nil)
suite.NoError(err)
go blocklist.Run(time.Hour)
time.Sleep(500 * time.Millisecond)
suite.False(blocklist.Contains(net.ParseIP("10.2.2.2")))
blocklist.Shutdown()
time.Sleep(500 * time.Millisecond)
}
func (suite *FireholTestSuite) TestMixed() {
dialer, _ := network.NewDefaultDialer(0, 0)
ntw, _ := network.NewNetwork(dialer, "mtg", "1.1.1.1", 0)
blocklist, err := ipblocklist.NewFirehol(logger.NewNoopLogger(),
ntw, 2,
[]string{
suite.httpServer.URL,
}, []string{
filepath.Join("testdata", "good_ipset.ipset"),
})
suite.NoError(err)
go blocklist.Run(time.Hour)
time.Sleep(500 * time.Millisecond)
suite.True(blocklist.Contains(net.ParseIP("10.2.2.2")))
suite.True(blocklist.Contains(net.ParseIP("10.1.0.100")))
blocklist.Shutdown()
time.Sleep(500 * time.Millisecond)
}
func TestFirehol(t *testing.T) {
t.Parallel()
suite.Run(t, &FireholTestSuite{})
}
-18
View File
@@ -1,18 +0,0 @@
// Package ipblocklist contains default implementation of the
// IPBlocklist for mtg.
//
// Please check documentation for mtglib.IPBlocklist interface to get an
// idea of this abstraction.
package ipblocklist
import "time"
const (
// DefaultFireholDownloadConcurrency defines a default max number of
// concurrent downloads of ip blocklists for Firehol.
DefaultFireholDownloadConcurrency = 1
// DefaultFireholUpdateEach defines a default time period when
// Firehol requests updates of the blocklists.
DefaultFireholUpdateEach = 6 * time.Hour
)
-17
View File
@@ -1,17 +0,0 @@
package ipblocklist
import (
"net"
"github.com/9seconds/mtg/v2/mtglib"
)
type noop struct{}
func (n noop) Contains(ip net.IP) bool { return false }
// NewNoop returns a dummy ipblocklist which allows all incoming
// connections.
func NewNoop() mtglib.IPBlocklist {
return noop{}
}

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