Просмотр исходного кода

Merge pull request #180 from 9seconds/v2

V2
tags/v2.0.0-rc1
Sergey Arkhipov 5 лет назад
Родитель
Сommit
81bca75c25
Аккаунт пользователя с таким Email не найден
100 измененных файлов: 4978 добавлений и 1974 удалений
  1. 4
    0
      .codecov.yml
  2. 0
    2
      .github/workflows/ci.yaml
  3. 1
    0
      .gitignore
  4. 1
    1
      .golangci.toml
  5. 2
    3
      Dockerfile
  6. 19
    4
      Makefile
  7. 251
    194
      README.md
  8. 0
    36
      antireplay/cache.go
  9. 13
    26
      antireplay/init.go
  10. 0
    8
      antireplay/nilcache.go
  11. 14
    0
      antireplay/noop.go
  12. 26
    0
      antireplay/noop_test.go
  13. 51
    0
      antireplay/stable_bloom_filter.go
  14. 26
    0
      antireplay/stable_bloom_filter_test.go
  15. 0
    26
      cli/generate.go
  16. 0
    101
      cli/proxy.go
  17. 0
    43
      cli/utils.go
  18. 0
    312
      config/config.go
  19. 0
    78
      config/global_ips.go
  20. 0
    99
      config/urls.go
  21. 0
    6
      conntypes/acks.go
  22. 0
    5
      conntypes/dc.go
  23. 0
    24
      conntypes/id.go
  24. 0
    3
      conntypes/packet.go
  25. 0
    22
      conntypes/protocol.go
  26. 0
    29
      conntypes/type.go
  27. 0
    14
      conntypes/wrap_interfaces.go
  28. 0
    41
      conntypes/wrap_packet_ack_interfaces.go
  29. 0
    51
      conntypes/wrap_packet_interfaces.go
  30. 0
    56
      conntypes/wrap_stream_interfaces.go
  31. 108
    0
      events/event_stream.go
  32. 212
    0
      events/event_stream_test.go
  33. 69
    0
      events/init.go
  34. 46
    0
      events/init_test.go
  35. 149
    0
      events/multi_observer.go
  36. 33
    0
      events/noop.go
  37. 78
    0
      events/noop_test.go
  38. 186
    0
      example.config.toml
  39. 0
    122
      faketls/client_protocol.go
  40. 0
    73
      faketls/cloak.go
  41. 0
    30
      faketls/consts.go
  42. 21
    18
      go.mod
  43. 54
    63
      go.sum
  44. 0
    172
      hub/connection.go
  45. 0
    70
      hub/connection_list.go
  46. 0
    40
      hub/hub.go
  47. 0
    24
      hub/init.go
  48. 0
    7
      hub/interface.go
  49. 0
    90
      hub/mux.go
  50. 0
    79
      hub/proxy_conn.go
  51. 192
    0
      internal/cli/access.go
  52. 197
    0
      internal/cli/access_test.go
  53. 81
    0
      internal/cli/base.go
  54. 33
    0
      internal/cli/base_internal_test.go
  55. 10
    0
      internal/cli/cli.go
  56. 24
    0
      internal/cli/generate_secret.go
  57. 51
    0
      internal/cli/generate_secret_test.go
  58. 37
    0
      internal/cli/init_test.go
  59. 167
    0
      internal/cli/proxy.go
  60. 2
    0
      internal/cli/testdata/minimal.toml
  61. 157
    0
      internal/config/config.go
  62. 54
    0
      internal/config/config_test.go
  63. 1
    0
      internal/config/testdata/broken.toml
  64. 2
    0
      internal/config/testdata/minimal.toml
  65. 1
    0
      internal/config/testdata/only_secret.toml
  66. 68
    0
      internal/config/type_blocklist_uri.go
  67. 177
    0
      internal/config/type_blocklist_uri_test.go
  68. 54
    0
      internal/config/type_bytes.go
  69. 120
    0
      internal/config/type_bytes_test.go
  70. 46
    0
      internal/config/type_duration.go
  71. 118
    0
      internal/config/type_duration_test.go
  72. 43
    0
      internal/config/type_error_rate.go
  73. 125
    0
      internal/config/type_error_rate_test.go
  74. 67
    0
      internal/config/type_hostport.go
  75. 115
    0
      internal/config/type_hostport_test.go
  76. 35
    0
      internal/config/type_http_path.go
  77. 91
    0
      internal/config/type_http_path_test.go
  78. 45
    0
      internal/config/type_ip.go
  79. 115
    0
      internal/config/type_ip_test.go
  80. 41
    0
      internal/config/type_metric_prefix.go
  81. 115
    0
      internal/config/type_metric_prefix_test.go
  82. 45
    0
      internal/config/type_port.go
  83. 117
    0
      internal/config/type_port_test.go
  84. 50
    0
      internal/config/type_prefer_ip.go
  85. 142
    0
      internal/config/type_prefer_ip_test.go
  86. 49
    0
      internal/config/type_statsd_tag_format.go
  87. 136
    0
      internal/config/type_statsd_tag_format_test.go
  88. 71
    0
      internal/config/type_url.go
  89. 107
    0
      internal/config/type_url_test.go
  90. 42
    0
      internal/testlib/capture_output.go
  91. 7
    0
      internal/testlib/events_observer_mock.go
  92. 11
    0
      internal/testlib/mtglib_antireplay_cache_mock.go
  93. 30
    0
      internal/testlib/mtglib_network_mock.go
  94. 48
    0
      internal/testlib/net_conn_mock.go
  95. 0
    0
      internal/utils/rlimit.go
  96. 0
    0
      internal/utils/rlimit_windows.go
  97. 1
    1
      internal/utils/root_context.go
  98. 1
    1
      internal/utils/root_context_windows.go
  99. 373
    0
      ipblocklist/firehol.go
  100. 0
    0
      ipblocklist/firehol_test.go

+ 4
- 0
.codecov.yml Просмотреть файл

1
+---
2
+
3
+fixes:
4
+  - "github.com/9seconds/mtg/v2/::"

+ 0
- 2
.github/workflows/ci.yaml Просмотреть файл

30
     strategy:
30
     strategy:
31
       matrix:
31
       matrix:
32
         go_version:
32
         go_version:
33
-          - ~1.14
34
-          - ~1.15
35
           - ^1.16
33
           - ^1.16
36
     steps:
34
     steps:
37
       - name: Checkout
35
       - name: Checkout

+ 1
- 0
.gitignore Просмотреть файл

9
 vendor/
9
 vendor/
10
 ccbuilds/
10
 ccbuilds/
11
 .bin/
11
 .bin/
12
+coverage.txt

+ 1
- 1
.golangci.toml Просмотреть файл

10
 
10
 
11
 [linters]
11
 [linters]
12
 enable-all = true
12
 enable-all = true
13
-disable = ["gochecknoglobals", "gas", "gomnd", "goerr113", "exhaustivestruct"]
13
+disable = ["gochecknoglobals", "gas", "goerr113", "exhaustivestruct"]

+ 2
- 3
Dockerfile Просмотреть файл

24
 FROM scratch
24
 FROM scratch
25
 
25
 
26
 ENTRYPOINT ["/mtg"]
26
 ENTRYPOINT ["/mtg"]
27
-ENV MTG_BIND=0.0.0.0:3128 \
28
-    MTG_STATS_BIND=0.0.0.0:3129
29
-EXPOSE 3128 3129
27
+CMD ["run", "/config.toml"]
30
 
28
 
31
 COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
29
 COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
32
 COPY --from=build /app/mtg /mtg
30
 COPY --from=build /app/mtg /mtg
31
+COPY --from=build /app/example.config.toml /config.toml

+ 19
- 4
Makefile Просмотреть файл

9
 VERSION_GO         := $(shell go version)
9
 VERSION_GO         := $(shell go version)
10
 VERSION_DATE       := $(shell date -Ru)
10
 VERSION_DATE       := $(shell date -Ru)
11
 VERSION_TAG        := $(shell git describe --tags --always)
11
 VERSION_TAG        := $(shell git describe --tags --always)
12
-COMMON_BUILD_FLAGS := -mod=readonly -ldflags="-s -w -X 'main.version=$(VERSION_TAG) ($(VERSION_GO)) [$(VERSION_DATE)]'"
12
+COMMON_BUILD_FLAGS := -mod=readonly -ldflags="-extldflags '-static' -s -w -X 'main.version=$(VERSION_TAG) ($(VERSION_GO)) [$(VERSION_DATE)]'"
13
 
13
 
14
 GOBIN  := $(ROOT_DIR)/.bin
14
 GOBIN  := $(ROOT_DIR)/.bin
15
 GOTOOL := env "GOBIN=$(GOBIN)" "PATH=$(ROOT_DIR)/.bin:$(PATH)"
15
 GOTOOL := env "GOBIN=$(GOBIN)" "PATH=$(ROOT_DIR)/.bin:$(PATH)"
27
 
27
 
28
 .PHONY: static
28
 .PHONY: static
29
 static:
29
 static:
30
-	@env CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo $(COMMON_BUILD_FLAGS) -o "$(APP_NAME)"
30
+	@env CGO_ENABLED=0 GOOS=linux go build \
31
+		$(COMMON_BUILD_FLAGS) \
32
+		-tags netgo \
33
+		-a \
34
+		-o "$(APP_NAME)"
31
 
35
 
32
 $(APP_NAME)-%: GOOS=$(shell echo -n "$@" | sed 's?$(APP_NAME)-??' | cut -f1 -d-)
36
 $(APP_NAME)-%: GOOS=$(shell echo -n "$@" | sed 's?$(APP_NAME)-??' | cut -f1 -d-)
33
 $(APP_NAME)-%: GOARCH=$(shell echo -n "$@" | sed 's?$(APP_NAME)-??' | cut -f2 -d-)
37
 $(APP_NAME)-%: GOARCH=$(shell echo -n "$@" | sed 's?$(APP_NAME)-??' | cut -f2 -d-)
35
 	@env "GOOS=$(GOOS)" "GOARCH=$(GOARCH)" \
39
 	@env "GOOS=$(GOOS)" "GOARCH=$(GOARCH)" \
36
 		go build \
40
 		go build \
37
 		$(COMMON_BUILD_FLAGS) \
41
 		$(COMMON_BUILD_FLAGS) \
42
+		-tags netgo \
43
+		-a \
38
 		-o "./ccbuilds/$(APP_NAME)-$(GOOS)-$(GOARCH)"
44
 		-o "./ccbuilds/$(APP_NAME)-$(GOOS)-$(GOARCH)"
39
 
45
 
40
 .PHONY: ccbuilds
46
 .PHONY: ccbuilds
44
 vendor: go.mod go.sum
50
 vendor: go.mod go.sum
45
 	@$(MOD_ON) go mod vendor
51
 	@$(MOD_ON) go mod vendor
46
 
52
 
53
+.PHONY: fmt
54
+fmt:
55
+	@$(GOTOOL) gofumpt -w -s -extra "$(ROOT_DIR)"
56
+
47
 .PHONY: test
57
 .PHONY: test
48
 test:
58
 test:
49
 	@go test -v ./...
59
 	@go test -v ./...
50
 
60
 
51
 .PHONY: citest
61
 .PHONY: citest
52
 citest:
62
 citest:
53
-	@go test -coverprofile=coverage.txt -covermode=atomic -race -v ./...
63
+	@go test -coverprofile=coverage.txt -covermode=atomic -parallel 2 -race -v ./...
54
 
64
 
55
 .PHONY: crosscompile
65
 .PHONY: crosscompile
56
 crosscompile: $(CC_BINARIES)
66
 crosscompile: $(CC_BINARIES)
74
 	@$(GOTOOL) godoc -http 0.0.0.0:10000
84
 	@$(GOTOOL) godoc -http 0.0.0.0:10000
75
 
85
 
76
 .PHONY: install-tools
86
 .PHONY: install-tools
77
-install-tools: install-tools-lint install-tools-godoc
87
+install-tools: install-tools-lint install-tools-godoc install-tools-gofumpt
78
 
88
 
79
 .PHONY: install-tools-lint
89
 .PHONY: install-tools-lint
80
 install-tools-lint:
90
 install-tools-lint:
87
 	@mkdir -p "$(GOBIN)" || true && \
97
 	@mkdir -p "$(GOBIN)" || true && \
88
 		$(GOTOOL) go get -u golang.org/x/tools/cmd/godoc
98
 		$(GOTOOL) go get -u golang.org/x/tools/cmd/godoc
89
 
99
 
100
+.PHONY: install-tools-gofumpt
101
+install-tools-gofumpt:
102
+	@mkdir -p "$(GOBIN)" || true && \
103
+		$(GOTOOL) go get -u mvdan.cc/gofumpt
104
+
90
 .PHONY: update-deps
105
 .PHONY: update-deps
91
 upgrade-deps:
106
 upgrade-deps:
92
 	$go get -u && go mod tidy
107
 	$go get -u && go mod tidy

+ 251
- 194
README.md Просмотреть файл

1
 # mtg
1
 # mtg
2
 
2
 
3
-Bullshit-free MTPROTO proxy for Telegram
3
+Highly-opionated (ex-bullshit-free) MTPROTO proxy for
4
+[Telegram](https://telegram.org/).
4
 
5
 
5
-[![Build Status](https://travis-ci.org/9seconds/mtg.svg?branch=master)](https://travis-ci.org/9seconds/mtg)
6
-[![Go Report Card](https://goreportcard.com/badge/github.com/9seconds/mtg)](https://goreportcard.com/report/github.com/9seconds/mtg)
7
-[![Docker Build Status](https://img.shields.io/docker/build/nineseconds/mtg.svg)](https://hub.docker.com/r/nineseconds/mtg/)
6
+[![CI](https://github.com/9seconds/mtg/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/9seconds/mtg/actions/workflows/ci.yaml)
7
+[![codecov](https://codecov.io/gh/9seconds/mtg/branch/master/graph/badge.svg?token=JfdDyGVpT4)](https://codecov.io/gh/9seconds/mtg)
8
+[![Go Reference](https://pkg.go.dev/badge/github.com/9seconds/mtg.svg)](https://pkg.go.dev/github.com/9seconds/mtg)
8
 
9
 
9
-**Please see a guide on upgrading to 1.0 at the end of this README.**
10
+**If you use v1.0 or upgrade broke you proxy, please read the chapter
11
+[Version 2](#version-2)**
10
 
12
 
11
-# Rationale
13
+## Rationale
12
 
14
 
13
 There are several available proxies for Telegram MTPROTO available. Here
15
 There are several available proxies for Telegram MTPROTO available. Here
14
 are the most notable:
16
 are the most notable:
17
 * [Python](https://github.com/alexbers/mtprotoproxy)
19
 * [Python](https://github.com/alexbers/mtprotoproxy)
18
 * [Erlang](https://github.com/seriyps/mtproto_proxy)
20
 * [Erlang](https://github.com/seriyps/mtproto_proxy)
19
 
21
 
20
-Almost all of them follow the way how official proxy was built. This
21
-includes support of multiple secrets, support of promoted channels, etc.
22
+You can use any of these. They work great and all implementations have
23
+feature parity now. This includes support of adtag, replay attack
24
+protection, domain fronting, faketls, and so on. mtg has a similar
25
+goal: to give a possibility to connect to Telegram in a restricted,
26
+censored environment. But it does it slightly differently in details
27
+that probably matter.
22
 
28
 
23
-mtg is an implementation in golang which is intended to be:
29
+* **Resource-efficient**
30
+
31
+  It has to be resource-efficient. It does not mean that you will see
32
+  the smallest memory usage. It means that it will try to use allocated
33
+  resources in zero-waste mode, reusing as much memory as possible and
34
+  so on.
24
 
35
 
25
-* **Lightweight**
26
-  It has to consume as few resources as possible but not by losing
27
-  maintainability.
28
 * **Easily deployable**
36
 * **Easily deployable**
37
+
29
   I strongly believe that Telegram proxies should follow the way of
38
   I strongly believe that Telegram proxies should follow the way of
30
-  ShadowSocks: promoted channels is a strange way of doing business
31
-  I suppose. I think the only viable way is to have a proxy with
32
-  minimum configuration which should work everywhere.
39
+  [ShadowSocks](https://shadowsocks.org): promoted channels is a strange
40
+  way of doing business I suppose. I think the only viable way is to
41
+  have a proxy that can be restored anywhere easily.
42
+
33
 * **A single secret**
43
 * **A single secret**
34
-  I think that multiple secrets solve no problems and just complexify
44
+
45
+  I think that multiple secrets solve no problems and just complex
35
   software. I also believe that in the case of throwout proxies, this
46
   software. I also believe that in the case of throwout proxies, this
36
-  feature is a useless luxury.
37
-* **Minimum docker image size**
38
-  Official image is less than 3.5 megabytes. Literally.
39
-* **No management WebUI**
40
-  This is an implementation of a simple lightweight proxy. I won't do that.
47
+  the feature is a useless luxury.
41
 
48
 
42
-This proxy supports 2 modes of work: direct connection to Telegram and
43
-promoted channel mode. If you do not need promoted channels, I would
44
-recommend you to go with direct mode: this way is more robust.
49
+* **No adtag support**
45
 
50
 
46
-To run a proxy in direct mode, all you need to do is just provide a
47
-secret. If you do not provide ADTag as a second parameter, promoted
48
-channels mode won't be activated.
51
+  Please read [Version 2](#version-2) chapter.
49
 
52
 
50
-To get promoted channel, please contact
51
-[@MTProxybot](https://t.me/MTProxybot) and provide generated adtag as a
52
-second parameter.
53
+* **No management WebUI**
53
 
54
 
55
+  This is an implementation of a simple lightweight proxy. I won't do that.
54
 
56
 
55
-# Source code organization
57
+* **Proxy chaining**
58
+
59
+  mtg has the support of [SOCKS5](https://en.wikipedia.org/wiki/SOCKS)
60
+  proxies. So, in theory, you can run this proxy as a frontend
61
+  and route traffic via [v2ray](https://www.v2ray.com/),
62
+  [Gost](https://docs.ginuerzh.xyz/gost/),
63
+  [Trojan](https://trojan-gfw.github.io/trojan/), or any other project
64
+  you like.
65
+
66
+* **Native blocklist support**
67
+
68
+  Previously, this was delegated to the [FireHOL](https://firehol.org/)
69
+  project or similar ones which track attacks and publish a list of
70
+  potentially dangerous IPs. mtg has native support of such blocklists.
71
+
72
+* **Can be used as a library**
56
 
73
 
57
-There are 2 main branches:
74
+  mtg v2 was redesigned in a way so it can be embedded into your
75
+  software (written in Golang) with a minimum effort + you can replace
76
+  some parts with those you want.
58
 
77
 
59
-1. `master` branch contains potentially unstable features
60
-2. `stable` branch contains stable version. Usually you want to use this branch.
78
+### Version 2
61
 
79
 
62
-# How to build
80
+If you use version 1.x before, you are probably noticed some major
81
+backward non-compatible details:
63
 
82
 
64
-```console
65
-make
66
-```
83
+1. Configuration file
84
+2. Removed support of adtag
67
 
85
 
68
-If you want to build for another platform:
86
+For the configuration file, please check out the full example in this
87
+repository. It has a lot of comments and most of the options are
88
+optional. We do have only `secret` and `bind-to` sections mandatory.
89
+Other sections in the example configuration file are filled with default
90
+values.
69
 
91
 
70
-```console
71
-make crosscompile
72
-```
92
+Adtag support was removed completely. This was done to debloat mtg and
93
+keep it simple and obvious. Hopefully, this goal is achieved and the
94
+source code is clean and straightforward enough.
73
 
95
 
74
-If you want to build Docker image (called `mtg`):
96
+I always was quite skeptical about adtag. In my POV, a proxy as a fat
97
+big connectivity point for hundreds of clients is an illusion. If you
98
+work in a censored environment, the first thing that authority does is
99
+IP blocking. For us, it means, those big proxies that can benefit from
100
+having a pinned channel are going to be blocked in a minute.
75
 
101
 
76
-```console
77
-make docker
78
-```
102
+Proxy has to be intimate. It has to be shared within a small group as
103
+a family or maybe your college friends. It has to have a small number
104
+of connections and never publicly announced its presence. It has to fly
105
+under the radar. If the proxy is detected, you need to be able to give
106
+a rebirth on a new IP address as soon as possible. I do no think that
107
+having some special channel for such a use case makes any sense.
79
 
108
 
80
-# Docker image
109
+But other details like replay attack protection, domain fronting,
110
+accurate FakeTLS implementation, IP blacklisting, and proxy
111
+chaining matter here. If you work in censored perimeter like
112
+[GFW](https://en.wikipedia.org/wiki/Great_Firewall)-protected
113
+country, you probably want to have an MTPROTO proxy as
114
+a frontend that transports traffic via cloaked tunnels
115
+made by [Trojan](https://trojan-gfw.github.io/trojan/),
116
+[Shadowsocks](https://shadowsocks.org), [v2ray](https://www.v2ray.com/),
117
+or [Gost](https://docs.ginuerzh.xyz/gost/). That's why you have to have
118
+the support of chaining as a first-class citizen.
81
 
119
 
82
-Docker follows the same policy as the source code organization:
120
+Yes, this is possible and doable with optional adtag support. But the
121
+truth is that the MTPROTO proxy for Telegram is just a thing that either
122
+work as a normal client (direct mode) or doing some RPC calls in [TL
123
+language](https://core.telegram.org/mtproto/TL) (adtag support). I
124
+understand the intention of the developers and I understand that they
125
+were under high pressure fighting with [RKN](https://rkn.gov.ru/) and
126
+doing TON after that. Nothing is ideal. But for the proxy, it means that
127
+source code is full of complex non-trivial code which is required only
128
+to support a feature that we barely need.
83
 
129
 
84
-- `latest` mirrors the master branch
85
-- `stable` mirrors the stable branch
86
-- tags are for tagged releases
130
+So, to have a reasonable MTPROTO proxy, adtag support was removed. This
131
+is a rare chance in my career where software v2 debloats a previous
132
+version. It feels so good :)
87
 
133
 
88
-```console
89
-docker pull nineseconds/mtg:latest
90
-```
134
+### Version 1 and 2
91
 
135
 
92
-```console
93
-docker pull nineseconds/mtg:stable
94
-```
136
+I do continue to support both versions 1 and 2. But in a different mode.
95
 
137
 
96
-```console
97
-docker pull nineseconds/mtg:0.10
98
-```
138
+Version 1 is now officially in maintenance mode. It means that I won't
139
+make any new features or improvements there. You can consider a feature
140
+freeze there. No bugs are going to be fixed there except for critical
141
+ones. PRs are welcome though. The goal is to keep it working. It will
142
+get some periodical updates like updates to the new Golang version of
143
+dependencies version bump, but that's mostly it.
99
 
144
 
100
-# Ansible role
145
+**If you want to have mtg with _adtag support_, please use version 1**.
101
 
146
 
102
-You can find unofficial Ansible role for mtg here: https://github.com/rlex/ansible-role-mtg
103
-Also, there is another project on Ansible Galaxy: https://galaxy.ansible.com/ivansible/lin_mtproxy
147
+Version 2 is going to have all my love, active support, bug fixing, etc.
148
+It is under active development and maintenance.
104
 
149
 
105
-# Configuration
150
+This project has several main branches
106
 
151
 
107
-To run this tool you need to configure as less as possible. Telegram
108
-clients support 3 different secret types:
152
+1. [`master`](https://github.com/9seconds/mtg/tree/master) branch
153
+   contains a bleeding edge. It may potentially have some features
154
+   which will break your source code.
155
+2. [`stable`](https://github.com/9seconds/mtg/tree/stable) branch contains
156
+   dumps of a master branch when we consider it 'stable'. This is a
157
+   branch you probably want to pick.
158
+3. [`v2`](https://github.com/9seconds/mtg/tree/v2) has a development
159
+   of the v2.x version. In theory, it is the same as `master` but this
160
+   will change when we have v3.x.
161
+4. [`v1`](https://github.com/9seconds/mtg/tree/v1) has a version 1.x.
162
+
163
+## Getting started
109
 
164
 
110
-* Simple - basically, it is just a flow of frames ciphered by AES-CTR stream
111
-  cipher.
112
-* Secured - the same stream as simple but with some random noise to prevent
113
-  statistical analysis of traffic flow.
114
-* FakeTLS - this mode envelops telegram stream in TLS so it looks (in theory)
115
-  the same as any TLS1.3 traffic from DPI point of view.
165
+### Download a tool
166
+
167
+#### Download binaries
168
+
169
+Binaries can be downloaded from the release page. Also, you can download
170
+docker image.
116
 
171
 
117
-If you do not have preferences, go with FakeTLS or at least secured.
118
-Simple mode is a little bit naive and traffic flow can be easily
119
-identified as Telegram one.
172
+For the current version, please download like
120
 
173
 
121
-Unlike the rest of implementation, mtg is quite strict about the
122
-execution mode: if you run a proxy instance with FakeTLS secret, you
123
-can't connect to it with simple or secured clients. You can't connect
124
-to the proxy with secured secret with FakeTLS key. It forces one mode
125
-of working. So, unfortunately, there is no way how to connect to the
126
-deployed proxy with another secret (if you know how to construct and
127
-convert them). But at the same time, old clients can't connect so they
128
-won't expose the type of the service.
174
+```console
175
+docker pull nineseconds/mtg:2
176
+```
129
 
177
 
130
-First, you need to generate a secret:
178
+For version 1:
131
 
179
 
132
 ```console
180
 ```console
133
-$ mtg generate-secret simple
134
-52a493bdfb90eea55739eabff2d92a14
181
+docker pull nineseconds/mtg:1
135
 ```
182
 ```
136
 
183
 
184
+You may also check both [Docker
185
+Hub](https://hub.docker.com/r/nineseconds/mtg/tags) and [Github
186
+Registry](https://github.com/users/9seconds/packages/container/package/mtg).
187
+Please do not choose `latest` or `stable` if you want to avoid
188
+surprises. Always choose some version tag.
189
+
190
+Also, if you have `go` installed, you can always download this tool with `go get`:
191
+
137
 ```console
192
 ```console
138
-$ mtg generate-secret secured
139
-ddf05fb7acb549be047a7c585116581418
193
+go get github.com/9seconds/mtg/v2
140
 ```
194
 ```
141
 
195
 
196
+#### Build from sources
197
+
142
 ```console
198
 ```console
143
-$ mtg generate-secret -c google.com tls
144
-ee852380f362a09343efb4690c4e17862e676f6f676c652e636f6d
199
+git clone https://github.com:9seconds/mtg.git
200
+cd mtg
201
+make static
145
 ```
202
 ```
146
 
203
 
147
-Or, if you prefer docker:
204
+or for the docker image:
148
 
205
 
149
 ```console
206
 ```console
150
-$ docker run --rm nineseconds/mtg generate-secret tls -c bing.com
151
-eedf71035a8ed48a623d8e83e66aec4d0562696e672e636f6d
207
+make docker
152
 ```
208
 ```
153
 
209
 
154
-## Antireplay cache
210
+### Generate secret
155
 
211
 
156
-To prevent replay attacks, we have internal storage of first frames
157
-messages for connected clients. These frames are generated randomly
158
-by design and we have the negligible possibility of duplication
159
-(probability is 1/(2^64)) but it could be quite effective to prevent
160
-replays.
212
+If you already have a secret in Base64 format or that, which starts with `ee`,
213
+you can skip this chapter. Otherwise:
161
 
214
 
162
-It is possible to disable this cache. To do that, please explicitly set
163
-its size to 0.
215
+```console
216
+$ mtg generate-secret google.com
217
+7ibaERuTSGPH1RdztfYnN4tnb29nbGUuY29t
218
+```
164
 
219
 
220
+or
165
 
221
 
166
-## FakeTLS
222
+```console
223
+$ mtg generate-secret --hex google.com
224
+ee473ce5d4958eb5f968c87680a23854a0676f6f676c652e636f6d
225
+```
167
 
226
 
168
-If you run this a proxy in faketls mode, this proxy will try to hide
169
-itself cloaking a host provided as a part of the generated secret. It
170
-means that if you cloak google.com then you can curl this proxy and
171
-you'll get a google.com response back.
227
+This secret is a keystone for a proxy and your password for a client.
228
+You need to keep it secured.
172
 
229
 
173
-mtg proxies L3 traffic. In other words, only TCP, without interfering in
174
-TLS, HTTP or any other high-level protocol.
230
+We recommend choosing a hostname wisely. Here we have a _google.com_
231
+but in reality, all providers can easily detect that this is not a
232
+Google. Google has a list of networks it officially uses and your IP
233
+address won't probably belong to it. It is a great idea to hide behind
234
+some domain that has some relation to this IP address.
175
 
235
 
236
+For example, you've bought a VPS from [Digital
237
+Ocean](https://www.digitalocean.com/). Then it might be a good idea to
238
+generate a secret for _digitalocean.com_ then.
176
 
239
 
177
-## Environment variables
240
+### Prepare a configuration file
178
 
241
 
179
-It is possible to configure this tool using environment variables. You
180
-can configure any flag but not secret or adtag. Here is the list of
181
-supported environment variables:
242
+Please checkout an example configuration file. All options except of
243
+`secret` and `bind-to` are optional. You can safely have this minimal
244
+configuration file:
182
 
245
 
183
-| Environment variable          | Corresponding flags          | Default value                     | Description                                                                                                                                                                                                                                                                     |
184
-|-------------------------------|------------------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
185
-| `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.                                                                                                                   |
186
-| `MTG_VERBOSE`                 | `-v`, `--verbose`            | `false`                           | Run in verbose mode. This is way less chatty than debug mode.                                                                                                                                                                                                                   |
187
-| `MTG_BIND`                    | `-b`, `--bind`               | `0.0.0.0:3128`                    | Which host/port pair should we bind to (listen on).                                                                                                                                                                                                                             |
188
-| `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. |
189
-| `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. |
190
-| `MTG_STATS_BIND`              | `-t`, `--stats-bind`         | `127.0.0.1:3129`                  | Which hist:port should we bind the internal statistics HTTP server (Prometheus).                                                                                                                                                                                                |
191
-| `MTG_STATS_NAMESPACE`         | `--stats-namespace`          | `mtg`                             | Which namespace should be used for prometheus metrics.                                                                                                                                                                                                                          |
192
-| `MTG_STATSD_ADDR`             | `--statsd-addr`              |                                   | host:port of statsd service. No defaults, by default we do not send anything there.                                                                                                                                                                                             |
193
-| `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`.                                                                                                                                         |
194
-| `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`.                                                   |
195
-| `MTG_STATSD_TAGS`             | `--statsd-tags`              |                                   | Which tags should we send to statsd with our metrics. Please specify them as `key=value` pairs.                                                                                                                                                                                 |
196
-| `MTG_BUFFER_WRITE`            | `-w`, `--write-buffer`       | `32KB`                            | The size of TCP write buffer in bytes. Write buffer is the buffer for messages which are going from client to Telegram.                                                                                                                                                         |
197
-| `MTG_BUFFER_READ`             | `-r`, `--read-buffer`        | `32KB`                            | The size of TCP read buffer in bytes. Read buffer is the buffer for messages from Telegram to client.                                                                                                                                                                           |
198
-| `MTG_ANTIREPLAY_MAXSIZE`      | `--anti-replay-max-size`     | `128MB`                           | Max size of antireplay cache.                                                                                                                                                                                                                                                   |
199
-| `MTG_CLOAK_PORT`              | `--cloak-port`               | `443`                             | Which port we should use to connect to cloaked host in FakeTLS mode.                                                                                                                                                                                                            |
200
-| `MTG_MULTIPLEX_PERCONNECTION` | `--multiplex-per-connection` | `50`                              | How many client connections can share a single Telegram connection in adtag mode                                                                                                                                                                                                |
201
-| `MTG_NTP_SERVERS`             | `--ntp-server`               | default pool                      | A list of NTP servers to use.                                                                                                                                                                                                                                                   |
202
-| `MTG_PREFER_DIRECT_IP`        | `--prefer-ip`                | `ipv6`                            | Which IP protocol to prefer if possible. Works mostly in direct mode.                                                                                                                                                                                                           |
246
+```toml
247
+secret = "ee473ce5d4958eb5f968c87680a23854a0676f6f676c652e636f6d"
248
+bind-to = "0.0.0.0:443"
249
+```
203
 
250
 
251
+This is enough to run the whole application. All other
252
+options already have sensible defaults for the app at almost any scale.
204
 
253
 
205
-Usually you want to modify only read/write buffer sizes. If you feel
206
-that proxy is slow, try to increase both sizes giving more priority to
207
-read buffer.
254
+Oh, the configuration is done in [TOML format](https://toml.io/en/).
208
 
255
 
209
-Unfortunately, MTPROTO proxy protocol does not allow us to use splice
210
-or any other neat tricks how to eliminate the need of copying data into
211
-userspace.
256
+### Run a proxy
212
 
257
 
213
-# How to run the tool
258
+Put a binary and a config into your webserver. Just for example,
259
+a binary goes to `/usr/local/bin/mtg` and configuration to `/etc/mtg.toml`.
214
 
260
 
215
-Now run the tool:
261
+Now you can create a systemd unit:
216
 
262
 
217
 ```console
263
 ```console
218
-$ mtg run <secret>
264
+$ cat /etc/systemd/system/mtg.service
265
+[Unit]
266
+Description=mtg
267
+
268
+[Service]
269
+ExecStart=/usr/local/bin/mtg run /etc/mtg.toml
270
+Restart=always
271
+RestartSec=3
272
+
273
+[Install]
274
+WantedBy=multi-user.target
275
+$ sudo systemctl daemon-reload
276
+$ sudo systemctl enable mtg
277
+$ sudo systemctl start mtg
219
 ```
278
 ```
220
 
279
 
221
-How to run the tool with ADTag:
280
+or you can run a docker image
222
 
281
 
223
 ```console
282
 ```console
224
-$ mtg run <secret> <adtag>
283
+docker run -d -v /etc/mtg.toml:/config.toml -p 443:3128 --restart=unless-stopped nineseconds/mtg:2
225
 ```
284
 ```
226
 
285
 
227
-This tool will listen on port 3128 by default with the given secret.
286
+where _443_ is a host port (a port you want to connect to from a
287
+client), and _3128_ is the one you have in your config in the `bind-to`
288
+section.
228
 
289
 
290
+### Access a proxy
229
 
291
 
230
-# oneliner to run this proxy
231
-
232
-Please ensure that docker is installed. After that just execute
292
+Now you can generate some useful links:
233
 
293
 
234
 ```console
294
 ```console
235
-curl -sfL --compressed https://raw.githubusercontent.com/9seconds/mtg/master/run.sh | bash
295
+$ mtg access /etc/mtg.toml
296
+{
297
+  "ipv4": {
298
+    "ip": "x.y.z.a",
299
+    "port": 3128,
300
+    "tg_url": "tg://proxy?...",
301
+    "tg_qrcode": "https://api.qrserver.com/v1/create-qr-code?data...",
302
+    "tme_url": "https://t.me/proxy?...",
303
+    "tme_qrcode": "https://api.qrserver.com/v1/create-qr-code?data..."
304
+  },
305
+  "secret": {
306
+    "hex": "...",
307
+    "base64": "..."
308
+  }
309
+}
236
 ```
310
 ```
237
 
311
 
238
-
239
-# statsd integration
240
-
241
-mtg provides an integration with statsd, you can enable it with command
242
-line interface. To enable it, you have to provide IP address of statsd
243
-service.
244
-
245
-Out of the box, mtg supports 2 additional dialects: [InfluxDB](https://www.influxdata.com/blog/getting-started-with-sending-statsd-metrics-to-telegraf-influxdb/)
246
-and [Datadog](https://docs.datadoghq.com/developers/dogstatsd/).
247
-
248
-All metrics are gauges. Here is the list of metrics and their meaning:
249
-
250
-| Metric name            | Unit    | Description                                |
251
-|------------------------|---------|--------------------------------------------|
252
-| `connections`          | number  | The number of active connections.          |
253
-| `telegram_connections` | number  | The number of active telegram connections. |
254
-| `crashes`              | number  | An amount of crashes in client handlers.   |
255
-| `traffic.egress`       | bytes   | Traffic from the start of application.     |
256
-| `replay_attacks`       | number  | The number of prevented replay attacks.    |
257
-
258
-All metrics are prefixed with given prefix. Default prefix is `mtg`.
259
-Also, metrics provide tags (ipv4/ipv6, dc indexes etc).
260
-
261
-
262
-# Prometheus integration
263
-
264
-[Prometheus](https://prometheus.io) integration comes out of
265
-the box, you do not need to setup anything special.
266
-
267
-
268
-# Upgrade to 1.0
269
-
270
-Version 1.0 breaks compatibility with previous versions so please read
271
-this chapter carefully:
272
-
273
-1. mtg now uses subcommands. Please use `mtg run` instead of just
274
-   `mtg` to run a proxy.
275
-2. Options which set host and port separately were removed in a
276
-   favor of fused `host:port` options.
277
-3. Own stats server was removed. Prometheus endpoint is moved to
278
-   default stats endpoint.
279
-4. It is possible to connect to this proxy only with a secret which
280
-   was used to run it. So, no backward compatibility of clients.
281
-5. Multiplexing involves connectivity with middle proxies and involves
282
-   the most complex code path of this proxy. To avoid potential bugs,
283
-   we still recommend using direct mode.
312
+## Metrics
313
+
314
+Out of the box, mtg works with
315
+[statsd](https://github.com/statsd/statsd) and
316
+[Prometheus](https://prometheus.io/). Please check configuration file
317
+example to get how to set this integration up.
318
+
319
+Here goes a list of metrics with their types but without a prefix.
320
+
321
+| Name                        | Type    | Tags                             | Description                                                                                |
322
+|-----------------------------|---------|----------------------------------|--------------------------------------------------------------------------------------------|
323
+| client_connections          | gauge   | `ip_family`                      | Count of processing client connections.                                                    |
324
+| telegram_connections        | gauge   | `telegram_ip`, `dc`              | Count of connections to Telegram servers.                                                  |
325
+| domain_fronting_connections | gauge   | `ip_family`                      | Count of connections to fronting domain.                                                   |
326
+| telegram_traffic            | counter | `telegram_ip`, `dc`, `direction` | Count of bytes, transmitted to/from Telegram.                                              |
327
+| domain_fronting_traffic     | counter | `direction`                      | Count of bytes, transmitted to/from fronting domain.                                       |
328
+| domain_fronting             | counter | –                                | Count of domain fronting events.                                                           |
329
+| concurrency_limited         | counter | –                                | Count of events, when client connection was rejected due to concurrency limit.             |
330
+| ip_blocklisted              | counter | –                                | Count of events when client connection was rejected because IP was found in the blacklist. |
331
+| replay_attacks              | counter | –                                | Count of detected replay attacks.                                                          |
332
+
333
+Tag meaning:
334
+
335
+| Name        | Values                 | Description                                   |
336
+|-------------|------------------------|-----------------------------------------------|
337
+| ip_family   | ipv4, ipv6             | A version of the IP protocol.                 |
338
+| dc          |                        | A number of the Telegram DC for a connection. |
339
+| telegram_ip |                        | IP address of the Telegram server.            |
340
+| direction   | to_client, from_client | A direction of the traffic flow.              |

+ 0
- 36
antireplay/cache.go Просмотреть файл

1
-package antireplay
2
-
3
-import "github.com/VictoriaMetrics/fastcache"
4
-
5
-var (
6
-	prefixObfuscated2 = []byte{0x00}
7
-	prefixTLS         = []byte{0x01}
8
-)
9
-
10
-type cache struct {
11
-	data *fastcache.Cache
12
-}
13
-
14
-func (c cache) AddObfuscated2(data []byte) {
15
-	c.data.Set(keyObfuscated2(data), nil)
16
-}
17
-
18
-func (c cache) AddTLS(data []byte) {
19
-	c.data.Set(keyTLS(data), nil)
20
-}
21
-
22
-func (c cache) HasObfuscated2(data []byte) bool {
23
-	return c.data.Has(keyObfuscated2(data))
24
-}
25
-
26
-func (c cache) HasTLS(data []byte) bool {
27
-	return c.data.Has(keyTLS(data))
28
-}
29
-
30
-func keyObfuscated2(data []byte) []byte {
31
-	return append(prefixObfuscated2, data...)
32
-}
33
-
34
-func keyTLS(data []byte) []byte {
35
-	return append(prefixTLS, data...)
36
-}

+ 13
- 26
antireplay/init.go Просмотреть файл

1
+// Antireplay package has cache implementations that are effective
2
+// against replay attacks.
3
+//
4
+// To understand more about replay attacks, please read documentation
5
+// for mtglib.AntiReplayCache interface. This package has a list of some
6
+// implementations of this interface.
1
 package antireplay
7
 package antireplay
2
 
8
 
3
-import (
4
-	"sync"
9
+const (
10
+	// DefaultStableBloomFilterMaxSize is a recommended byte size for a
11
+	// stable bloom filter.
12
+	DefaultStableBloomFilterMaxSize = 1024 * 1024 // 1MiB
5
 
13
 
6
-	"github.com/9seconds/mtg/config"
7
-	"github.com/VictoriaMetrics/fastcache"
14
+	// DefaultStableBloomFilterErrorRate is a recommended default error
15
+	// rate for a stable bloom filter.
16
+	DefaultStableBloomFilterErrorRate = 0.001
8
 )
17
 )
9
-
10
-type CacheInterface interface {
11
-	AddObfuscated2([]byte)
12
-	AddTLS([]byte)
13
-	HasObfuscated2([]byte) bool
14
-	HasTLS([]byte) bool
15
-}
16
-
17
-var (
18
-	Cache    CacheInterface
19
-	initOnce sync.Once
20
-)
21
-
22
-func Init() {
23
-	initOnce.Do(func() {
24
-		if config.C.AntiReplayMaxSize == 0 {
25
-			Cache = nilCache{}
26
-		} else {
27
-			Cache = cache{fastcache.New(config.C.AntiReplayMaxSize)}
28
-		}
29
-	})
30
-}

+ 0
- 8
antireplay/nilcache.go Просмотреть файл

1
-package antireplay
2
-
3
-type nilCache struct{}
4
-
5
-func (n nilCache) AddObfuscated2(_ []byte)      {}
6
-func (n nilCache) AddTLS(_ []byte)              {}
7
-func (n nilCache) HasObfuscated2(_ []byte) bool { return false }
8
-func (n nilCache) HasTLS(_ []byte) bool         { return false }

+ 14
- 0
antireplay/noop.go Просмотреть файл

1
+package antireplay
2
+
3
+import "github.com/9seconds/mtg/v2/mtglib"
4
+
5
+type noop struct{}
6
+
7
+func (n noop) SeenBefore(_ []byte) bool { return false }
8
+
9
+// NewNoop returns an implementation that does nothing. A corresponding
10
+// method always returns false, so this cache accepts everything you
11
+// pass to it.
12
+func NewNoop() mtglib.AntiReplayCache {
13
+	return noop{}
14
+}

+ 26
- 0
antireplay/noop_test.go Просмотреть файл

1
+package antireplay_test
2
+
3
+import (
4
+	"testing"
5
+
6
+	"github.com/9seconds/mtg/v2/antireplay"
7
+	"github.com/stretchr/testify/suite"
8
+)
9
+
10
+type NoopTestSuite struct {
11
+	suite.Suite
12
+}
13
+
14
+func (suite *NoopTestSuite) TestOp() {
15
+	filter := antireplay.NewNoop()
16
+
17
+	suite.False(filter.SeenBefore([]byte{1, 2, 3}))
18
+	suite.False(filter.SeenBefore([]byte{4, 5, 6}))
19
+	suite.False(filter.SeenBefore([]byte{1, 2, 3}))
20
+	suite.False(filter.SeenBefore([]byte{4, 5, 6}))
21
+}
22
+
23
+func TestNoop(t *testing.T) {
24
+	t.Parallel()
25
+	suite.Run(t, &NoopTestSuite{})
26
+}

+ 51
- 0
antireplay/stable_bloom_filter.go Просмотреть файл

1
+package antireplay
2
+
3
+import (
4
+	"sync"
5
+
6
+	"github.com/9seconds/mtg/v2/mtglib"
7
+	"github.com/OneOfOne/xxhash"
8
+	boom "github.com/tylertreat/BoomFilters"
9
+)
10
+
11
+type stableBloomFilter struct {
12
+	filter boom.StableBloomFilter
13
+	mutex  sync.Mutex
14
+}
15
+
16
+func (s *stableBloomFilter) SeenBefore(digest []byte) bool {
17
+	s.mutex.Lock()
18
+	defer s.mutex.Unlock()
19
+
20
+	return s.filter.TestAndAdd(digest)
21
+}
22
+
23
+// NewStableBloomFilter returns an implementation of AntiReplayCache
24
+// based on stable bloom filter.
25
+//
26
+// http://webdocs.cs.ualberta.ca/~drafiei/papers/DupDet06Sigmod.pdf
27
+//
28
+// The basic idea of a stable bloom filter is quite simple: each time
29
+// when you set a new element, you randomly reset P elements. There is a
30
+// hardcore math which proves that if you choose this P correctly, you
31
+// can maintain the same error rate for a stream of elements.
32
+//
33
+// byteSize is the number of bytes you want to give to a bloom filter.
34
+// errorRate is desired false-positive error rate. If you want to use
35
+// default values, please pass 0 for byteSize and <0 for errorRate.
36
+func NewStableBloomFilter(byteSize uint, errorRate float64) mtglib.AntiReplayCache {
37
+	if byteSize == 0 {
38
+		byteSize = DefaultStableBloomFilterMaxSize
39
+	}
40
+
41
+	if errorRate < 0 {
42
+		errorRate = DefaultStableBloomFilterErrorRate
43
+	}
44
+
45
+	sf := boom.NewDefaultStableBloomFilter(byteSize*8, errorRate) // nolint: gomnd
46
+	sf.SetHash(xxhash.New64())
47
+
48
+	return &stableBloomFilter{
49
+		filter: *sf,
50
+	}
51
+}

+ 26
- 0
antireplay/stable_bloom_filter_test.go Просмотреть файл

1
+package antireplay_test
2
+
3
+import (
4
+	"testing"
5
+
6
+	"github.com/9seconds/mtg/v2/antireplay"
7
+	"github.com/stretchr/testify/suite"
8
+)
9
+
10
+type StableBloomFilterTestSuite struct {
11
+	suite.Suite
12
+}
13
+
14
+func (suite *StableBloomFilterTestSuite) TestOp() {
15
+	filter := antireplay.NewStableBloomFilter(500, 0.001)
16
+
17
+	suite.False(filter.SeenBefore([]byte{1, 2, 3}))
18
+	suite.False(filter.SeenBefore([]byte{4, 5, 6}))
19
+	suite.True(filter.SeenBefore([]byte{1, 2, 3}))
20
+	suite.True(filter.SeenBefore([]byte{4, 5, 6}))
21
+}
22
+
23
+func TestStableBloomFilter(t *testing.T) {
24
+	t.Parallel()
25
+	suite.Run(t, &StableBloomFilterTestSuite{})
26
+}

+ 0
- 26
cli/generate.go Просмотреть файл

1
-package cli
2
-
3
-import (
4
-	"crypto/rand"
5
-	"encoding/hex"
6
-
7
-	"github.com/9seconds/mtg/config"
8
-)
9
-
10
-func Generate(secretType, hostname string) {
11
-	data := make([]byte, config.SimpleSecretLength)
12
-	if _, err := rand.Read(data); err != nil {
13
-		panic(err)
14
-	}
15
-
16
-	secret := hex.EncodeToString(data)
17
-
18
-	switch secretType {
19
-	case "simple":
20
-		PrintStdout(secret)
21
-	case "secured":
22
-		PrintStdout("dd" + secret)
23
-	default:
24
-		PrintStdout("ee" + secret + hex.EncodeToString([]byte(hostname)))
25
-	}
26
-}

+ 0
- 101
cli/proxy.go Просмотреть файл

1
-package cli
2
-
3
-import (
4
-	"net"
5
-	"os"
6
-	"time"
7
-
8
-	"github.com/9seconds/mtg/antireplay"
9
-	"github.com/9seconds/mtg/config"
10
-	"github.com/9seconds/mtg/faketls"
11
-	"github.com/9seconds/mtg/hub"
12
-	"github.com/9seconds/mtg/ntp"
13
-	"github.com/9seconds/mtg/obfuscated2"
14
-	"github.com/9seconds/mtg/proxy"
15
-	"github.com/9seconds/mtg/stats"
16
-	"github.com/9seconds/mtg/telegram"
17
-	"github.com/9seconds/mtg/utils"
18
-	"go.uber.org/zap"
19
-	"go.uber.org/zap/zapcore"
20
-)
21
-
22
-func Proxy() error { // nolint: funlen,cyclop
23
-	ctx := utils.GetSignalContext()
24
-
25
-	atom := zap.NewAtomicLevel()
26
-
27
-	switch {
28
-	case config.C.Debug:
29
-		atom.SetLevel(zapcore.DebugLevel)
30
-	case config.C.Verbose:
31
-		atom.SetLevel(zapcore.InfoLevel)
32
-	default:
33
-		atom.SetLevel(zapcore.ErrorLevel)
34
-	}
35
-
36
-	encoderCfg := zap.NewProductionEncoderConfig()
37
-	logger := zap.New(zapcore.NewCore(
38
-		zapcore.NewJSONEncoder(encoderCfg),
39
-		zapcore.Lock(os.Stderr),
40
-		atom,
41
-	))
42
-
43
-	zap.ReplaceGlobals(logger)
44
-	defer logger.Sync() // nolint: errcheck
45
-
46
-	if err := config.InitPublicAddress(ctx); err != nil {
47
-		Fatal(err)
48
-	}
49
-
50
-	zap.S().Debugw("Configuration", "config", config.Printable())
51
-
52
-	if config.C.MiddleProxyMode() {
53
-		zap.S().Infow("Use middle proxy connection to Telegram")
54
-
55
-		diff, err := ntp.Fetch()
56
-		if err != nil {
57
-			Fatal("Cannot fetch time data from NTP")
58
-		}
59
-
60
-		if diff > time.Second {
61
-			Fatal("Your local time is skewed and drift is bigger than a second. Please sync your time.")
62
-		}
63
-
64
-		go ntp.AutoUpdate()
65
-	} else {
66
-		zap.S().Infow("Use direct connection to Telegram")
67
-	}
68
-
69
-	PrintJSONStdout(config.GetURLs())
70
-
71
-	if err := stats.Init(ctx); err != nil {
72
-		Fatal(err)
73
-	}
74
-
75
-	antireplay.Init()
76
-	telegram.Init()
77
-	hub.Init(ctx)
78
-
79
-	proxyListener, err := net.Listen("tcp", config.C.Bind.String())
80
-	if err != nil {
81
-		Fatal(err)
82
-	}
83
-
84
-	go func() {
85
-		<-ctx.Done()
86
-		proxyListener.Close()
87
-	}()
88
-
89
-	app := &proxy.Proxy{
90
-		Logger:              zap.S().Named("proxy"),
91
-		Context:             ctx,
92
-		ClientProtocolMaker: obfuscated2.MakeClientProtocol,
93
-	}
94
-	if config.C.SecretMode == config.SecretModeTLS {
95
-		app.ClientProtocolMaker = faketls.MakeClientProtocol
96
-	}
97
-
98
-	app.Serve(proxyListener)
99
-
100
-	return nil
101
-}

+ 0
- 43
cli/utils.go Просмотреть файл

1
-package cli
2
-
3
-import (
4
-	"encoding/json"
5
-	"fmt"
6
-	"io"
7
-	"os"
8
-)
9
-
10
-func Fatal(arg interface{}) {
11
-	if value, ok := arg.(error); ok {
12
-		arg = fmt.Errorf("fatal error: %+v", value) // nolint: errorlint
13
-	}
14
-
15
-	PrintStderr(arg)
16
-	os.Exit(1)
17
-}
18
-
19
-func PrintStderr(args ...interface{}) {
20
-	fmt.Fprintln(os.Stderr, args...)
21
-}
22
-
23
-func PrintStdout(args ...interface{}) {
24
-	fmt.Println(args...) // nolint: forbidigo
25
-}
26
-
27
-func PrintJSONStderr(data interface{}) {
28
-	printJSON(os.Stderr, data)
29
-}
30
-
31
-func PrintJSONStdout(data interface{}) {
32
-	printJSON(os.Stdout, data)
33
-}
34
-
35
-func printJSON(writer io.Writer, data interface{}) {
36
-	encoder := json.NewEncoder(writer)
37
-	encoder.SetEscapeHTML(false)
38
-	encoder.SetIndent("", "  ")
39
-
40
-	if err := encoder.Encode(data); err != nil {
41
-		panic(err)
42
-	}
43
-}

+ 0
- 312
config/config.go Просмотреть файл

1
-package config
2
-
3
-import (
4
-	"bytes"
5
-	"context"
6
-	"encoding/json"
7
-	"errors"
8
-	"fmt"
9
-	"math"
10
-	"net"
11
-
12
-	"github.com/alecthomas/units"
13
-	statsd "github.com/smira/go-statsd"
14
-	"go.uber.org/zap"
15
-)
16
-
17
-type SecretMode uint8
18
-
19
-func (s SecretMode) String() string {
20
-	switch s {
21
-	case SecretModeSimple:
22
-		return "simple"
23
-	case SecretModeSecured:
24
-		return "secured"
25
-	case SecretModeTLS:
26
-		return "tls"
27
-	}
28
-
29
-	return "tls"
30
-}
31
-
32
-const (
33
-	SecretModeSimple SecretMode = iota
34
-	SecretModeSecured
35
-	SecretModeTLS
36
-)
37
-
38
-type PreferIP uint8
39
-
40
-const (
41
-	PreferIPv4 PreferIP = iota
42
-	PreferIPv6
43
-)
44
-
45
-const SimpleSecretLength = 16
46
-
47
-type OptionType uint8
48
-
49
-const (
50
-	OptionTypeDebug OptionType = iota
51
-	OptionTypeVerbose
52
-
53
-	OptionTypePreferIP
54
-
55
-	OptionTypeBind
56
-	OptionTypePublicIPv4
57
-	OptionTypePublicIPv6
58
-
59
-	OptionTypeStatsBind
60
-	OptionTypeStatsNamespace
61
-	OptionTypeStatsdAddress
62
-	OptionTypeStatsdTagsFormat
63
-	OptionTypeStatsdTags
64
-
65
-	OptionTypeWriteBufferSize
66
-	OptionTypeReadBufferSize
67
-
68
-	OptionTypeCloakPort
69
-
70
-	OptionTypeAntiReplayMaxSize
71
-
72
-	OptionTypeMultiplexPerConnection
73
-
74
-	OptionTypeNTPServers
75
-
76
-	OptionTypeSecret
77
-	OptionTypeAdtag
78
-)
79
-
80
-type Config struct {
81
-	Bind             *net.TCPAddr      `json:"bind"`
82
-	PublicIPv4       *net.TCPAddr      `json:"public_ipv4"`
83
-	PublicIPv6       *net.TCPAddr      `json:"public_ipv6"`
84
-	StatsBind        *net.TCPAddr      `json:"stats_bind"`
85
-	StatsdAddr       *net.TCPAddr      `json:"stats_addr"`
86
-	StatsdTagsFormat *statsd.TagFormat `json:"statsd_tags_format"`
87
-
88
-	StatsNamespace string            `json:"stats_namespace"`
89
-	CloakHost      string            `json:"cloak_host"`
90
-	StatsdTags     map[string]string `json:"statsd_tags"`
91
-
92
-	WriteBuffer int `json:"write_buffer"`
93
-	ReadBuffer  int `json:"read_buffer"`
94
-	CloakPort   int `json:"cloak_port"`
95
-
96
-	AntiReplayMaxSize int `json:"anti_replay_max_size"`
97
-
98
-	MultiplexPerConnection int `json:"multiplex_per_connection"`
99
-
100
-	Debug      bool       `json:"debug"`
101
-	Verbose    bool       `json:"verbose"`
102
-	SecretMode SecretMode `json:"secret_mode"`
103
-	PreferIP   PreferIP   `json:"prefer_ip"`
104
-	NTPServers []string   `json:"ntp_servers"`
105
-
106
-	Secret []byte `json:"secret"`
107
-	AdTag  []byte `json:"adtag"`
108
-}
109
-
110
-func (c *Config) ClientReadBuffer() int {
111
-	return c.ReadBuffer
112
-}
113
-
114
-func (c *Config) ClientWriteBuffer() int {
115
-	return c.WriteBuffer
116
-}
117
-
118
-func (c *Config) MiddleProxyMode() bool {
119
-	return len(c.AdTag) > 0
120
-}
121
-
122
-func (c *Config) ProxyReadBuffer() int {
123
-	value := c.ReadBuffer
124
-
125
-	if c.MiddleProxyMode() {
126
-		value = c.adjustProxyValue(value)
127
-	}
128
-
129
-	return value
130
-}
131
-
132
-func (c *Config) ProxyWriteBuffer() int {
133
-	value := c.WriteBuffer
134
-
135
-	if c.MiddleProxyMode() {
136
-		value = c.adjustProxyValue(value)
137
-	}
138
-
139
-	return value
140
-}
141
-
142
-func (c *Config) adjustProxyValue(value int) int {
143
-	if c.MultiplexPerConnection == 0 {
144
-		return value
145
-	}
146
-
147
-	fvalue := float64(value)
148
-
149
-	newValue := fvalue * 2 * math.Log(float64(c.MultiplexPerConnection))
150
-	newValue = math.Ceil(newValue)
151
-	newValue = math.Max(fvalue, newValue)
152
-
153
-	return int(newValue)
154
-}
155
-
156
-type Opt struct {
157
-	Option OptionType
158
-	Value  interface{}
159
-}
160
-
161
-var C = Config{}
162
-
163
-func Init(options ...Opt) error { // nolint: gocyclo, funlen, cyclop
164
-	for _, opt := range options {
165
-		switch opt.Option {
166
-		case OptionTypeDebug:
167
-			C.Debug = opt.Value.(bool)
168
-		case OptionTypeVerbose:
169
-			C.Verbose = opt.Value.(bool)
170
-		case OptionTypePreferIP:
171
-			value := opt.Value.(string)
172
-			switch value {
173
-			case "ipv4":
174
-				C.PreferIP = PreferIPv4
175
-			case "ipv6":
176
-				C.PreferIP = PreferIPv6
177
-			default:
178
-				return fmt.Errorf("incorrect direct IP mode %s", value)
179
-			}
180
-		case OptionTypeBind:
181
-			C.Bind = opt.Value.(*net.TCPAddr)
182
-		case OptionTypePublicIPv4:
183
-			C.PublicIPv4 = opt.Value.(*net.TCPAddr)
184
-			if C.PublicIPv4 == nil {
185
-				C.PublicIPv4 = &net.TCPAddr{}
186
-			}
187
-		case OptionTypePublicIPv6:
188
-			C.PublicIPv6 = opt.Value.(*net.TCPAddr)
189
-			if C.PublicIPv6 == nil {
190
-				C.PublicIPv6 = &net.TCPAddr{}
191
-			}
192
-		case OptionTypeStatsBind:
193
-			C.StatsBind = opt.Value.(*net.TCPAddr)
194
-		case OptionTypeStatsNamespace:
195
-			C.StatsNamespace = opt.Value.(string)
196
-		case OptionTypeStatsdAddress:
197
-			C.StatsdAddr = opt.Value.(*net.TCPAddr)
198
-		case OptionTypeStatsdTagsFormat:
199
-			value := opt.Value.(string)
200
-			switch value {
201
-			case "datadog":
202
-				C.StatsdTagsFormat = statsd.TagFormatDatadog
203
-			case "influxdb":
204
-				C.StatsdTagsFormat = statsd.TagFormatInfluxDB
205
-			default:
206
-				return fmt.Errorf("incorrect statsd tag %s", value)
207
-			}
208
-		case OptionTypeStatsdTags:
209
-			C.StatsdTags = opt.Value.(map[string]string)
210
-		case OptionTypeWriteBufferSize:
211
-			C.WriteBuffer = int(opt.Value.(units.Base2Bytes))
212
-		case OptionTypeReadBufferSize:
213
-			C.ReadBuffer = int(opt.Value.(units.Base2Bytes))
214
-		case OptionTypeCloakPort:
215
-			C.CloakPort = int(opt.Value.(uint16))
216
-		case OptionTypeAntiReplayMaxSize:
217
-			C.AntiReplayMaxSize = int(opt.Value.(units.Base2Bytes))
218
-		case OptionTypeMultiplexPerConnection:
219
-			C.MultiplexPerConnection = int(opt.Value.(uint))
220
-		case OptionTypeNTPServers:
221
-			C.NTPServers = opt.Value.([]string)
222
-			if len(C.NTPServers) == 0 {
223
-				return errors.New("ntp server list is empty")
224
-			}
225
-		case OptionTypeSecret:
226
-			C.Secret = opt.Value.([]byte)
227
-		case OptionTypeAdtag:
228
-			C.AdTag = opt.Value.([]byte)
229
-		default:
230
-			return fmt.Errorf("unknown tag %v", opt.Option)
231
-		}
232
-	}
233
-
234
-	switch {
235
-	case len(C.Secret) == 1+SimpleSecretLength && bytes.HasPrefix(C.Secret, []byte{0xdd}):
236
-		C.SecretMode = SecretModeSecured
237
-		C.Secret = bytes.TrimPrefix(C.Secret, []byte{0xdd})
238
-	case len(C.Secret) > SimpleSecretLength && bytes.HasPrefix(C.Secret, []byte{0xee}):
239
-		C.SecretMode = SecretModeTLS
240
-		secret := bytes.TrimPrefix(C.Secret, []byte{0xee})
241
-		C.Secret = secret[:SimpleSecretLength]
242
-		C.CloakHost = string(secret[SimpleSecretLength:])
243
-	case len(C.Secret) == SimpleSecretLength:
244
-		C.SecretMode = SecretModeSimple
245
-	default:
246
-		return errors.New("incorrect secret")
247
-	}
248
-
249
-	if C.MultiplexPerConnection == 0 {
250
-		return errors.New("cannot use 0 clients per connection for multiplexing")
251
-	}
252
-
253
-	if C.CloakHost != "" {
254
-		if _, err := net.LookupHost(C.CloakHost); err != nil {
255
-			zap.S().Warnw("Cannot resolve address of host", "hostname", C.CloakHost, "error", err)
256
-		}
257
-	}
258
-
259
-	return nil
260
-}
261
-
262
-func InitPublicAddress(ctx context.Context) error {
263
-	if C.PublicIPv4.Port == 0 {
264
-		C.PublicIPv4.Port = C.Bind.Port
265
-	}
266
-
267
-	if C.PublicIPv6.Port == 0 {
268
-		C.PublicIPv6.Port = C.Bind.Port
269
-	}
270
-
271
-	foundAddress := C.PublicIPv4.IP != nil || C.PublicIPv6.IP != nil
272
-
273
-	if C.PublicIPv4.IP == nil {
274
-		ip, err := getGlobalIPv4(ctx)
275
-		if err != nil {
276
-			zap.S().Warnw("Cannot resolve public address", "error", err)
277
-		} else {
278
-			C.PublicIPv4.IP = ip
279
-			foundAddress = true
280
-		}
281
-	}
282
-
283
-	if C.PublicIPv6.IP == nil {
284
-		ip, err := getGlobalIPv6(ctx)
285
-		if err != nil {
286
-			zap.S().Warnw("Cannot resolve public address", "error", err)
287
-		} else {
288
-			C.PublicIPv6.IP = ip
289
-			foundAddress = true
290
-		}
291
-	}
292
-
293
-	if !foundAddress {
294
-		return errors.New("cannot resolve any public address")
295
-	}
296
-
297
-	return nil
298
-}
299
-
300
-func Printable() interface{} {
301
-	data, err := json.Marshal(C)
302
-	if err != nil {
303
-		panic(err)
304
-	}
305
-
306
-	rv := map[string]interface{}{}
307
-	if err := json.Unmarshal(data, &rv); err != nil {
308
-		panic(err)
309
-	}
310
-
311
-	return rv
312
-}

+ 0
- 78
config/global_ips.go Просмотреть файл

1
-package config
2
-
3
-import (
4
-	"context"
5
-	"fmt"
6
-	"io"
7
-	"io/ioutil"
8
-	"net"
9
-	"net/http"
10
-	"strings"
11
-	"time"
12
-)
13
-
14
-const (
15
-	ifconfigAddress = "https://ifconfig.co/ip"
16
-	ifconfigTimeout = 10 * time.Second
17
-)
18
-
19
-func getGlobalIPv4(ctx context.Context) (net.IP, error) {
20
-	ip, err := fetchIP(ctx, "tcp4")
21
-	if err != nil || ip.To4() == nil {
22
-		return nil, fmt.Errorf("cannot find public ipv4 address: %w", err)
23
-	}
24
-
25
-	return ip, nil
26
-}
27
-
28
-func getGlobalIPv6(ctx context.Context) (net.IP, error) {
29
-	ip, err := fetchIP(ctx, "tcp6")
30
-	if err != nil || ip.To4() != nil {
31
-		return nil, fmt.Errorf("cannot find public ipv6 address: %w", err)
32
-	}
33
-
34
-	return ip, nil
35
-}
36
-
37
-func fetchIP(ctx context.Context, network string) (net.IP, error) {
38
-	dialer := &net.Dialer{FallbackDelay: -1}
39
-	client := &http.Client{
40
-		Jar:     nil,
41
-		Timeout: ifconfigTimeout,
42
-		Transport: &http.Transport{
43
-			DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) {
44
-				return dialer.DialContext(ctx, network, addr)
45
-			},
46
-		},
47
-	}
48
-
49
-	req, err := http.NewRequest("GET", ifconfigAddress, nil)
50
-	if err != nil {
51
-		return nil, fmt.Errorf("cannot create a request: %w", err)
52
-	}
53
-
54
-	resp, err := client.Do(req.WithContext(ctx))
55
-	if err != nil {
56
-		if resp != nil {
57
-			io.Copy(ioutil.Discard, resp.Body) // nolint: errcheck
58
-		}
59
-
60
-		return nil, fmt.Errorf("cannot perform a request: %w", err)
61
-	}
62
-
63
-	defer resp.Body.Close()
64
-
65
-	respDataBytes, err := ioutil.ReadAll(resp.Body)
66
-	if err != nil {
67
-		return nil, fmt.Errorf("cannot read response body: %w", err)
68
-	}
69
-
70
-	respData := strings.TrimSpace(string(respDataBytes))
71
-
72
-	ip := net.ParseIP(respData)
73
-	if ip == nil {
74
-		return nil, fmt.Errorf("ifconfig.co returns incorrect IP %s", respData)
75
-	}
76
-
77
-	return ip, nil
78
-}

+ 0
- 99
config/urls.go Просмотреть файл

1
-package config
2
-
3
-import (
4
-	"encoding/hex"
5
-	"net"
6
-	"net/url"
7
-	"strconv"
8
-)
9
-
10
-type URLs struct {
11
-	TG        string `json:"tg_url"`
12
-	TMe       string `json:"tme_url"`
13
-	TGQRCode  string `json:"tg_qrcode"`
14
-	TMeQRCode string `json:"tme_qrcode"`
15
-}
16
-
17
-type IPURLs struct {
18
-	IPv4      *URLs  `json:"ipv4,omitempty"`
19
-	IPv6      *URLs  `json:"ipv6,omitempty"`
20
-	BotSecret string `json:"secret_for_mtproxybot"`
21
-}
22
-
23
-func GetURLs() (urls IPURLs) {
24
-	secret := ""
25
-
26
-	switch C.SecretMode {
27
-	case SecretModeSimple:
28
-		secret = hex.EncodeToString(C.Secret)
29
-	case SecretModeSecured:
30
-		secret = "dd" + hex.EncodeToString(C.Secret)
31
-	case SecretModeTLS:
32
-		secret = "ee" + hex.EncodeToString(C.Secret) + hex.EncodeToString([]byte(C.CloakHost))
33
-	}
34
-
35
-	if C.PublicIPv4.IP != nil {
36
-		urls.IPv4 = makeURLs(C.PublicIPv4, secret)
37
-	}
38
-
39
-	if C.PublicIPv6.IP != nil {
40
-		urls.IPv6 = makeURLs(C.PublicIPv6, secret)
41
-	}
42
-
43
-	urls.BotSecret = hex.EncodeToString(C.Secret)
44
-
45
-	return urls
46
-}
47
-
48
-func makeURLs(addr *net.TCPAddr, secret string) *URLs {
49
-	urls := &URLs{}
50
-
51
-	values := url.Values{}
52
-	values.Set("server", addr.IP.String())
53
-	values.Set("port", strconv.Itoa(addr.Port))
54
-	values.Set("secret", secret)
55
-
56
-	return &URLs{
57
-		TG:        makeTGURL(values),
58
-		TMe:       makeTMeURL(values),
59
-		TGQRCode:  makeQRCodeURL(urls.TG),
60
-		TMeQRCode: makeQRCodeURL(urls.TMe),
61
-	}
62
-}
63
-
64
-func makeTGURL(values url.Values) string {
65
-	tgURL := url.URL{
66
-		Scheme:   "tg",
67
-		Host:     "proxy",
68
-		RawQuery: values.Encode(),
69
-	}
70
-
71
-	return tgURL.String()
72
-}
73
-
74
-func makeTMeURL(values url.Values) string {
75
-	tMeURL := url.URL{
76
-		Scheme:   "https",
77
-		Host:     "t.me",
78
-		Path:     "proxy",
79
-		RawQuery: values.Encode(),
80
-	}
81
-
82
-	return tMeURL.String()
83
-}
84
-
85
-func makeQRCodeURL(data string) string {
86
-	qr := url.URL{
87
-		Scheme: "https",
88
-		Host:   "api.qrserver.com",
89
-		Path:   "v1/create-qr-code",
90
-	}
91
-
92
-	values := url.Values{}
93
-	values.Set("qzone", "4")
94
-	values.Set("format", "svg")
95
-	values.Set("data", data)
96
-	qr.RawQuery = values.Encode()
97
-
98
-	return qr.String()
99
-}

+ 0
- 6
conntypes/acks.go Просмотреть файл

1
-package conntypes
2
-
3
-type ConnectionAcks struct {
4
-	Simple bool
5
-	Quick  bool
6
-}

+ 0
- 5
conntypes/dc.go Просмотреть файл

1
-package conntypes
2
-
3
-type DC int16
4
-
5
-const DCDefaultIdx DC = 1

+ 0
- 24
conntypes/id.go Просмотреть файл

1
-package conntypes
2
-
3
-import (
4
-	"crypto/rand"
5
-	"encoding/hex"
6
-)
7
-
8
-const ConnIDLength = 8
9
-
10
-type ConnID [ConnIDLength]byte
11
-
12
-func (c ConnID) String() string {
13
-	return hex.EncodeToString(c[:])
14
-}
15
-
16
-func NewConnID() ConnID {
17
-	var id ConnID
18
-
19
-	if _, err := rand.Read(id[:]); err != nil {
20
-		panic(err)
21
-	}
22
-
23
-	return id
24
-}

+ 0
- 3
conntypes/packet.go Просмотреть файл

1
-package conntypes
2
-
3
-type Packet []byte

+ 0
- 22
conntypes/protocol.go Просмотреть файл

1
-package conntypes
2
-
3
-type ConnectionProtocol uint8
4
-
5
-func (c ConnectionProtocol) String() string {
6
-	switch c {
7
-	case ConnectionProtocolAny:
8
-		return "any"
9
-	case ConnectionProtocolIPv4:
10
-		return "ipv4"
11
-	case ConnectionProtocolIPv6:
12
-		return "ipv6"
13
-	}
14
-
15
-	return "ipv6"
16
-}
17
-
18
-const (
19
-	ConnectionProtocolIPv4 ConnectionProtocol = 1
20
-	ConnectionProtocolIPv6                    = ConnectionProtocolIPv4 << 1
21
-	ConnectionProtocolAny                     = ConnectionProtocolIPv4 | ConnectionProtocolIPv6
22
-)

+ 0
- 29
conntypes/type.go Просмотреть файл

1
-package conntypes
2
-
3
-type ConnectionType uint8
4
-
5
-const (
6
-	ConnectionTypeUnknown ConnectionType = iota
7
-	ConnectionTypeAbridged
8
-	ConnectionTypeIntermediate
9
-	ConnectionTypeSecure
10
-)
11
-
12
-var (
13
-	ConnectionTagAbridged     = []byte{0xef, 0xef, 0xef, 0xef}
14
-	ConnectionTagIntermediate = []byte{0xee, 0xee, 0xee, 0xee}
15
-	ConnectionTagSecure       = []byte{0xdd, 0xdd, 0xdd, 0xdd}
16
-)
17
-
18
-func (t ConnectionType) Tag() []byte {
19
-	switch t {
20
-	case ConnectionTypeAbridged:
21
-		return ConnectionTagAbridged
22
-	case ConnectionTypeIntermediate:
23
-		return ConnectionTagIntermediate
24
-	case ConnectionTypeSecure, ConnectionTypeUnknown:
25
-		return ConnectionTagSecure
26
-	}
27
-
28
-	return ConnectionTagSecure
29
-}

+ 0
- 14
conntypes/wrap_interfaces.go Просмотреть файл

1
-package conntypes
2
-
3
-import (
4
-	"net"
5
-
6
-	"go.uber.org/zap"
7
-)
8
-
9
-type Wrap interface {
10
-	Conn() net.Conn
11
-	Logger() *zap.SugaredLogger
12
-	LocalAddr() *net.TCPAddr
13
-	RemoteAddr() *net.TCPAddr
14
-}

+ 0
- 41
conntypes/wrap_packet_ack_interfaces.go Просмотреть файл

1
-package conntypes
2
-
3
-import "io"
4
-
5
-type PacketAckReader interface {
6
-	Read(*ConnectionAcks) (Packet, error)
7
-}
8
-
9
-type PacketAckWriter interface {
10
-	Write(Packet, *ConnectionAcks) error
11
-}
12
-
13
-type PacketAckCloser interface {
14
-	io.Closer
15
-}
16
-
17
-type PacketAckReadCloser interface {
18
-	PacketAckReader
19
-	PacketAckCloser
20
-}
21
-
22
-type PacketAckWriteCloser interface {
23
-	PacketAckWriter
24
-	PacketAckCloser
25
-}
26
-
27
-type PacketAckReadWriter interface {
28
-	PacketAckReader
29
-	PacketAckWriter
30
-}
31
-
32
-type PacketAckReadWriteCloser interface {
33
-	PacketAckReader
34
-	PacketAckWriter
35
-	PacketAckCloser
36
-}
37
-
38
-type PacketAckFullReadWriteCloser interface {
39
-	Wrap
40
-	PacketAckReadWriteCloser
41
-}

+ 0
- 51
conntypes/wrap_packet_interfaces.go Просмотреть файл

1
-package conntypes
2
-
3
-import "io"
4
-
5
-type BasePacketReader interface {
6
-	Read() (Packet, error)
7
-}
8
-
9
-type BasePacketWriter interface {
10
-	Write(Packet) error
11
-}
12
-
13
-type PacketReader interface {
14
-	Wrap
15
-	BasePacketReader
16
-}
17
-
18
-type PacketWriter interface {
19
-	Wrap
20
-	BasePacketWriter
21
-}
22
-
23
-type PacketCloser interface {
24
-	Wrap
25
-	io.Closer
26
-}
27
-
28
-type PacketReadCloser interface {
29
-	Wrap
30
-	BasePacketReader
31
-	io.Closer
32
-}
33
-
34
-type PacketWriteCloser interface {
35
-	Wrap
36
-	BasePacketWriter
37
-	io.Closer
38
-}
39
-
40
-type PacketReadWriter interface {
41
-	Wrap
42
-	BasePacketWriter
43
-	BasePacketReader
44
-}
45
-
46
-type PacketReadWriteCloser interface {
47
-	Wrap
48
-	BasePacketWriter
49
-	BasePacketReader
50
-	io.Closer
51
-}

+ 0
- 56
conntypes/wrap_stream_interfaces.go Просмотреть файл

1
-package conntypes
2
-
3
-import (
4
-	"io"
5
-	"time"
6
-)
7
-
8
-type BaseStreamReaderWithTimeout interface {
9
-	ReadTimeout([]byte, time.Duration) (int, error)
10
-}
11
-
12
-type BaseStreamWriterWithTimeout interface {
13
-	WriteTimeout([]byte, time.Duration) (int, error)
14
-}
15
-
16
-type StreamReader interface {
17
-	Wrap
18
-	io.Reader
19
-	BaseStreamReaderWithTimeout
20
-}
21
-
22
-type StreamWriter interface {
23
-	Wrap
24
-	io.Writer
25
-	BaseStreamWriterWithTimeout
26
-}
27
-
28
-type StreamCloser interface {
29
-	Wrap
30
-	io.Closer
31
-}
32
-
33
-type StreamReadCloser interface {
34
-	Wrap
35
-	io.ReadCloser
36
-	BaseStreamReaderWithTimeout
37
-}
38
-
39
-type StreamWriteCloser interface {
40
-	Wrap
41
-	io.WriteCloser
42
-	BaseStreamWriterWithTimeout
43
-}
44
-
45
-type StreamReadWriter interface {
46
-	Wrap
47
-	io.ReadWriter
48
-	BaseStreamReaderWithTimeout
49
-}
50
-
51
-type StreamReadWriteCloser interface {
52
-	Wrap
53
-	io.ReadWriteCloser
54
-	BaseStreamReaderWithTimeout
55
-	BaseStreamWriterWithTimeout
56
-}

+ 108
- 0
events/event_stream.go Просмотреть файл

1
+package events
2
+
3
+import (
4
+	"context"
5
+	"math/rand"
6
+	"runtime"
7
+
8
+	"github.com/9seconds/mtg/v2/mtglib"
9
+	"github.com/OneOfOne/xxhash"
10
+)
11
+
12
+// EventStream is a default implementation of the mtglib.EventStream
13
+// interface.
14
+//
15
+// EventStream manages a set of goroutines, observers. Main
16
+// responsibility of the event stream is to route an event to relevant
17
+// observer based on some hash so each observer will have all events
18
+// which belong to some stream id.
19
+//
20
+// Thus, EventStream can spawn many observers.
21
+type EventStream struct {
22
+	ctx       context.Context
23
+	ctxCancel context.CancelFunc
24
+	chans     []chan mtglib.Event
25
+}
26
+
27
+// Send starts delivering of the message to observer with respect to a
28
+// given context If context is closed, message could be not delivered.
29
+func (e EventStream) Send(ctx context.Context, evt mtglib.Event) {
30
+	var chanNo uint32
31
+
32
+	if streamID := evt.StreamID(); streamID != "" {
33
+		chanNo = xxhash.ChecksumString32(streamID)
34
+	} else {
35
+		chanNo = rand.Uint32()
36
+	}
37
+
38
+	select {
39
+	case <-ctx.Done():
40
+	case <-e.ctx.Done():
41
+	case e.chans[int(chanNo)%len(e.chans)] <- evt:
42
+	}
43
+}
44
+
45
+// Shutdown stops an event stream pipeline.
46
+func (e EventStream) Shutdown() {
47
+	e.ctxCancel()
48
+}
49
+
50
+// NewEventStream builds a new default event stream.
51
+//
52
+// If you give an empty array of observers, then NoopObserver is going
53
+// to be used. If you give many observers, then they will process a
54
+// message concurrently.
55
+func NewEventStream(observerFactories []ObserverFactory) EventStream {
56
+	if len(observerFactories) == 0 {
57
+		observerFactories = append(observerFactories, NewNoopObserver)
58
+	}
59
+
60
+	ctx, cancel := context.WithCancel(context.Background())
61
+	rv := EventStream{
62
+		ctx:       ctx,
63
+		ctxCancel: cancel,
64
+		chans:     make([]chan mtglib.Event, runtime.NumCPU()),
65
+	}
66
+
67
+	for i := 0; i < runtime.NumCPU(); i++ {
68
+		rv.chans[i] = make(chan mtglib.Event, 1)
69
+
70
+		if len(observerFactories) == 1 {
71
+			go eventStreamProcessor(ctx, rv.chans[i], observerFactories[0]())
72
+		} else {
73
+			go eventStreamProcessor(ctx, rv.chans[i], newMultiObserver(observerFactories))
74
+		}
75
+	}
76
+
77
+	return rv
78
+}
79
+
80
+func eventStreamProcessor(ctx context.Context, eventChan <-chan mtglib.Event, observer Observer) { // nolint: cyclop
81
+	defer observer.Shutdown()
82
+
83
+	for {
84
+		select {
85
+		case <-ctx.Done():
86
+			return
87
+		case evt := <-eventChan:
88
+			switch typedEvt := evt.(type) {
89
+			case mtglib.EventTraffic:
90
+				observer.EventTraffic(typedEvt)
91
+			case mtglib.EventStart:
92
+				observer.EventStart(typedEvt)
93
+			case mtglib.EventFinish:
94
+				observer.EventFinish(typedEvt)
95
+			case mtglib.EventConnectedToDC:
96
+				observer.EventConnectedToDC(typedEvt)
97
+			case mtglib.EventDomainFronting:
98
+				observer.EventDomainFronting(typedEvt)
99
+			case mtglib.EventIPBlocklisted:
100
+				observer.EventIPBlocklisted(typedEvt)
101
+			case mtglib.EventConcurrencyLimited:
102
+				observer.EventConcurrencyLimited(typedEvt)
103
+			case mtglib.EventReplayAttack:
104
+				observer.EventReplayAttack(typedEvt)
105
+			}
106
+		}
107
+	}
108
+}

+ 212
- 0
events/event_stream_test.go Просмотреть файл

1
+package events_test
2
+
3
+import (
4
+	"context"
5
+	"net"
6
+	"testing"
7
+	"time"
8
+
9
+	"github.com/9seconds/mtg/v2/events"
10
+	"github.com/9seconds/mtg/v2/mtglib"
11
+	"github.com/stretchr/testify/mock"
12
+	"github.com/stretchr/testify/suite"
13
+)
14
+
15
+type EventStreamTestSuite struct {
16
+	suite.Suite
17
+
18
+	ctx           context.Context
19
+	ctxCancel     context.CancelFunc
20
+	observerMock1 *ObserverMock
21
+	observerMock2 *ObserverMock
22
+	stream        events.EventStream
23
+}
24
+
25
+func (suite *EventStreamTestSuite) SetupTest() {
26
+	suite.ctx, suite.ctxCancel = context.WithCancel(context.Background())
27
+
28
+	suite.observerMock1 = &ObserverMock{}
29
+	suite.observerMock2 = &ObserverMock{}
30
+
31
+	suite.observerMock1.On("Shutdown")
32
+	suite.observerMock2.On("Shutdown")
33
+
34
+	factories := make([]events.ObserverFactory, 2)
35
+	factories[0] = func() events.Observer { return suite.observerMock1 }
36
+	factories[1] = func() events.Observer { return suite.observerMock2 }
37
+
38
+	suite.stream = events.NewEventStream(factories)
39
+}
40
+
41
+func (suite *EventStreamTestSuite) TestEventStart() {
42
+	evt := mtglib.NewEventStart("connID", net.ParseIP("10.0.0.1"))
43
+
44
+	for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
45
+		v.
46
+			On("EventStart", mock.Anything).
47
+			Once().
48
+			Run(func(args mock.Arguments) {
49
+				caught := args.Get(0).(mtglib.EventStart)
50
+
51
+				suite.Equal(evt.RemoteIP.String(), caught.RemoteIP.String())
52
+				suite.Equal(evt.StreamID(), caught.StreamID())
53
+				suite.Equal(evt.Timestamp(), caught.Timestamp())
54
+			})
55
+	}
56
+
57
+	suite.stream.Send(suite.ctx, evt)
58
+	time.Sleep(100 * time.Millisecond)
59
+}
60
+
61
+func (suite *EventStreamTestSuite) TestEventConnectedToDC() {
62
+	evt := mtglib.NewEventConnectedToDC("connID", net.ParseIP("10.0.0.1"), 3)
63
+
64
+	for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
65
+		v.
66
+			On("EventConnectedToDC", mock.Anything).
67
+			Once().
68
+			Run(func(args mock.Arguments) {
69
+				caught := args.Get(0).(mtglib.EventConnectedToDC)
70
+
71
+				suite.Equal(evt.RemoteIP.String(), caught.RemoteIP.String())
72
+				suite.Equal(evt.StreamID(), caught.StreamID())
73
+				suite.Equal(evt.DC, caught.DC)
74
+				suite.Equal(evt.Timestamp(), caught.Timestamp())
75
+			})
76
+	}
77
+
78
+	suite.stream.Send(suite.ctx, evt)
79
+	time.Sleep(100 * time.Millisecond)
80
+}
81
+
82
+func (suite *EventStreamTestSuite) TestEventDomainFronting() {
83
+	evt := mtglib.NewEventDomainFronting("connID")
84
+
85
+	for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
86
+		v.
87
+			On("EventDomainFronting", mock.Anything).
88
+			Once().
89
+			Run(func(args mock.Arguments) {
90
+				caught := args.Get(0).(mtglib.EventDomainFronting)
91
+
92
+				suite.Equal(evt.StreamID(), caught.StreamID())
93
+				suite.Equal(evt.Timestamp(), caught.Timestamp())
94
+			})
95
+	}
96
+
97
+	suite.stream.Send(suite.ctx, evt)
98
+	time.Sleep(100 * time.Millisecond)
99
+}
100
+
101
+func (suite *EventStreamTestSuite) TestEventTraffic() {
102
+	evt := mtglib.NewEventTraffic("connID", 1024, true)
103
+
104
+	for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
105
+		v.
106
+			On("EventTraffic", mock.Anything).
107
+			Once().
108
+			Run(func(args mock.Arguments) {
109
+				caught := args.Get(0).(mtglib.EventTraffic)
110
+
111
+				suite.Equal(evt.StreamID(), caught.StreamID())
112
+				suite.Equal(evt.Timestamp(), caught.Timestamp())
113
+				suite.Equal(evt.Traffic, caught.Traffic)
114
+				suite.Equal(evt.IsRead, caught.IsRead)
115
+			})
116
+	}
117
+
118
+	suite.stream.Send(suite.ctx, evt)
119
+	time.Sleep(100 * time.Millisecond)
120
+}
121
+
122
+func (suite *EventStreamTestSuite) TestEventFinish() {
123
+	evt := mtglib.NewEventFinish("connID")
124
+
125
+	for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
126
+		v.
127
+			On("EventFinish", mock.Anything).
128
+			Once().
129
+			Run(func(args mock.Arguments) {
130
+				caught := args.Get(0).(mtglib.EventFinish)
131
+
132
+				suite.Equal(evt.StreamID(), caught.StreamID())
133
+				suite.Equal(evt.Timestamp(), caught.Timestamp())
134
+			})
135
+	}
136
+
137
+	suite.stream.Send(suite.ctx, evt)
138
+	time.Sleep(100 * time.Millisecond)
139
+}
140
+
141
+func (suite *EventStreamTestSuite) TestEventConcurrencyLimited() {
142
+	evt := mtglib.NewEventConcurrencyLimited()
143
+
144
+	for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
145
+		v.
146
+			On("EventConcurrencyLimited", mock.Anything).
147
+			Once().
148
+			Run(func(args mock.Arguments) {
149
+				caught := args.Get(0).(mtglib.EventConcurrencyLimited)
150
+
151
+				suite.Equal(evt.Timestamp(), caught.Timestamp())
152
+				suite.Empty(evt.StreamID())
153
+			})
154
+	}
155
+
156
+	suite.stream.Send(suite.ctx, evt)
157
+	time.Sleep(100 * time.Millisecond)
158
+}
159
+
160
+func (suite *EventStreamTestSuite) TestEventIPBlocklisted() {
161
+	evt := mtglib.NewEventIPBlocklisted(net.ParseIP("10.0.0.10"))
162
+
163
+	for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
164
+		v.
165
+			On("EventIPBlocklisted", mock.Anything).
166
+			Once().
167
+			Run(func(args mock.Arguments) {
168
+				caught := args.Get(0).(mtglib.EventIPBlocklisted)
169
+
170
+				suite.Equal(evt.StreamID(), caught.StreamID())
171
+				suite.Equal(evt.Timestamp(), caught.Timestamp())
172
+				suite.Equal(evt.RemoteIP.String(), caught.RemoteIP.String())
173
+			})
174
+	}
175
+
176
+	suite.stream.Send(suite.ctx, evt)
177
+	time.Sleep(100 * time.Millisecond)
178
+}
179
+
180
+func (suite *EventStreamTestSuite) TestEventReplayAttack() {
181
+	evt := mtglib.NewEventReplayAttack("CONNID")
182
+
183
+	for _, v := range []*ObserverMock{suite.observerMock1, suite.observerMock2} {
184
+		v.
185
+			On("EventReplayAttack", mock.Anything).
186
+			Once().
187
+			Run(func(args mock.Arguments) {
188
+				caught := args.Get(0).(mtglib.EventReplayAttack)
189
+
190
+				suite.Equal(evt.StreamID(), caught.StreamID())
191
+				suite.Equal(evt.Timestamp(), caught.Timestamp())
192
+			})
193
+	}
194
+
195
+	suite.stream.Send(suite.ctx, evt)
196
+	time.Sleep(100 * time.Millisecond)
197
+}
198
+
199
+func (suite *EventStreamTestSuite) TearDownTest() {
200
+	suite.stream.Shutdown()
201
+	suite.ctxCancel()
202
+
203
+	time.Sleep(100 * time.Millisecond)
204
+
205
+	suite.observerMock1.AssertExpectations(suite.T())
206
+	suite.observerMock2.AssertExpectations(suite.T())
207
+}
208
+
209
+func TestEventStream(t *testing.T) {
210
+	t.Parallel()
211
+	suite.Run(t, &EventStreamTestSuite{})
212
+}

+ 69
- 0
events/init.go Просмотреть файл

1
+// Events has a default implementations of EventStream for mtglib.
2
+//
3
+// Please see documentation for mtglib.EventStream interface to get an
4
+// idea of such an abstraction. This package has implementations for the
5
+// default event stream.
6
+//
7
+// Default event stream has a list of its own concepts. First, all it
8
+// does is a routing of messages to known observers. It takes an event,
9
+// defines its type and pass this message to a method of the observer.
10
+//
11
+// There might be many observers, but default event stream has a
12
+// guarantee though. It uses StreamID as a sharding key and guarantees
13
+// that a message with the same StreamID will be devlivered to the same
14
+// observer instance. So, each producer is guarateed to get all relevant
15
+// messages related to the same session. It is not possible that it will
16
+// get EventFinish if it has not seen EventStart for that session yet.
17
+package events
18
+
19
+import "github.com/9seconds/mtg/v2/mtglib"
20
+
21
+// Observer is an instance that listens for the incoming events.
22
+//
23
+// As it is said in the package description, the default event stream
24
+// guarantees that all events with the same StreamID are going to be
25
+// routed to the same instance of the observer. So, there is no need
26
+// to synchronize information about streams between many observers
27
+// instances, they can have their local storage.
28
+type Observer interface {
29
+	// EventStart reacts on incoming mtglib.EventStart event.
30
+	EventStart(mtglib.EventStart)
31
+
32
+	// EventFinish reacts on incoming mtglib.EventFinish event.
33
+	EventFinish(mtglib.EventFinish)
34
+
35
+	// EventConnectedToDC reacts on incoming mtglib.EventConnectedToDC
36
+	// event.
37
+	EventConnectedToDC(mtglib.EventConnectedToDC)
38
+
39
+	// EventDomainFronting reacts on incoming mtglib.EventDomainFronting
40
+	// event.
41
+	EventDomainFronting(mtglib.EventDomainFronting)
42
+
43
+	// EventTraffic reacts on incoming mtglib.EventTraffic event.
44
+	EventTraffic(mtglib.EventTraffic)
45
+
46
+	// EventConcurrencyLimited reacts on incoming
47
+	// mtglib.EventConcurrencyLimited event.
48
+	EventConcurrencyLimited(mtglib.EventConcurrencyLimited)
49
+
50
+	// EventIPBlocklisted reacts on incoming mtglib.EventIPBlocklisted event.
51
+	EventIPBlocklisted(mtglib.EventIPBlocklisted)
52
+
53
+	// EventReplayAttack reacts on incoming mtglib.EventReplayAttack event.
54
+	EventReplayAttack(mtglib.EventReplayAttack)
55
+
56
+	// Shutdown stop observer. Default event stream guarantees:
57
+	//   1. If shutdown is executed, it is executed only once
58
+	//   2. Observer won't receieve any new message after this
59
+	//      function call.
60
+	Shutdown()
61
+}
62
+
63
+// ObserverFactory creates a new instance of the observer.
64
+//
65
+// Default event stream creates a small set of goroutines to manage
66
+// incoming messages. Each message is routed to an appropriate observer
67
+// based on a sharding key, stream id. So, it is possible that an
68
+// instance of mtg will have many observer instances, not a single one.
69
+type ObserverFactory func() Observer

+ 46
- 0
events/init_test.go Просмотреть файл

1
+package events_test
2
+
3
+import (
4
+	"github.com/9seconds/mtg/v2/mtglib"
5
+	"github.com/stretchr/testify/mock"
6
+)
7
+
8
+type ObserverMock struct {
9
+	mock.Mock
10
+}
11
+
12
+func (o *ObserverMock) EventStart(evt mtglib.EventStart) {
13
+	o.Called(evt)
14
+}
15
+
16
+func (o *ObserverMock) EventConnectedToDC(evt mtglib.EventConnectedToDC) {
17
+	o.Called(evt)
18
+}
19
+
20
+func (o *ObserverMock) EventDomainFronting(evt mtglib.EventDomainFronting) {
21
+	o.Called(evt)
22
+}
23
+
24
+func (o *ObserverMock) EventTraffic(evt mtglib.EventTraffic) {
25
+	o.Called(evt)
26
+}
27
+
28
+func (o *ObserverMock) EventFinish(evt mtglib.EventFinish) {
29
+	o.Called(evt)
30
+}
31
+
32
+func (o *ObserverMock) EventConcurrencyLimited(evt mtglib.EventConcurrencyLimited) {
33
+	o.Called(evt)
34
+}
35
+
36
+func (o *ObserverMock) EventIPBlocklisted(evt mtglib.EventIPBlocklisted) {
37
+	o.Called(evt)
38
+}
39
+
40
+func (o *ObserverMock) EventReplayAttack(evt mtglib.EventReplayAttack) {
41
+	o.Called(evt)
42
+}
43
+
44
+func (o *ObserverMock) Shutdown() {
45
+	o.Called()
46
+}

+ 149
- 0
events/multi_observer.go Просмотреть файл

1
+package events
2
+
3
+import (
4
+	"sync"
5
+
6
+	"github.com/9seconds/mtg/v2/mtglib"
7
+)
8
+
9
+type multiObserver struct {
10
+	observers []Observer
11
+}
12
+
13
+func (m multiObserver) EventStart(evt mtglib.EventStart) {
14
+	wg := &sync.WaitGroup{}
15
+	wg.Add(len(m.observers))
16
+
17
+	for _, v := range m.observers {
18
+		go func(obs Observer) {
19
+			defer wg.Done()
20
+
21
+			obs.EventStart(evt)
22
+		}(v)
23
+	}
24
+
25
+	wg.Wait()
26
+}
27
+
28
+func (m multiObserver) EventConnectedToDC(evt mtglib.EventConnectedToDC) {
29
+	wg := &sync.WaitGroup{}
30
+	wg.Add(len(m.observers))
31
+
32
+	for _, v := range m.observers {
33
+		go func(obs Observer) {
34
+			defer wg.Done()
35
+
36
+			obs.EventConnectedToDC(evt)
37
+		}(v)
38
+	}
39
+
40
+	wg.Wait()
41
+}
42
+
43
+func (m multiObserver) EventDomainFronting(evt mtglib.EventDomainFronting) {
44
+	wg := &sync.WaitGroup{}
45
+	wg.Add(len(m.observers))
46
+
47
+	for _, v := range m.observers {
48
+		go func(obs Observer) {
49
+			defer wg.Done()
50
+
51
+			obs.EventDomainFronting(evt)
52
+		}(v)
53
+	}
54
+
55
+	wg.Wait()
56
+}
57
+
58
+func (m multiObserver) EventTraffic(evt mtglib.EventTraffic) {
59
+	wg := &sync.WaitGroup{}
60
+	wg.Add(len(m.observers))
61
+
62
+	for _, v := range m.observers {
63
+		go func(obs Observer) {
64
+			defer wg.Done()
65
+
66
+			obs.EventTraffic(evt)
67
+		}(v)
68
+	}
69
+
70
+	wg.Wait()
71
+}
72
+
73
+func (m multiObserver) EventFinish(evt mtglib.EventFinish) {
74
+	wg := &sync.WaitGroup{}
75
+	wg.Add(len(m.observers))
76
+
77
+	for _, v := range m.observers {
78
+		go func(obs Observer) {
79
+			defer wg.Done()
80
+
81
+			obs.EventFinish(evt)
82
+		}(v)
83
+	}
84
+
85
+	wg.Wait()
86
+}
87
+
88
+func (m multiObserver) EventConcurrencyLimited(evt mtglib.EventConcurrencyLimited) {
89
+	wg := &sync.WaitGroup{}
90
+	wg.Add(len(m.observers))
91
+
92
+	for _, v := range m.observers {
93
+		go func(obs Observer) {
94
+			defer wg.Done()
95
+
96
+			obs.EventConcurrencyLimited(evt)
97
+		}(v)
98
+	}
99
+
100
+	wg.Wait()
101
+}
102
+
103
+func (m multiObserver) EventIPBlocklisted(evt mtglib.EventIPBlocklisted) {
104
+	wg := &sync.WaitGroup{}
105
+	wg.Add(len(m.observers))
106
+
107
+	for _, v := range m.observers {
108
+		go func(obs Observer) {
109
+			defer wg.Done()
110
+
111
+			obs.EventIPBlocklisted(evt)
112
+		}(v)
113
+	}
114
+
115
+	wg.Wait()
116
+}
117
+
118
+func (m multiObserver) EventReplayAttack(evt mtglib.EventReplayAttack) {
119
+	wg := &sync.WaitGroup{}
120
+	wg.Add(len(m.observers))
121
+
122
+	for _, v := range m.observers {
123
+		go func(obs Observer) {
124
+			defer wg.Done()
125
+
126
+			obs.EventReplayAttack(evt)
127
+		}(v)
128
+	}
129
+
130
+	wg.Wait()
131
+}
132
+
133
+func (m multiObserver) Shutdown() {
134
+	for _, v := range m.observers {
135
+		v.Shutdown()
136
+	}
137
+}
138
+
139
+func newMultiObserver(factories []ObserverFactory) Observer {
140
+	observers := make([]Observer, len(factories))
141
+
142
+	for i, v := range factories {
143
+		observers[i] = v()
144
+	}
145
+
146
+	return multiObserver{
147
+		observers: observers,
148
+	}
149
+}

+ 33
- 0
events/noop.go Просмотреть файл

1
+package events
2
+
3
+import (
4
+	"context"
5
+
6
+	"github.com/9seconds/mtg/v2/mtglib"
7
+)
8
+
9
+type noop struct{}
10
+
11
+func (n noop) Send(ctx context.Context, evt mtglib.Event) {}
12
+
13
+// NewNoopStream creates a stream which discards each message.
14
+func NewNoopStream() mtglib.EventStream {
15
+	return noop{}
16
+}
17
+
18
+type noopObserver struct{}
19
+
20
+func (n noopObserver) EventStart(_ mtglib.EventStart)                           {}
21
+func (n noopObserver) EventConnectedToDC(_ mtglib.EventConnectedToDC)           {}
22
+func (n noopObserver) EventDomainFronting(_ mtglib.EventDomainFronting)         {}
23
+func (n noopObserver) EventTraffic(_ mtglib.EventTraffic)                       {}
24
+func (n noopObserver) EventFinish(_ mtglib.EventFinish)                         {}
25
+func (n noopObserver) EventConcurrencyLimited(_ mtglib.EventConcurrencyLimited) {}
26
+func (n noopObserver) EventIPBlocklisted(_ mtglib.EventIPBlocklisted)           {}
27
+func (n noopObserver) EventReplayAttack(_ mtglib.EventReplayAttack)             {}
28
+func (n noopObserver) Shutdown()                                                {}
29
+
30
+// NewNoopObserver creates an observer which discards each message.
31
+func NewNoopObserver() Observer {
32
+	return noopObserver{}
33
+}

+ 78
- 0
events/noop_test.go Просмотреть файл

1
+package events_test
2
+
3
+import (
4
+	"context"
5
+	"net"
6
+	"testing"
7
+
8
+	"github.com/9seconds/mtg/v2/events"
9
+	"github.com/9seconds/mtg/v2/mtglib"
10
+	"github.com/stretchr/testify/suite"
11
+)
12
+
13
+type NoopTestSuite struct {
14
+	suite.Suite
15
+
16
+	testData map[string]mtglib.Event
17
+	ctx      context.Context
18
+}
19
+
20
+func (suite *NoopTestSuite) SetupSuite() {
21
+	suite.testData = map[string]mtglib.Event{
22
+		"start":               mtglib.NewEventStart("connID", net.ParseIP("127.0.0.1")),
23
+		"connected-to-dc":     mtglib.NewEventConnectedToDC("connID", net.ParseIP("127.1.0.1"), 2),
24
+		"domain-fronting":     mtglib.NewEventDomainFronting("connID"),
25
+		"traffic":             mtglib.NewEventTraffic("connID", 1000, true),
26
+		"finish":              mtglib.NewEventFinish("connID"),
27
+		"concurrency-limited": mtglib.NewEventConcurrencyLimited(),
28
+		"ip-blacklisted":      mtglib.NewEventIPBlocklisted(net.ParseIP("10.0.0.10")),
29
+		"replay-attack":       mtglib.NewEventReplayAttack("connID"),
30
+	}
31
+	suite.ctx = context.Background()
32
+}
33
+
34
+func (suite *NoopTestSuite) TestStream() {
35
+	stream := events.NewNoopStream()
36
+
37
+	for name, v := range suite.testData {
38
+		value := v
39
+
40
+		suite.T().Run(name, func(t *testing.T) {
41
+			stream.Send(suite.ctx, value)
42
+		})
43
+	}
44
+}
45
+
46
+func (suite *NoopTestSuite) TestObserver() {
47
+	observer := events.NewNoopObserver()
48
+
49
+	for name, v := range suite.testData {
50
+		value := v
51
+
52
+		suite.T().Run(name, func(t *testing.T) {
53
+			switch typedEvt := value.(type) {
54
+			case mtglib.EventStart:
55
+				observer.EventStart(typedEvt)
56
+			case mtglib.EventConnectedToDC:
57
+				observer.EventConnectedToDC(typedEvt)
58
+			case mtglib.EventDomainFronting:
59
+				observer.EventDomainFronting(typedEvt)
60
+			case mtglib.EventFinish:
61
+				observer.EventFinish(typedEvt)
62
+			case mtglib.EventConcurrencyLimited:
63
+				observer.EventConcurrencyLimited(typedEvt)
64
+			case mtglib.EventIPBlocklisted:
65
+				observer.EventIPBlocklisted(typedEvt)
66
+			case mtglib.EventReplayAttack:
67
+				observer.EventReplayAttack(typedEvt)
68
+			}
69
+		})
70
+	}
71
+
72
+	observer.Shutdown()
73
+}
74
+
75
+func TestNoop(t *testing.T) {
76
+	t.Parallel()
77
+	suite.Run(t, &NoopTestSuite{})
78
+}

+ 186
- 0
example.config.toml Просмотреть файл

1
+# This is an example of the configuration file for mtg. You actually can
2
+# run mtg with it. It starts a proxy on all interfaces with a secret
3
+# ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d
4
+#
5
+# It has all possible options with default values. So, a real world
6
+# configuration file should contain only those options you are going to
7
+# use. You do not need to enumerate all of them. In other words, each
8
+# option here has a default value. If you comment a key-value pair, it
9
+# should not make any effect.
10
+#
11
+# stats is the only exception.
12
+
13
+# Debug starts application in debug mode. It starts to be quite verbose
14
+# in output. Actually, the idea is that you run it in debug mode only if
15
+# you have any issue.
16
+debug = true
17
+
18
+# A secret. Please remember that mtg supports only FakeTLS mode, legacy
19
+# simple and secured mode are prohibited. For you it means that secret
20
+# should either be base64-encoded or starts with ee.
21
+secret = "ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d"
22
+
23
+# Host:port pair to run proxy on.
24
+bind-to = "0.0.0.0:3128"
25
+
26
+# Defines how many concurrent connections are allowed to this proxy.
27
+# All other incoming connections are going to be dropped.
28
+concurrency = 8192
29
+
30
+# A size of user-space buffer for TCP to use. Since we do 2 connections,
31
+# then we have tcp-buffer * (4 + 2) per each connection: read/write for
32
+# each connection + 2 copy buffers to pump the data between sockets.
33
+tcp-buffer = "4kb"
34
+
35
+# Sometimes you want to enforce mtg to use some types of
36
+# IP connectivity to Telegram. We have 4 modes:
37
+#   - prefer-ipv6:
38
+#     We can use both ipv4 and ipv6 but ipv6 has a preference
39
+#   - prefer-ipv4:
40
+#     We can use both ipv4 and ipv6 but ipv4 has a preference
41
+#   - only-ipv6:
42
+#     Only ipv6 connectivity is used
43
+#   - only-ipv4:
44
+#     Only ipv4 connectivity is used
45
+prefer-ip = "prefer-ipv6"
46
+
47
+# FakeTLS uses domain fronting protection. So it needs to know a port to
48
+# access.
49
+domain-fronting-port = 443
50
+
51
+# FakeTLS can compare timestamps to prevent probes. Each message has
52
+# encrypted timestamp. So, mtg can compare this timestamp and decide if
53
+# we need to proceed with connection or not.
54
+#
55
+# Sometimes time can be skewed so we accept all messages within a
56
+# time range of this parameter.
57
+tolerate-time-skewness = "5s"
58
+
59
+# network defines different network-related settings
60
+[network]
61
+# please be aware that mtg needs to do some external requests. For
62
+# example, if you do not pass public ips, it will request your public ip
63
+# address from some external service.
64
+#
65
+# As for 2.0, if you set a public-ip on your own, mtg won't issue any
66
+# network requests except of those required for Telegram.
67
+#
68
+# so, in order of doing them, it needs to do DNS lookup. mtg ignores DNS
69
+# resolver of the operating system and uses DOH instead. This is a host
70
+# it has to access.
71
+#
72
+# By default we use Quad9.
73
+doh-ip = "9.9.9.9"
74
+
75
+# mtg can work via proxies (for now, we support only socks5). Proxy
76
+# configuration is done via list. So, you can specify many proxies
77
+# there.
78
+#
79
+# Actually, if you supply an empty list, then no proxies are going to be
80
+# used. If you supply a single proxy, then mtg will use it exclusively.
81
+# If you supply >= 2, then mtg will load balance between them.
82
+#
83
+# If you add an empty string here, this is an equivalent of 'plain network',
84
+# with no proxy usage.
85
+#
86
+# Proxy configuration is done via ordinary URI schema:
87
+#
88
+#     socks5://user:password@host:port?open_threshold=5&half_open_timeout=1m&reset_failures_timeout=10s
89
+#
90
+# Only socks5 proxy is used. user/password is optional. As you can
91
+# see, you can specify some parameters in GET query. These parameters
92
+# configure circuit breaker.
93
+#
94
+# open_threshold means a number of errors which should happen so we stop
95
+# use a proxy.
96
+#
97
+# half_open_timeout means a time period (in Golang duration notation)
98
+# after which we can retry with this proxy
99
+#
100
+# reset_failures_timeout means a time period when we flush out errors
101
+# when circuit breaker in closed state.
102
+#
103
+# Please see https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker
104
+# on details about circuit breakers.
105
+proxies = [
106
+    # "socks5://user:password@host:port?open_threshold=5&half_open_timeout=1m&reset_failures_timeout=10s"
107
+]
108
+
109
+# network timeouts define different settings for timeouts. tcp timeout
110
+# define a global timeout on establishing of network connections. idle
111
+# means a timeout on pumping data between sockset when nothing is
112
+# happening.
113
+#
114
+# please be noticed that handshakes have no timeouts intentionally. You can
115
+# find a reasoning here:
116
+# https://www.ndss-symposium.org/wp-content/uploads/2020/02/23087-paper.pdf
117
+[network.timeout]
118
+tcp = "5s"
119
+http = "10s"
120
+idle = "1m"
121
+
122
+# Some countries do active probing on Telegram connections. This technique
123
+# allows to protect from such effort.
124
+#
125
+# mtg has a cache of some connection fingerprints. Actually, first bytes
126
+# of each connection. So, it stores them in some in-memory LRU+TTL cache.
127
+# You can configure this cache here.
128
+[defense.anti-replay]
129
+# You can enable/disable this feature.
130
+enabled = true
131
+# max size of such a cache. Please be aware that this number is
132
+# approximate we try hard to store data quite dense but it is possible
133
+# that we can go over this limit for 10-20% under some conditions and
134
+# architectures.
135
+max-size = "1mib"
136
+# we use stable bloom filters for anti-replay cache. This helps
137
+# to maintain a desired error ratio.
138
+error-rate = 0.001
139
+
140
+# You can protect proxies by using different blocklists. If client has
141
+# ip from the given range, we do not try to do a proper handshake. We
142
+# actually route it to fronting domain. So, this client will never ever
143
+# have a chance to use mtg to access Telegram.
144
+#
145
+# Please remember that blocklists are initialized in async way. So,
146
+# when you start a proxy, blocklists are empty, they are populated and
147
+# processed in backgrounds. An error in any URL is ignored.
148
+[defense.blocklist]
149
+# You can enable/disable this feature.
150
+enabled = true
151
+# This is a limiter for concurrency. In order to protect website
152
+# from overloading, we download files in this number of threads.
153
+download-concurrency = 2
154
+# A list of URLs in FireHOL format (https://iplists.firehol.org/)
155
+# You can provider links here (starts with https:// or http://) or
156
+# path to a local file, but in this case it should be absolute.
157
+urls = [
158
+    # "https://iplists.firehol.org/files/firehol_level1.netset",
159
+    # "/local.file"
160
+]
161
+# How often do we need to update a blocklist set.
162
+update-each = "24h"
163
+
164
+# statsd statistics integration.
165
+[stats.statsd]
166
+# enabled/disabled
167
+enabled = false
168
+# host:port for UDP endpoint of statsd
169
+address = "127.0.0.1:8888"
170
+# prefix of metric for statsd
171
+metric-prefix = "mtg"
172
+# tag format to use
173
+# supported values are 'datadog', 'influxdb' and 'graphite'
174
+# default format is graphite.
175
+tag-format = "datadog"
176
+
177
+# prometheus metrics integration.
178
+[stats.prometheus]
179
+# enabled/disabled
180
+enabled = true
181
+# host:port where to start http server for endpoint
182
+bind-to = "127.0.0.1:3129"
183
+# prefix of http path
184
+http-path = "/"
185
+# prefix for metrics for prometheus
186
+metric-prefix = "mtg"

+ 0
- 122
faketls/client_protocol.go Просмотреть файл

1
-package faketls
2
-
3
-import (
4
-	"bufio"
5
-	"bytes"
6
-	"encoding/binary"
7
-	"errors"
8
-	"fmt"
9
-	"io"
10
-	"net"
11
-	"strconv"
12
-	"time"
13
-
14
-	"github.com/9seconds/mtg/antireplay"
15
-	"github.com/9seconds/mtg/config"
16
-	"github.com/9seconds/mtg/conntypes"
17
-	"github.com/9seconds/mtg/obfuscated2"
18
-	"github.com/9seconds/mtg/protocol"
19
-	"github.com/9seconds/mtg/stats"
20
-	"github.com/9seconds/mtg/tlstypes"
21
-	"github.com/9seconds/mtg/wrappers/stream"
22
-)
23
-
24
-type ClientProtocol struct {
25
-	obfuscated2.ClientProtocol
26
-}
27
-
28
-func (c *ClientProtocol) Handshake(socket conntypes.StreamReadWriteCloser) (conntypes.StreamReadWriteCloser, error) {
29
-	rewinded := stream.NewRewind(socket)
30
-	bufferedReader := bufio.NewReader(rewinded)
31
-
32
-	for _, expected := range faketlsStartBytes {
33
-		if actual, err := bufferedReader.ReadByte(); err != nil || actual != expected {
34
-			rewinded.Rewind()
35
-			c.cloakHost(rewinded)
36
-
37
-			return nil, errors.New("failed first bytes of tls handshake")
38
-		}
39
-	}
40
-
41
-	rewinded.Rewind()
42
-	rewinded = stream.NewRewind(rewinded)
43
-
44
-	if err := c.tlsHandshake(rewinded); err != nil {
45
-		rewinded.Rewind()
46
-		c.cloakHost(rewinded)
47
-
48
-		return nil, fmt.Errorf("failed tls handshake: %w", err)
49
-	}
50
-
51
-	conn := stream.NewFakeTLS(socket)
52
-
53
-	conn, err := c.ClientProtocol.Handshake(conn)
54
-	if err != nil {
55
-		return nil, err // nolint: wrapcheck
56
-	}
57
-
58
-	return conn, err // nolint: wrapcheck
59
-}
60
-
61
-func (c *ClientProtocol) tlsHandshake(conn io.ReadWriter) error {
62
-	helloRecord, err := tlstypes.ReadRecord(conn)
63
-	if err != nil {
64
-		return fmt.Errorf("cannot read initial record: %w", err)
65
-	}
66
-
67
-	buf := &bytes.Buffer{}
68
-	helloRecord.Data.WriteBytes(buf)
69
-
70
-	clientHello, err := tlstypes.ParseClientHello(buf.Bytes())
71
-	if err != nil {
72
-		return fmt.Errorf("cannot parse client hello: %w", err)
73
-	}
74
-
75
-	digest := clientHello.Digest()
76
-	for i := 0; i < len(digest)-4; i++ {
77
-		if digest[i] != 0 {
78
-			return errBadDigest
79
-		}
80
-	}
81
-
82
-	timestamp := int64(binary.LittleEndian.Uint32(digest[len(digest)-4:]))
83
-	createdAt := time.Unix(timestamp, 0)
84
-	timeDiff := time.Since(createdAt)
85
-
86
-	if (timeDiff > TimeSkew || timeDiff < -TimeSkew) && timestamp > TimeFromBoot {
87
-		return errBadTime
88
-	}
89
-
90
-	if antireplay.Cache.HasTLS(clientHello.Random[:]) {
91
-		stats.Stats.ReplayDetected()
92
-
93
-		return errors.New("replay attack is detected")
94
-	}
95
-
96
-	antireplay.Cache.AddTLS(clientHello.Random[:])
97
-	serverHello := tlstypes.NewServerHello(clientHello)
98
-	serverHelloPacket := serverHello.WelcomePacket()
99
-
100
-	if _, err := conn.Write(serverHelloPacket); err != nil {
101
-		return fmt.Errorf("cannot send welcome packet: %w", err)
102
-	}
103
-
104
-	return nil
105
-}
106
-
107
-func (c *ClientProtocol) cloakHost(clientConn io.ReadWriteCloser) {
108
-	stats.Stats.CloakedRequest()
109
-
110
-	addr := net.JoinHostPort(config.C.CloakHost, strconv.Itoa(config.C.CloakPort))
111
-
112
-	hostConn, err := net.Dial("tcp", addr)
113
-	if err != nil {
114
-		return
115
-	}
116
-
117
-	cloak(clientConn, hostConn)
118
-}
119
-
120
-func MakeClientProtocol() protocol.ClientProtocol {
121
-	return &ClientProtocol{}
122
-}

+ 0
- 73
faketls/cloak.go Просмотреть файл

1
-package faketls
2
-
3
-import (
4
-	"context"
5
-	"io"
6
-	"sync"
7
-	"time"
8
-
9
-	"github.com/9seconds/mtg/wrappers/rwc"
10
-)
11
-
12
-const (
13
-	cloakLastActivityTimeout = 5 * time.Second
14
-	cloakMaxTimeout          = 30 * time.Second
15
-)
16
-
17
-func cloak(one, another io.ReadWriteCloser) {
18
-	defer func() {
19
-		one.Close()
20
-		another.Close()
21
-	}()
22
-
23
-	channelPing := make(chan struct{}, 1)
24
-	ctx, cancel := context.WithCancel(context.Background())
25
-	one = rwc.NewPing(ctx, one, channelPing)
26
-	another = rwc.NewPing(ctx, another, channelPing)
27
-	wg := &sync.WaitGroup{}
28
-
29
-	wg.Add(2)
30
-
31
-	go cloakPipe(one, another, wg)
32
-
33
-	go cloakPipe(another, one, wg)
34
-
35
-	go func() {
36
-		wg.Wait()
37
-		cancel()
38
-	}()
39
-
40
-	go func() {
41
-		lastActivityTimer := time.NewTimer(cloakLastActivityTimeout)
42
-		defer lastActivityTimer.Stop()
43
-
44
-		maxTimer := time.NewTimer(cloakMaxTimeout)
45
-		defer maxTimer.Stop()
46
-
47
-		for {
48
-			select {
49
-			case <-channelPing:
50
-				lastActivityTimer.Stop()
51
-				lastActivityTimer = time.NewTimer(cloakLastActivityTimeout)
52
-			case <-ctx.Done():
53
-				return
54
-			case <-lastActivityTimer.C:
55
-				cancel()
56
-
57
-				return
58
-			case <-maxTimer.C:
59
-				cancel()
60
-
61
-				return
62
-			}
63
-		}
64
-	}()
65
-
66
-	<-ctx.Done()
67
-}
68
-
69
-func cloakPipe(one io.Writer, another io.Reader, wg *sync.WaitGroup) {
70
-	defer wg.Done()
71
-
72
-	io.Copy(one, another) // nolint: errcheck
73
-}

+ 0
- 30
faketls/consts.go Просмотреть файл

1
-package faketls
2
-
3
-import (
4
-	"errors"
5
-	"time"
6
-)
7
-
8
-const (
9
-	TimeSkew     = 5 * time.Second
10
-	TimeFromBoot = 24 * 60 * 60
11
-)
12
-
13
-var (
14
-	errBadDigest = errors.New("bad digest")
15
-	errBadTime   = errors.New("bad time")
16
-
17
-	faketlsStartBytes = [...]byte{
18
-		0x16,
19
-		0x03,
20
-		0x01,
21
-		0x02,
22
-		0x00,
23
-		0x01,
24
-		0x00,
25
-		0x01,
26
-		0xfc,
27
-		0x03,
28
-		0x03,
29
-	}
30
-)

+ 21
- 18
go.mod Просмотреть файл

1
-module github.com/9seconds/mtg
1
+module github.com/9seconds/mtg/v2
2
 
2
 
3
-go 1.13
3
+go 1.16
4
 
4
 
5
 require (
5
 require (
6
-	github.com/VictoriaMetrics/fastcache v1.5.7
6
+	github.com/OneOfOne/xxhash v1.2.8
7
+	github.com/alecthomas/kong v0.2.16
7
 	github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15
8
 	github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15
8
-	github.com/beevik/ntp v0.3.0
9
-	github.com/golang/snappy v0.0.3 // indirect
9
+	github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
10
+	github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6
11
+	github.com/d4l3k/messagediff v1.2.1 // indirect
12
+	github.com/jarcoal/httpmock v1.0.8
13
+	github.com/kentik/patricia v0.0.0-20201202224819-f9447a6e25f1
14
+	github.com/libp2p/go-reuseport v0.0.2
15
+	github.com/mccutchen/go-httpbin v1.1.1
16
+	github.com/panjf2000/ants/v2 v2.4.3
17
+	github.com/pelletier/go-toml v1.8.1
10
 	github.com/prometheus/client_golang v1.9.0
18
 	github.com/prometheus/client_golang v1.9.0
11
-	github.com/prometheus/common v0.18.0 // indirect
12
-	github.com/prometheus/procfs v0.6.0 // indirect
19
+	github.com/rs/zerolog v1.20.0
13
 	github.com/smira/go-statsd v1.3.2
20
 	github.com/smira/go-statsd v1.3.2
14
-	go.uber.org/multierr v1.6.0 // indirect
15
-	go.uber.org/zap v1.16.0
16
-	golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
17
-	golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
18
-	golang.org/x/mod v0.4.1 // indirect
19
-	golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
20
-	golang.org/x/sys v0.0.0-20210303074136-134d130e1a04
21
-	golang.org/x/tools v0.1.0 // indirect
22
-	google.golang.org/protobuf v1.25.0 // indirect
23
-	gopkg.in/alecthomas/kingpin.v2 v2.2.6
24
-	honnef.co/go/tools v0.0.1-2020.1.3 // indirect
21
+	github.com/stretchr/objx v0.3.0 // indirect
22
+	github.com/stretchr/testify v1.7.0
23
+	github.com/tylertreat/BoomFilters v0.0.0-20200520150052-42a7b4300c0c
24
+	github.com/xeipuuv/gojsonschema v1.2.0
25
+	golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
26
+	golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
27
+	golang.org/x/sys v0.0.0-20210309074719-68d13333faf2
25
 )
28
 )

+ 54
- 63
go.sum Просмотреть файл

1
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
1
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2
 cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3
-github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
4
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
3
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
5
 github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
4
 github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
5
+github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
6
+github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
6
 github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
7
 github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
7
 github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
8
 github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
8
-github.com/VictoriaMetrics/fastcache v1.5.7 h1:4y6y0G8PRzszQUYIQHHssv/jgPHAb5qQuuDNdCbyAgw=
9
-github.com/VictoriaMetrics/fastcache v1.5.7/go.mod h1:ptDBkNMQI4RtmVo8VS/XwRY6RoTu1dAWCbrk+6WsEM8=
10
 github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
9
 github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
11
 github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
10
 github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
11
+github.com/alecthomas/kong v0.2.16 h1:F232CiYSn54Tnl1sJGTeHmx4vJDNLVP2b9yCVMOQwHQ=
12
+github.com/alecthomas/kong v0.2.16/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
12
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
13
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
13
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
14
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
14
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
15
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
15
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
16
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
16
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
17
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
17
 github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
18
 github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4=
18
 github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4=
19
 github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
19
 github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
20
-github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
21
-github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
22
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
20
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
23
 github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
21
 github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
24
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
22
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
25
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
23
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
26
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
24
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
25
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
26
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
27
 github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
27
 github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
28
 github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
28
 github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
29
 github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
29
 github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
30
 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
30
 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
31
-github.com/beevik/ntp v0.3.0 h1:xzVrPrE4ziasFXgBVBZJDP0Wg/KpMwk2KHJ4Ba8GrDw=
32
-github.com/beevik/ntp v0.3.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg=
31
+github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6 h1:4NNbNM2Iq/k57qEu7WfL67UrbPq1uFWxW4qODCohi+0=
32
+github.com/babolivier/go-doh-client v0.0.0-20201028162107-a76cff4cb8b6/go.mod h1:J29hk+f9lJrblVIfiJOtTFk+OblBawmib4uz/VdKzlg=
33
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
33
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
34
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
34
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
35
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
35
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
46
 github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
46
 github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
47
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
47
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
48
 github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
48
 github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
49
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
49
 github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
50
 github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
50
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
51
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
51
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
52
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
53
+github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
54
+github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
52
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
55
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
53
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
56
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
54
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
57
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
90
 github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
93
 github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
91
 github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
94
 github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
92
 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
95
 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
93
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
94
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
96
 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
95
 github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
97
 github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
96
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
98
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
97
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
99
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
98
-github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
99
-github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
100
-github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
101
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
100
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
102
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
101
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
103
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
102
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
104
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
103
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
105
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
104
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
105
+github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
106
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
106
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
107
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
108
-github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
109
-github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
110
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
107
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
111
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
108
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
112
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
109
 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
142
 github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
139
 github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
143
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
140
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
144
 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
141
 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
142
+github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k=
143
+github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
145
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
144
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
146
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
145
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
147
 github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
146
 github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
152
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
151
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
153
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
152
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
154
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
153
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
154
+github.com/kentik/patricia v0.0.0-20201202224819-f9447a6e25f1 h1:D7qhJP3R49ZjUzpzKQ6B2H3lgejPs6DTO5gRomhhOpE=
155
+github.com/kentik/patricia v0.0.0-20201202224819-f9447a6e25f1/go.mod h1:2OfLA+0esiUJpwMjrH39pEk79cb8MvGTBS9YlZpejJ4=
155
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
156
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
156
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
157
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
157
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
158
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
162
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
163
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
163
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
164
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
164
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
165
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
166
+github.com/libp2p/go-reuseport v0.0.2 h1:XSG94b1FJfGA01BUrT82imejHQyTxO4jEWqheyCXYvU=
167
+github.com/libp2p/go-reuseport v0.0.2/go.mod h1:SPD+5RwGC7rcnzngoYC86GjPzjSywuQyMVAheVBD9nQ=
165
 github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
168
 github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
166
 github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
169
 github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
167
 github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
170
 github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
171
 github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
174
 github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
172
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
175
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
173
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
176
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
177
+github.com/mccutchen/go-httpbin v1.1.1 h1:aEws49HEJEyXHLDnshQVswfUlCVoS8g6h9YaDyaW7RE=
178
+github.com/mccutchen/go-httpbin v1.1.1/go.mod h1:fhpOYavp5g2K74XDl/ao2y4KvhqVtKlkg1e+0UaQv7I=
174
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
179
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
175
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
180
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
176
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
181
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
208
 github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
213
 github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
209
 github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
214
 github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
210
 github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
215
 github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
216
+github.com/panjf2000/ants/v2 v2.4.3 h1:wHghL17YKFanB62QjPQ9o+DuM4q7WrQ7zAhoX8+eBXU=
217
+github.com/panjf2000/ants/v2 v2.4.3/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
211
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
218
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
212
 github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
219
 github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
220
+github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
221
+github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
213
 github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
222
 github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
214
 github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
223
 github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
215
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
224
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
239
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
248
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
240
 github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
249
 github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
241
 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
250
 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
251
+github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM=
242
 github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
252
 github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
243
-github.com/prometheus/common v0.18.0 h1:WCVKW7aL6LEe1uryfI9dnEc2ZqNB1Fn0ok930v0iL1Y=
244
-github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
245
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
253
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
246
 github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
254
 github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
247
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
255
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
248
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
256
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
249
 github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
257
 github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
258
+github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
250
 github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
259
 github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
251
-github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
252
-github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
253
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
260
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
254
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
261
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
255
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
262
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
263
+github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
264
+github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs=
265
+github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo=
256
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
266
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
257
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
267
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
258
 github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
268
 github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
274
 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
284
 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
275
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
285
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
276
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
286
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
287
+github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
288
+github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
289
+github.com/stretchr/testify v1.1.5-0.20170809224252-890a5c3458b4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
277
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
290
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
278
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
291
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
279
-github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
280
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
292
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
293
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
294
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
295
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
281
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
296
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
297
+github.com/tylertreat/BoomFilters v0.0.0-20200520150052-42a7b4300c0c h1:pGEq55pv/5i+G/Dy+kDsUugzvg6R02jzegxtrwOhE7A=
298
+github.com/tylertreat/BoomFilters v0.0.0-20200520150052-42a7b4300c0c/go.mod h1:OYRfF6eb5wY9VRFkXJH8FFBi3plw2v+giaIu7P054pM=
282
 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
299
 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
283
 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
300
 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
301
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
302
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
303
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
304
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
305
+github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
306
+github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
284
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
307
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
285
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
286
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
308
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
287
 go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
309
 go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
288
 go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
310
 go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
290
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
312
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
291
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
313
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
292
 go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
314
 go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
293
-go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
294
-go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
295
-go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
296
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
315
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
297
 go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
316
 go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
298
-go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
299
-go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
300
-go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
301
 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
317
 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
302
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
318
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
303
 go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
319
 go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
304
-go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=
305
-go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
306
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
320
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
307
 golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
321
 golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
308
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
322
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
310
 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
324
 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
311
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
325
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
312
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
326
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
313
-golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
314
-golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
327
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
328
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
315
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
329
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
316
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
330
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
317
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
331
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
318
 golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
332
 golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
319
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
333
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
320
 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
334
 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
321
-golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
322
-golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
323
 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
335
 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
324
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
336
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
325
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
326
-golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY=
327
-golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
328
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
337
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
329
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
338
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
330
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
339
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
342
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
351
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
343
 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
352
 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
344
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
353
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
345
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
346
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
354
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
347
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
355
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
348
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
356
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
353
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
361
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
354
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
362
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
355
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
363
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
356
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
357
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
358
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
364
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
359
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
365
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
360
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
366
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
364
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
370
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
365
 golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
371
 golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
366
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
372
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
373
+golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
367
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
374
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
368
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
375
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
369
 golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
376
 golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
370
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
377
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
371
 golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
378
 golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
372
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
373
 golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
379
 golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
374
 golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
380
 golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
375
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
381
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
376
 golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
382
 golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
377
 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
383
 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
378
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
379
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
384
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
380
 golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
385
 golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
381
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
382
-golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
383
-golang.org/x/sys v0.0.0-20210303074136-134d130e1a04 h1:cEhElsAv9LUt9ZUUocxzWe05oFLVd+AA2nstydTeI8g=
384
-golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
385
-golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
386
+golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY=
387
+golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
386
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
388
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
387
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
389
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
388
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
390
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
399
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
401
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
400
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
402
 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
401
 golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
403
 golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
404
+golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
402
 golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
405
 golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
403
 golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
406
 golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
404
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
405
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
406
 golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
407
 golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
407
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
408
-golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
409
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
410
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
408
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
411
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
409
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
410
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
412
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
411
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
413
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
414
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
415
 google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
412
 google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
416
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
413
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
417
 google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
414
 google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
421
 google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
418
 google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
422
 google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
419
 google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
423
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
420
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
424
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
425
 google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
421
 google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
426
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
422
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
427
 google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
423
 google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
431
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
427
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
432
 google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
428
 google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
433
 google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
429
 google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
434
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
435
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
430
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
436
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
431
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
437
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
432
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
438
 google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
433
 google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
439
 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
434
 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
440
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
435
+google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
441
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
436
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
442
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
443
-google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
444
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
445
-gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
446
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
437
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
447
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
438
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
448
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
439
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
460
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
451
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
461
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
452
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
462
 gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
453
 gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
463
-gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
454
+gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
464
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
455
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
456
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
457
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
465
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
458
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
466
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
459
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
467
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
460
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
468
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
461
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
469
-honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U=
470
-honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
471
 sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
462
 sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
472
 sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
463
 sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=

+ 0
- 172
hub/connection.go Просмотреть файл

1
-package hub
2
-
3
-import (
4
-	"fmt"
5
-	"math/rand"
6
-	"sync"
7
-	"time"
8
-
9
-	"github.com/9seconds/mtg/conntypes"
10
-	"github.com/9seconds/mtg/mtproto"
11
-	"github.com/9seconds/mtg/mtproto/rpc"
12
-	"github.com/9seconds/mtg/protocol"
13
-	"go.uber.org/zap"
14
-)
15
-
16
-const connectionTTL = time.Hour
17
-
18
-type connection struct {
19
-	conn            conntypes.PacketReadWriteCloser
20
-	proxyConns      map[string]*ProxyConn
21
-	closeOnce       sync.Once
22
-	proxyConnsMutex sync.RWMutex
23
-	id              int
24
-	logger          *zap.SugaredLogger
25
-
26
-	channelDone       chan struct{}
27
-	channelWrite      chan conntypes.Packet
28
-	channelRead       chan *rpc.ProxyResponse
29
-	channelConnAttach chan *ProxyConn
30
-	channelConnDetach chan conntypes.ConnID
31
-}
32
-
33
-func (c *connection) run() { // nolint: cyclop
34
-	defer c.Close()
35
-
36
-	ttl := time.NewTimer(connectionTTL)
37
-	defer ttl.Stop()
38
-
39
-	for {
40
-		select {
41
-		case <-c.channelDone:
42
-			for _, v := range c.proxyConns {
43
-				v.Close()
44
-			}
45
-
46
-			return
47
-		case <-ttl.C:
48
-			c.logger.Debugw("Closing connection by TTL")
49
-			c.Close()
50
-		case resp := <-c.channelRead:
51
-			if channel, ok := c.proxyConns[string(resp.ConnID[:])]; ok {
52
-				if resp.Type == rpc.ProxyResponseTypeCloseExt {
53
-					channel.Close()
54
-				} else {
55
-					channel.put(resp)
56
-				}
57
-			}
58
-		case packet := <-c.channelWrite:
59
-			if err := c.conn.Write(packet); err != nil {
60
-				c.logger.Debugw("Cannot write packet", "error", err)
61
-				c.Close()
62
-			}
63
-		case conn := <-c.channelConnAttach:
64
-			c.proxyConnsMutex.Lock()
65
-			c.proxyConns[string(conn.req.ConnID[:])] = conn
66
-			c.proxyConnsMutex.Unlock()
67
-			conn.channelWrite = c.channelWrite
68
-		case connID := <-c.channelConnDetach:
69
-			if conn, ok := c.proxyConns[string(connID[:])]; ok {
70
-				c.proxyConnsMutex.Lock()
71
-				delete(c.proxyConns, string(connID[:]))
72
-				c.proxyConnsMutex.Unlock()
73
-				conn.Close()
74
-			}
75
-		}
76
-	}
77
-}
78
-
79
-func (c *connection) readLoop() {
80
-	for {
81
-		packet, err := c.conn.Read()
82
-		if err != nil {
83
-			c.logger.Debugw("Cannot read packet", "error", err)
84
-			c.Close()
85
-
86
-			return
87
-		}
88
-
89
-		response, err := rpc.ParseProxyResponse(packet)
90
-		if err != nil {
91
-			c.logger.Debugw("Failed response", "error", err)
92
-
93
-			continue
94
-		}
95
-
96
-		select {
97
-		case <-c.channelDone:
98
-			return
99
-		case c.channelRead <- response:
100
-		}
101
-	}
102
-}
103
-
104
-func (c *connection) Close() {
105
-	c.closeOnce.Do(func() {
106
-		c.logger.Debugw("Closing connection")
107
-
108
-		close(c.channelDone)
109
-		c.conn.Close()
110
-	})
111
-}
112
-
113
-func (c *connection) Done() bool {
114
-	select {
115
-	case <-c.channelDone:
116
-		return true
117
-	default:
118
-		return c.Len() == 0
119
-	}
120
-}
121
-
122
-func (c *connection) Len() int {
123
-	c.proxyConnsMutex.RLock()
124
-	defer c.proxyConnsMutex.RUnlock()
125
-
126
-	return len(c.proxyConns)
127
-}
128
-
129
-func (c *connection) Attach(conn *ProxyConn) error {
130
-	select {
131
-	case <-c.channelDone:
132
-		return ErrClosed
133
-	case c.channelConnAttach <- conn:
134
-		return nil
135
-	}
136
-}
137
-
138
-func (c *connection) Detach(connID conntypes.ConnID) {
139
-	select {
140
-	case <-c.channelDone:
141
-	case c.channelConnDetach <- connID:
142
-	}
143
-}
144
-
145
-func newConnection(req *protocol.TelegramRequest) (*connection, error) {
146
-	conn, err := mtproto.TelegramProtocol(req)
147
-	if err != nil {
148
-		return nil, fmt.Errorf("cannot create a new connection: %w", err)
149
-	}
150
-
151
-	id := rand.Int() // nolint: gosec
152
-	rv := &connection{
153
-		conn: conn,
154
-		id:   id,
155
-		logger: zap.S().Named("hub-connection").With("id", id,
156
-			"dc", req.ClientProtocol.DC(),
157
-			"protocol", req.ClientProtocol.ConnectionProtocol()),
158
-		proxyConns: make(map[string]*ProxyConn),
159
-
160
-		channelRead:       make(chan *rpc.ProxyResponse, 1),
161
-		channelDone:       make(chan struct{}),
162
-		channelWrite:      make(chan conntypes.Packet),
163
-		channelConnAttach: make(chan *ProxyConn),
164
-		channelConnDetach: make(chan conntypes.ConnID),
165
-	}
166
-
167
-	go rv.readLoop()
168
-
169
-	go rv.run()
170
-
171
-	return rv, nil
172
-}

+ 0
- 70
hub/connection_list.go Просмотреть файл

1
-package hub
2
-
3
-import (
4
-	"fmt"
5
-	"sort"
6
-
7
-	"github.com/9seconds/mtg/config"
8
-)
9
-
10
-type connectionList struct {
11
-	connections []*connection
12
-}
13
-
14
-func (c *connectionList) get(conn *ProxyConn) (*connection, error) {
15
-	if len(c.connections) > 0 && c.connections[0].Len() < config.C.MultiplexPerConnection {
16
-		if err := c.connections[0].Attach(conn); err == nil {
17
-			return c.connections[0], nil
18
-		}
19
-	}
20
-
21
-	newConn, err := newConnection(conn.req)
22
-	if err != nil {
23
-		return nil, fmt.Errorf("cannot allocate a new connection: %w", err)
24
-	}
25
-
26
-	if err = newConn.Attach(conn); err != nil {
27
-		newConn.Close()
28
-
29
-		return nil, fmt.Errorf("cannot attach to the newly created connection: %w", err)
30
-	}
31
-
32
-	c.connections = append(c.connections, newConn)
33
-	lastIndex := len(c.connections) - 1
34
-	c.connections[0], c.connections[lastIndex] = c.connections[lastIndex], c.connections[0]
35
-
36
-	return newConn, nil
37
-}
38
-
39
-func (c *connectionList) gc() {
40
-	prevLen := len(c.connections)
41
-	if prevLen == 0 {
42
-		return
43
-	}
44
-
45
-	for i := len(c.connections) - 1; i >= 0; i-- {
46
-		lastIndex := len(c.connections) - 1
47
-
48
-		if c.connections[i].Done() {
49
-			c.connections[i].Close()
50
-
51
-			if len(c.connections)-1 == i {
52
-				c.connections = c.connections[:lastIndex]
53
-			} else {
54
-				c.connections[i], c.connections[lastIndex] = c.connections[lastIndex], c.connections[i]
55
-			}
56
-		}
57
-	}
58
-
59
-	if prevLen != len(c.connections) {
60
-		c.sort()
61
-	}
62
-}
63
-
64
-func (c *connectionList) sort() {
65
-	if len(c.connections) > 1 {
66
-		sort.Slice(c.connections, func(i, j int) bool {
67
-			return c.connections[i].Len() < c.connections[j].Len()
68
-		})
69
-	}
70
-}

+ 0
- 40
hub/hub.go Просмотреть файл

1
-package hub
2
-
3
-import (
4
-	"context"
5
-	"sync"
6
-
7
-	"github.com/9seconds/mtg/protocol"
8
-)
9
-
10
-type hub struct {
11
-	muxes map[int32]*mux
12
-	mutex sync.RWMutex
13
-	ctx   context.Context
14
-}
15
-
16
-func (h *hub) Register(req *protocol.TelegramRequest) (*ProxyConn, error) {
17
-	return h.getMux(req).Get(req)
18
-}
19
-
20
-func (h *hub) getMux(req *protocol.TelegramRequest) *mux {
21
-	var key int32 = 32767 + int32(req.ClientProtocol.DC()) + 100000*int32(req.ClientProtocol.ConnectionProtocol())
22
-
23
-	h.mutex.RLock()
24
-	m, ok := h.muxes[key]
25
-	h.mutex.RUnlock()
26
-
27
-	if !ok {
28
-		h.mutex.Lock()
29
-		m, ok = h.muxes[key]
30
-
31
-		if !ok {
32
-			m = newMux(h.ctx)
33
-			h.muxes[key] = m
34
-		}
35
-
36
-		h.mutex.Unlock()
37
-	}
38
-
39
-	return m
40
-}

+ 0
- 24
hub/init.go Просмотреть файл

1
-package hub
2
-
3
-import (
4
-	"context"
5
-	"errors"
6
-	"sync"
7
-)
8
-
9
-var (
10
-	ErrTimeout = errors.New("timeout")
11
-	ErrClosed  = errors.New("context is closed")
12
-
13
-	Hub      Interface
14
-	initOnce sync.Once
15
-)
16
-
17
-func Init(ctx context.Context) {
18
-	initOnce.Do(func() {
19
-		Hub = &hub{
20
-			muxes: make(map[int32]*mux),
21
-			ctx:   ctx,
22
-		}
23
-	})
24
-}

+ 0
- 7
hub/interface.go Просмотреть файл

1
-package hub
2
-
3
-import "github.com/9seconds/mtg/protocol"
4
-
5
-type Interface interface {
6
-	Register(*protocol.TelegramRequest) (*ProxyConn, error)
7
-}

+ 0
- 90
hub/mux.go Просмотреть файл

1
-package hub
2
-
3
-import (
4
-	"context"
5
-	"time"
6
-
7
-	"github.com/9seconds/mtg/conntypes"
8
-	"github.com/9seconds/mtg/protocol"
9
-)
10
-
11
-const muxGCEvery = time.Minute
12
-
13
-type muxNewRequest struct {
14
-	req  *protocol.TelegramRequest
15
-	resp chan<- muxNewResponse
16
-}
17
-
18
-type muxNewResponse struct {
19
-	conn *ProxyConn
20
-	err  error
21
-}
22
-
23
-type mux struct {
24
-	connections   connectionList
25
-	clients       map[string]*connection
26
-	ctx           context.Context
27
-	channelClosed chan conntypes.ConnID
28
-	channelNew    chan muxNewRequest
29
-}
30
-
31
-func (m *mux) run() {
32
-	gcTicker := time.NewTicker(muxGCEvery)
33
-	defer gcTicker.Stop()
34
-
35
-	for {
36
-		select {
37
-		case <-m.ctx.Done():
38
-			for _, v := range m.clients {
39
-				v.Close()
40
-			}
41
-
42
-			return
43
-		case <-gcTicker.C:
44
-			m.connections.gc()
45
-		case req := <-m.channelNew:
46
-			m.connections.gc()
47
-			proxyConn := newProxyConn(req.req, m.channelClosed)
48
-			conn, err := m.connections.get(proxyConn)
49
-
50
-			if err == nil {
51
-				m.clients[string(req.req.ConnID[:])] = conn
52
-			}
53
-
54
-			req.resp <- muxNewResponse{
55
-				conn: proxyConn,
56
-				err:  err,
57
-			}
58
-			close(req.resp)
59
-		case connID := <-m.channelClosed:
60
-			if conn, ok := m.clients[string(connID[:])]; ok {
61
-				conn.Detach(connID)
62
-				delete(m.clients, string(connID[:]))
63
-			}
64
-		}
65
-	}
66
-}
67
-
68
-func (m *mux) Get(req *protocol.TelegramRequest) (*ProxyConn, error) {
69
-	resp := make(chan muxNewResponse)
70
-	m.channelNew <- muxNewRequest{
71
-		req:  req,
72
-		resp: resp,
73
-	}
74
-
75
-	rv := <-resp
76
-
77
-	return rv.conn, rv.err
78
-}
79
-
80
-func newMux(ctx context.Context) *mux {
81
-	m := &mux{
82
-		ctx:           ctx,
83
-		clients:       make(map[string]*connection),
84
-		channelClosed: make(chan conntypes.ConnID, 1),
85
-		channelNew:    make(chan muxNewRequest),
86
-	}
87
-	go m.run()
88
-
89
-	return m
90
-}

+ 0
- 79
hub/proxy_conn.go Просмотреть файл

1
-package hub
2
-
3
-import (
4
-	"sync"
5
-	"time"
6
-
7
-	"github.com/9seconds/mtg/conntypes"
8
-	"github.com/9seconds/mtg/mtproto/rpc"
9
-	"github.com/9seconds/mtg/protocol"
10
-)
11
-
12
-const (
13
-	proxyConnWriteTimeout = 2 * time.Minute
14
-	proxyConnReadTimeout  = 2 * time.Minute
15
-
16
-	proxyConnBackpressureAfter = 10
17
-)
18
-
19
-type ProxyConn struct {
20
-	closeOnce       sync.Once
21
-	req             *protocol.TelegramRequest
22
-	channelResponse chan *rpc.ProxyResponse
23
-	channelClosed   chan<- conntypes.ConnID
24
-	channelWrite    chan<- conntypes.Packet
25
-	channelDone     chan struct{}
26
-}
27
-
28
-func (p *ProxyConn) Read() (*rpc.ProxyResponse, error) {
29
-	timer := time.NewTimer(proxyConnReadTimeout)
30
-	defer timer.Stop()
31
-
32
-	select {
33
-	case <-timer.C:
34
-		return nil, ErrTimeout
35
-	case <-p.channelDone:
36
-		return nil, ErrClosed
37
-	case packet := <-p.channelResponse:
38
-		return packet, nil
39
-	}
40
-}
41
-
42
-func (p *ProxyConn) Write(packet conntypes.Packet) error {
43
-	timer := time.NewTimer(proxyConnWriteTimeout)
44
-	defer timer.Stop()
45
-
46
-	select {
47
-	case <-timer.C:
48
-		return ErrTimeout
49
-	case <-p.channelDone:
50
-		return ErrClosed
51
-	case p.channelWrite <- packet:
52
-		return nil
53
-	}
54
-}
55
-
56
-func (p *ProxyConn) put(response *rpc.ProxyResponse) {
57
-	select {
58
-	case <-p.channelDone:
59
-	case p.channelResponse <- response:
60
-	}
61
-}
62
-
63
-func (p *ProxyConn) Close() {
64
-	p.closeOnce.Do(func() {
65
-		close(p.channelDone)
66
-		go func() {
67
-			p.channelClosed <- p.req.ConnID
68
-		}()
69
-	})
70
-}
71
-
72
-func newProxyConn(req *protocol.TelegramRequest, channelClosed chan<- conntypes.ConnID) *ProxyConn {
73
-	return &ProxyConn{
74
-		channelResponse: make(chan *rpc.ProxyResponse, proxyConnBackpressureAfter),
75
-		channelDone:     make(chan struct{}),
76
-		channelClosed:   channelClosed,
77
-		req:             req,
78
-	}
79
-}

+ 192
- 0
internal/cli/access.go Просмотреть файл

1
+package cli
2
+
3
+import (
4
+	"context"
5
+	"encoding/json"
6
+	"fmt"
7
+	"io"
8
+	"net"
9
+	"net/http"
10
+	"net/url"
11
+	"os"
12
+	"strconv"
13
+	"strings"
14
+	"sync"
15
+)
16
+
17
+type accessResponse struct {
18
+	IPv4   *accessResponseURLs `json:"ipv4,omitempty"`
19
+	IPv6   *accessResponseURLs `json:"ipv6,omitempty"`
20
+	Secret struct {
21
+		Hex    string `json:"hex"`
22
+		Base64 string `json:"base64"`
23
+	} `json:"secret"`
24
+}
25
+
26
+type accessResponseURLs struct {
27
+	IP        net.IP `json:"ip"`
28
+	Port      uint   `json:"port"`
29
+	TgURL     string `json:"tg_url"`
30
+	TgQrCode  string `json:"tg_qrcode"`
31
+	TmeURL    string `json:"tme_url"`
32
+	TmeQrCode string `json:"tme_qrcode"`
33
+}
34
+
35
+type Access struct {
36
+	base
37
+
38
+	PublicIPv4 net.IP `kong:"help='Public IPv4 address for proxy. By default it is resolved via remote website',name='ipv4',short='i'"`   // nolint: lll
39
+	PublicIPv6 net.IP `kong:"help='Public IPv6 address for proxy. By default it is resolved via remote website',name='ipv6',short='I'"`   // nolint: lll
40
+	Port       uint   `kong:"help='Port number. Default port is taken from configuration file, bind-to parameter',type:'uint',short='p'"` // nolint: lll
41
+	Hex        bool   `kong:"help='Print secret in hex encoding.',short='x'"`
42
+}
43
+
44
+func (c *Access) Run(cli *CLI, version string) error {
45
+	if err := c.ReadConfig(version); err != nil {
46
+		return fmt.Errorf("cannot init config: %w", err)
47
+	}
48
+
49
+	return c.Execute(cli)
50
+}
51
+
52
+func (c *Access) Execute(cli *CLI) error {
53
+	resp := &accessResponse{}
54
+	resp.Secret.Base64 = c.Config.Secret.Base64()
55
+	resp.Secret.Hex = c.Config.Secret.Hex()
56
+
57
+	wg := &sync.WaitGroup{}
58
+	wg.Add(2) // nolint: gomnd
59
+
60
+	go func() {
61
+		defer wg.Done()
62
+
63
+		ip := cli.Access.PublicIPv4
64
+		if ip == nil {
65
+			ip = c.getIP("tcp4")
66
+		}
67
+
68
+		if ip != nil {
69
+			ip = ip.To4()
70
+		}
71
+
72
+		resp.IPv4 = c.makeURLs(ip, cli)
73
+	}()
74
+
75
+	go func() {
76
+		defer wg.Done()
77
+
78
+		ip := cli.Access.PublicIPv6
79
+		if ip == nil {
80
+			ip = c.getIP("tcp6")
81
+		}
82
+
83
+		if ip != nil {
84
+			ip = ip.To16()
85
+		}
86
+
87
+		resp.IPv6 = c.makeURLs(ip, cli)
88
+	}()
89
+
90
+	wg.Wait()
91
+
92
+	encoder := json.NewEncoder(os.Stdout)
93
+	encoder.SetEscapeHTML(false)
94
+	encoder.SetIndent("", "  ")
95
+
96
+	if err := encoder.Encode(resp); err != nil {
97
+		return fmt.Errorf("cannot dump access json: %w", err)
98
+	}
99
+
100
+	return nil
101
+}
102
+
103
+func (c *Access) getIP(protocol string) net.IP {
104
+	client := c.Network.MakeHTTPClient(func(ctx context.Context, network, address string) (net.Conn, error) {
105
+		return c.Network.DialContext(ctx, protocol, address)
106
+	})
107
+
108
+	req, err := http.NewRequest(http.MethodGet, "https://ifconfig.co", nil) // nolint: noctx
109
+	if err != nil {
110
+		panic(err)
111
+	}
112
+
113
+	req.Header.Add("Accept", "text/plain")
114
+
115
+	resp, err := client.Do(req)
116
+	if err != nil {
117
+		return nil
118
+	}
119
+
120
+	if resp.StatusCode != http.StatusOK {
121
+		return nil
122
+	}
123
+
124
+	defer func() {
125
+		io.Copy(io.Discard, resp.Body) // nolint: errcheck
126
+		resp.Body.Close()
127
+	}()
128
+
129
+	data, err := io.ReadAll(resp.Body)
130
+	if err != nil {
131
+		return nil
132
+	}
133
+
134
+	return net.ParseIP(strings.TrimSpace(string(data)))
135
+}
136
+
137
+func (c *Access) makeURLs(ip net.IP, cli *CLI) *accessResponseURLs {
138
+	if ip == nil {
139
+		return nil
140
+	}
141
+
142
+	portNo := cli.Access.Port
143
+	if portNo == 0 {
144
+		portNo = c.Config.BindTo.PortValue(0)
145
+	}
146
+
147
+	values := url.Values{}
148
+	values.Set("server", ip.String())
149
+	values.Set("port", strconv.Itoa(int(portNo)))
150
+
151
+	if cli.Access.Hex {
152
+		values.Set("secret", c.Config.Secret.Hex())
153
+	} else {
154
+		values.Set("secret", c.Config.Secret.Base64())
155
+	}
156
+
157
+	urlQuery := values.Encode()
158
+
159
+	rv := &accessResponseURLs{
160
+		IP:   ip,
161
+		Port: portNo,
162
+		TgURL: (&url.URL{
163
+			Scheme:   "tg",
164
+			Host:     "proxy",
165
+			RawQuery: urlQuery,
166
+		}).String(),
167
+		TmeURL: (&url.URL{
168
+			Scheme:   "https",
169
+			Host:     "t.me",
170
+			Path:     "proxy",
171
+			RawQuery: urlQuery,
172
+		}).String(),
173
+	}
174
+	rv.TgQrCode = c.makeQRCode(rv.TgURL)
175
+	rv.TmeQrCode = c.makeQRCode(rv.TmeURL)
176
+
177
+	return rv
178
+}
179
+
180
+func (c *Access) makeQRCode(data string) string {
181
+	values := url.Values{}
182
+	values.Set("qzone", "4")
183
+	values.Set("format", "svg")
184
+	values.Set("data", data)
185
+
186
+	return (&url.URL{
187
+		Scheme:   "https",
188
+		Host:     "api.qrserver.com",
189
+		Path:     "v1/create-qr-code",
190
+		RawQuery: values.Encode(),
191
+	}).String()
192
+}

+ 197
- 0
internal/cli/access_test.go Просмотреть файл

1
+package cli_test
2
+
3
+import (
4
+	"net"
5
+	"net/http"
6
+	"testing"
7
+
8
+	"github.com/9seconds/mtg/v2/internal/config"
9
+	"github.com/9seconds/mtg/v2/internal/testlib"
10
+	"github.com/9seconds/mtg/v2/mtglib"
11
+	"github.com/jarcoal/httpmock"
12
+	"github.com/stretchr/testify/suite"
13
+	"github.com/xeipuuv/gojsonschema"
14
+)
15
+
16
+var accressResponseJSONSchema = func() *gojsonschema.Schema {
17
+	schema, err := gojsonschema.NewSchema(gojsonschema.NewStringLoader(`
18
+{
19
+    "type": "object",
20
+    "required": ["secret"],
21
+    "additionalProperties": true,
22
+    "properties": {
23
+        "secret": {
24
+            "type": "object",
25
+            "required": [
26
+                "hex",
27
+                "base64"
28
+            ],
29
+            "additionalProperties": false,
30
+            "properties": {
31
+                "hex": {
32
+                    "type": "string",
33
+                    "minLength": 34
34
+                },
35
+                "base64": {
36
+                    "type": "string",
37
+                    "minLength": 10
38
+                }
39
+            }
40
+        },
41
+        "ipv4": {
42
+            "$ref": "#/definitions/ip"
43
+        },
44
+        "ipv6": {
45
+            "$ref": "#/definitions/ip"
46
+        }
47
+    },
48
+    "definitions": {
49
+        "ip": {
50
+            "type": "object",
51
+            "required": [
52
+                "ip",
53
+                "port",
54
+                "tg_url",
55
+                "tg_qrcode",
56
+                "tme_url",
57
+                "tme_qrcode"
58
+            ],
59
+            "additionalProperties": false,
60
+            "properties": {
61
+                "ip": {
62
+                    "type": "string",
63
+                    "minLength": 1,
64
+                    "anyOf": [
65
+                        {
66
+                            "format": "ipv4"
67
+                        },
68
+                        {
69
+                            "format": "ipv6"
70
+                        }
71
+                    ]
72
+                },
73
+                "port": {
74
+                    "type": "integer",
75
+                    "multipleOf": 1.0,
76
+                    "exclusiveMinimum": 0,
77
+                    "exclusiveMaximum": 65536
78
+                },
79
+                "tg_url": {
80
+                    "type": "string",
81
+                    "minLength": 1,
82
+                    "format": "uri"
83
+                },
84
+                "tg_qrcode": {
85
+                    "type": "string",
86
+                    "minLength": 1,
87
+                    "format": "uri"
88
+                },
89
+                "tme_url": {
90
+                    "type": "string",
91
+                    "minLength": 1,
92
+                    "format": "uri"
93
+                },
94
+                "tme_qrcode": {
95
+                    "type": "string",
96
+                    "minLength": 1,
97
+                    "format": "uri"
98
+                }
99
+            }
100
+        }
101
+    }
102
+}
103
+    `))
104
+	if err != nil {
105
+		panic(err)
106
+	}
107
+
108
+	return schema
109
+}()
110
+
111
+type AccessTestSuite struct {
112
+	CommonTestSuite
113
+}
114
+
115
+func (suite *AccessTestSuite) SetupTest() {
116
+	suite.CommonTestSuite.SetupTest()
117
+
118
+	suite.cli.Access.Config = &config.Config{}
119
+	suite.cli.Access.Config.Secret = mtglib.GenerateSecret("google.com")
120
+	suite.cli.Access.Network = suite.networkMock
121
+
122
+	suite.NoError(
123
+		suite.cli.Access.Config.BindTo.UnmarshalText([]byte("0.0.0.0:80")))
124
+}
125
+
126
+func (suite *AccessTestSuite) TestGenerateNoCalls() {
127
+	suite.cli.Access.PublicIPv4 = net.ParseIP("10.0.0.10")
128
+	suite.cli.Access.PublicIPv6 = net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")
129
+
130
+	output := testlib.CaptureStdout(func() {
131
+		suite.NoError(suite.cli.Access.Execute(suite.cli))
132
+	})
133
+
134
+	validated, err := accressResponseJSONSchema.Validate(
135
+		gojsonschema.NewStringLoader(output))
136
+	suite.NoError(err)
137
+	suite.Empty(validated.Errors())
138
+	suite.True(validated.Valid())
139
+
140
+	suite.Contains(output, "10.0.0.10")
141
+	suite.Contains(output, "2001:db8:85a3::8a2e:370:7334")
142
+	suite.Contains(output, "ipv4")
143
+	suite.Contains(output, "ipv6")
144
+	suite.Contains(output, suite.cli.Access.Config.Secret.Base64())
145
+	suite.Contains(output, suite.cli.Access.Config.Secret.Hex())
146
+}
147
+
148
+func (suite *AccessTestSuite) TestGenerateIPv4Call() {
149
+	suite.cli.Access.PublicIPv6 = net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")
150
+
151
+	httpmock.RegisterResponder(http.MethodGet, "https://ifconfig.co",
152
+		httpmock.NewStringResponder(http.StatusOK, "10.11.12.13"))
153
+
154
+	output := testlib.CaptureStdout(func() {
155
+		suite.NoError(suite.cli.Access.Execute(suite.cli))
156
+	})
157
+
158
+	validated, err := accressResponseJSONSchema.Validate(
159
+		gojsonschema.NewStringLoader(output))
160
+	suite.NoError(err)
161
+	suite.Empty(validated.Errors())
162
+	suite.True(validated.Valid())
163
+
164
+	suite.Contains(output, "10.11.12.13")
165
+	suite.Contains(output, "2001:db8:85a3::8a2e:370:7334")
166
+	suite.Contains(output, "ipv4")
167
+	suite.Contains(output, "ipv6")
168
+	suite.Contains(output, suite.cli.Access.Config.Secret.Base64())
169
+	suite.Contains(output, suite.cli.Access.Config.Secret.Hex())
170
+}
171
+
172
+func (suite *AccessTestSuite) TestIPv4CallFail() {
173
+	suite.cli.Access.PublicIPv6 = net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")
174
+
175
+	httpmock.RegisterResponder(http.MethodGet, "https://ifconfig.co",
176
+		httpmock.NewStringResponder(http.StatusForbidden, ""))
177
+
178
+	output := testlib.CaptureStdout(func() {
179
+		suite.NoError(suite.cli.Access.Execute(suite.cli))
180
+	})
181
+
182
+	validated, err := accressResponseJSONSchema.Validate(
183
+		gojsonschema.NewStringLoader(output))
184
+	suite.NoError(err)
185
+	suite.Empty(validated.Errors())
186
+	suite.True(validated.Valid())
187
+
188
+	suite.Contains(output, "2001:db8:85a3::8a2e:370:7334")
189
+	suite.NotContains(output, "ipv4")
190
+	suite.Contains(output, "ipv6")
191
+	suite.Contains(output, suite.cli.Access.Config.Secret.Base64())
192
+	suite.Contains(output, suite.cli.Access.Config.Secret.Hex())
193
+}
194
+
195
+func TestAccess(t *testing.T) { // nolint: paralleltest
196
+	suite.Run(t, &AccessTestSuite{})
197
+}

+ 81
- 0
internal/cli/base.go Просмотреть файл

1
+package cli
2
+
3
+import (
4
+	"fmt"
5
+	"net"
6
+	"net/url"
7
+	"os"
8
+
9
+	"github.com/9seconds/mtg/v2/internal/config"
10
+	"github.com/9seconds/mtg/v2/mtglib"
11
+	"github.com/9seconds/mtg/v2/network"
12
+)
13
+
14
+type base struct {
15
+	ConfigPath string `kong:"arg,required,type='existingfile',help='Path to the configuration file.',name='config-path'"` // nolint: lll
16
+
17
+	Network mtglib.Network `kong:"-"`
18
+	Config  *config.Config `kong:"-"`
19
+}
20
+
21
+func (b *base) ReadConfig(version string) error {
22
+	content, err := os.ReadFile(b.ConfigPath)
23
+	if err != nil {
24
+		return fmt.Errorf("cannot read config file: %w", err)
25
+	}
26
+
27
+	conf, err := config.Parse(content)
28
+	if err != nil {
29
+		return fmt.Errorf("cannot parse config: %w", err)
30
+	}
31
+
32
+	ntw, err := b.makeNetwork(conf, version)
33
+	if err != nil {
34
+		return fmt.Errorf("cannot build a network: %w", err)
35
+	}
36
+
37
+	b.Config = conf
38
+	b.Network = ntw
39
+
40
+	return nil
41
+}
42
+
43
+func (b *base) makeNetwork(conf *config.Config, version string) (mtglib.Network, error) {
44
+	tcpTimeout := conf.Network.Timeout.TCP.Value(network.DefaultTimeout)
45
+	httpTimeout := conf.Network.Timeout.HTTP.Value(network.DefaultHTTPTimeout)
46
+	dohIP := conf.Network.DOHIP.Value(net.ParseIP(network.DefaultDOHHostname)).String()
47
+	bufferSize := conf.TCPBuffer.Value(network.DefaultBufferSize)
48
+	userAgent := "mtg/" + version
49
+
50
+	baseDialer, err := network.NewDefaultDialer(tcpTimeout, int(bufferSize))
51
+	if err != nil {
52
+		return nil, fmt.Errorf("cannot build a default dialer: %w", err)
53
+	}
54
+
55
+	proxyURLs := make([]*url.URL, 0, len(conf.Network.Proxies))
56
+
57
+	for _, v := range conf.Network.Proxies {
58
+		if value := v.Value(nil); value != nil {
59
+			proxyURLs = append(proxyURLs, v.Value(nil))
60
+		}
61
+	}
62
+
63
+	switch len(proxyURLs) {
64
+	case 0:
65
+		return network.NewNetwork(baseDialer, userAgent, dohIP, httpTimeout)
66
+	case 1:
67
+		socksDialer, err := network.NewSocks5Dialer(baseDialer, proxyURLs[0])
68
+		if err != nil {
69
+			return nil, fmt.Errorf("cannot build socks5 dialer: %w", err)
70
+		}
71
+
72
+		return network.NewNetwork(socksDialer, userAgent, dohIP, httpTimeout)
73
+	}
74
+
75
+	socksDialer, err := network.NewLoadBalancedSocks5Dialer(baseDialer, proxyURLs)
76
+	if err != nil {
77
+		return nil, fmt.Errorf("cannot build socks5 dialer: %w", err)
78
+	}
79
+
80
+	return network.NewNetwork(socksDialer, userAgent, dohIP, httpTimeout)
81
+}

+ 33
- 0
internal/cli/base_internal_test.go Просмотреть файл

1
+package cli
2
+
3
+import (
4
+	"path/filepath"
5
+	"testing"
6
+
7
+	"github.com/stretchr/testify/suite"
8
+)
9
+
10
+type BaseTestSuite struct {
11
+	suite.Suite
12
+
13
+	b base
14
+}
15
+
16
+func (suite *BaseTestSuite) SetupTest() {
17
+	suite.b = base{}
18
+}
19
+
20
+func (suite *BaseTestSuite) TestReadConfigNok() {
21
+	suite.b.ConfigPath = filepath.Join("testdata", "unknown")
22
+	suite.Error(suite.b.ReadConfig("dev"))
23
+}
24
+
25
+func (suite *BaseTestSuite) TestReadConfig() {
26
+	suite.b.ConfigPath = filepath.Join("testdata", "minimal.toml")
27
+	suite.NoError(suite.b.ReadConfig("dev"))
28
+}
29
+
30
+func TestBase(t *testing.T) {
31
+	t.Parallel()
32
+	suite.Run(t, &BaseTestSuite{})
33
+}

+ 10
- 0
internal/cli/cli.go Просмотреть файл

1
+package cli
2
+
3
+import "github.com/alecthomas/kong"
4
+
5
+type CLI struct {
6
+	GenerateSecret GenerateSecret   `kong:"cmd,help='Generate new proxy secret'"`
7
+	Access         Access           `kong:"cmd,help='Print access information.'"`
8
+	Run            Proxy            `kong:"cmd,help='Run proxy.'"`
9
+	Version        kong.VersionFlag `kong:"help='Print version.',short='v'"`
10
+}

+ 24
- 0
internal/cli/generate_secret.go Просмотреть файл

1
+package cli
2
+
3
+import (
4
+	"fmt"
5
+
6
+	"github.com/9seconds/mtg/v2/mtglib"
7
+)
8
+
9
+type GenerateSecret struct {
10
+	HostName string `kong:"arg,required,help='Hostname to use for domain fronting.',name='hostname'"`
11
+	Hex      bool   `kong:"help='Print secret in hex encoding.',short='x'"`
12
+}
13
+
14
+func (c *GenerateSecret) Run(cli *CLI, _ string) error {
15
+	secret := mtglib.GenerateSecret(cli.GenerateSecret.HostName)
16
+
17
+	if cli.GenerateSecret.Hex {
18
+		fmt.Println(secret.Hex()) // nolint: forbidigo
19
+	} else {
20
+		fmt.Println(secret.Base64()) // nolint: forbidigo
21
+	}
22
+
23
+	return nil
24
+}

+ 51
- 0
internal/cli/generate_secret_test.go Просмотреть файл

1
+package cli_test
2
+
3
+import (
4
+	"strings"
5
+	"testing"
6
+
7
+	"github.com/9seconds/mtg/v2/internal/testlib"
8
+	"github.com/9seconds/mtg/v2/mtglib"
9
+	"github.com/stretchr/testify/suite"
10
+)
11
+
12
+type GenerateSecretTestSuite struct {
13
+	CommonTestSuite
14
+}
15
+
16
+func (suite *GenerateSecretTestSuite) SetupTest() {
17
+	suite.CommonTestSuite.SetupTest()
18
+
19
+	suite.cli.GenerateSecret.HostName = "google.com"
20
+}
21
+
22
+func (suite *GenerateSecretTestSuite) TestDefault() {
23
+	output := testlib.CaptureStdout(func() {
24
+		suite.NoError(suite.cli.GenerateSecret.Run(suite.cli, "dev"))
25
+	})
26
+	suite.True(strings.HasPrefix(output, "7"))
27
+
28
+	secret, err := mtglib.ParseSecret(output)
29
+	suite.NoError(err)
30
+	suite.True(secret.Valid())
31
+	suite.Equal("google.com", secret.Host)
32
+}
33
+
34
+func (suite *GenerateSecretTestSuite) TestHex() {
35
+	suite.cli.GenerateSecret.Hex = true
36
+
37
+	output := testlib.CaptureStdout(func() {
38
+		suite.NoError(suite.cli.GenerateSecret.Run(suite.cli, "dev"))
39
+	})
40
+	suite.True(strings.HasPrefix(output, "ee"))
41
+
42
+	secret, err := mtglib.ParseSecret(output)
43
+	suite.NoError(err)
44
+	suite.True(secret.Valid())
45
+	suite.Equal("google.com", secret.Host)
46
+}
47
+
48
+func TestGenerateSecret(t *testing.T) {
49
+	t.Parallel()
50
+	suite.Run(t, &GenerateSecretTestSuite{})
51
+}

+ 37
- 0
internal/cli/init_test.go Просмотреть файл

1
+package cli_test
2
+
3
+import (
4
+	"net/http"
5
+
6
+	"github.com/9seconds/mtg/v2/internal/cli"
7
+	"github.com/9seconds/mtg/v2/internal/testlib"
8
+	"github.com/jarcoal/httpmock"
9
+	"github.com/stretchr/testify/mock"
10
+	"github.com/stretchr/testify/suite"
11
+)
12
+
13
+type CommonTestSuite struct {
14
+	suite.Suite
15
+
16
+	cli         *cli.CLI
17
+	networkMock *testlib.MtglibNetworkMock
18
+	httpClient  *http.Client
19
+}
20
+
21
+func (suite *CommonTestSuite) SetupTest() {
22
+	suite.networkMock = &testlib.MtglibNetworkMock{}
23
+	suite.httpClient = &http.Client{}
24
+	suite.cli = &cli.CLI{}
25
+
26
+	httpmock.ActivateNonDefault(suite.httpClient)
27
+
28
+	suite.networkMock.
29
+		On("MakeHTTPClient", mock.Anything).
30
+		Maybe().
31
+		Return(suite.httpClient)
32
+}
33
+
34
+func (suite *CommonTestSuite) TearDownTest() {
35
+	suite.networkMock.AssertExpectations(suite.T())
36
+	httpmock.DeactivateAndReset()
37
+}

+ 167
- 0
internal/cli/proxy.go Просмотреть файл

1
+package cli
2
+
3
+import (
4
+	"fmt"
5
+	"net"
6
+	"os"
7
+
8
+	"github.com/9seconds/mtg/v2/antireplay"
9
+	"github.com/9seconds/mtg/v2/events"
10
+	"github.com/9seconds/mtg/v2/internal/utils"
11
+	"github.com/9seconds/mtg/v2/ipblocklist"
12
+	"github.com/9seconds/mtg/v2/logger"
13
+	"github.com/9seconds/mtg/v2/mtglib"
14
+	"github.com/9seconds/mtg/v2/stats"
15
+	"github.com/rs/zerolog"
16
+)
17
+
18
+type Proxy struct {
19
+	base
20
+}
21
+
22
+func (c *Proxy) Run(cli *CLI, version string) error {
23
+	if err := c.ReadConfig(version); err != nil {
24
+		return fmt.Errorf("cannot init config: %w", err)
25
+	}
26
+
27
+	return c.Execute()
28
+}
29
+
30
+func (c *Proxy) Execute() error {
31
+	zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
32
+	zerolog.TimestampFieldName = "timestamp"
33
+	zerolog.LevelFieldName = "level"
34
+
35
+	if c.Config.Debug {
36
+		zerolog.SetGlobalLevel(zerolog.DebugLevel)
37
+	} else {
38
+		zerolog.SetGlobalLevel(zerolog.WarnLevel)
39
+	}
40
+
41
+	ctx := utils.RootContext()
42
+	opts := mtglib.ProxyOpts{
43
+		Logger:          logger.NewZeroLogger(zerolog.New(os.Stdout).With().Timestamp().Logger()),
44
+		Network:         c.Network,
45
+		AntiReplayCache: antireplay.NewNoop(),
46
+		IPBlocklist:     ipblocklist.NewNoop(),
47
+		EventStream:     events.NewNoopStream(),
48
+
49
+		Secret:             c.Config.Secret,
50
+		BufferSize:         c.Config.TCPBuffer.Value(mtglib.DefaultBufferSize),
51
+		DomainFrontingPort: c.Config.DomainFrontingPort.Value(mtglib.DefaultDomainFrontingPort),
52
+		IdleTimeout:        c.Config.Network.Timeout.Idle.Value(mtglib.DefaultIdleTimeout),
53
+		PreferIP:           c.Config.PreferIP.Value(mtglib.DefaultPreferIP),
54
+	}
55
+
56
+	opts.Logger.BindStr("configuration", c.Config.String()).Debug("configuration")
57
+
58
+	c.setupAntiReplayCache(&opts)
59
+
60
+	if err := c.setupIPBlocklist(&opts); err != nil {
61
+		return fmt.Errorf("cannot setup ipblocklist: %w", err)
62
+	}
63
+
64
+	if err := c.setupEventStream(&opts); err != nil {
65
+		return fmt.Errorf("cannot setup event stream: %w", err)
66
+	}
67
+
68
+	proxy, err := mtglib.NewProxy(opts)
69
+	if err != nil {
70
+		return fmt.Errorf("cannot create a proxy: %w", err)
71
+	}
72
+
73
+	listener, err := net.Listen("tcp", c.Config.BindTo.String())
74
+	if err != nil {
75
+		return fmt.Errorf("cannot start proxy: %w", err)
76
+	}
77
+
78
+	go proxy.Serve(listener) // nolint: errcheck
79
+
80
+	<-ctx.Done()
81
+	listener.Close()
82
+	proxy.Shutdown()
83
+
84
+	return nil
85
+}
86
+
87
+func (c *Proxy) setupAntiReplayCache(opts *mtglib.ProxyOpts) {
88
+	if !c.Config.Defense.AntiReplay.Enabled {
89
+		return
90
+	}
91
+
92
+	opts.AntiReplayCache = antireplay.NewStableBloomFilter(
93
+		c.Config.Defense.AntiReplay.MaxSize.Value(antireplay.DefaultStableBloomFilterMaxSize),
94
+		c.Config.Defense.AntiReplay.ErrorRate.Value(antireplay.DefaultStableBloomFilterErrorRate),
95
+	)
96
+}
97
+
98
+func (c *Proxy) setupIPBlocklist(opts *mtglib.ProxyOpts) error {
99
+	if !c.Config.Defense.Blocklist.Enabled {
100
+		return nil
101
+	}
102
+
103
+	remoteURLs := []string{}
104
+	localFiles := []string{}
105
+
106
+	for _, v := range c.Config.Defense.Blocklist.URLs {
107
+		if v.IsRemote() {
108
+			remoteURLs = append(remoteURLs, v.String())
109
+		} else {
110
+			localFiles = append(localFiles, v.String())
111
+		}
112
+	}
113
+
114
+	firehol, err := ipblocklist.NewFirehol(opts.Logger.Named("ipblockist"),
115
+		c.Network,
116
+		c.Config.Defense.Blocklist.DownloadConcurrency,
117
+		remoteURLs,
118
+		localFiles)
119
+	if err != nil {
120
+		return err // nolint: wrapcheck
121
+	}
122
+
123
+	go firehol.Run(c.Config.Defense.Blocklist.UpdateEach.Value(ipblocklist.DefaultFireholUpdateEach))
124
+
125
+	opts.IPBlocklist = firehol
126
+
127
+	return nil
128
+}
129
+
130
+func (c *Proxy) setupEventStream(opts *mtglib.ProxyOpts) error {
131
+	factories := make([]events.ObserverFactory, 0, 2)
132
+
133
+	if c.Config.Stats.StatsD.Enabled {
134
+		statsdFactory, err := stats.NewStatsd(
135
+			c.Config.Stats.StatsD.Address.String(),
136
+			opts.Logger.Named("statsd"),
137
+			c.Config.Stats.StatsD.MetricPrefix.Value(stats.DefaultStatsdMetricPrefix),
138
+			c.Config.Stats.StatsD.TagFormat.Value(stats.DefaultStatsdTagFormat))
139
+		if err != nil {
140
+			return fmt.Errorf("cannot build statsd observer: %w", err)
141
+		}
142
+
143
+		factories = append(factories, statsdFactory.Make)
144
+	}
145
+
146
+	if c.Config.Stats.Prometheus.Enabled {
147
+		prometheus := stats.NewPrometheus(
148
+			c.Config.Stats.Prometheus.MetricPrefix.Value(stats.DefaultMetricPrefix),
149
+			c.Config.Stats.Prometheus.HTTPPath.Value("/"),
150
+		)
151
+
152
+		listener, err := net.Listen("tcp", c.Config.Stats.Prometheus.BindTo.String())
153
+		if err != nil {
154
+			return fmt.Errorf("cannot start a listener for prometheus: %w", err)
155
+		}
156
+
157
+		go prometheus.Serve(listener) // nolint: errcheck
158
+
159
+		factories = append(factories, prometheus.Make)
160
+	}
161
+
162
+	if len(factories) > 0 {
163
+		opts.EventStream = events.NewEventStream(factories)
164
+	}
165
+
166
+	return nil
167
+}

+ 2
- 0
internal/cli/testdata/minimal.toml Просмотреть файл

1
+secret = "7mqFMMq3P2Tvvt_rPx5qhmFnb29nbGUuY29t"
2
+bind-to = "0.0.0.0:80"

+ 157
- 0
internal/config/config.go Просмотреть файл

1
+package config
2
+
3
+import (
4
+	"bytes"
5
+	"encoding/json"
6
+	"fmt"
7
+
8
+	"github.com/9seconds/mtg/v2/mtglib"
9
+	"github.com/pelletier/go-toml"
10
+)
11
+
12
+type Config struct {
13
+	Debug                bool          `json:"debug"`
14
+	Secret               mtglib.Secret `json:"secret"`
15
+	BindTo               TypeHostPort  `json:"bind-to"`
16
+	TCPBuffer            TypeBytes     `json:"tcp-buffer"`
17
+	PreferIP             TypePreferIP  `json:"prefer-ip"`
18
+	DomainFrontingPort   TypePort      `json:"domain-fronting-port"`
19
+	TolerateTimeSkewness TypeDuration  `json:"tolerate-time-skewness"`
20
+	Concurrency          uint          `json:"concurrency"`
21
+	Defense              struct {
22
+		AntiReplay struct {
23
+			Enabled   bool          `json:"enabled"`
24
+			MaxSize   TypeBytes     `json:"max-size"`
25
+			ErrorRate TypeErrorRate `json:"error-rate"`
26
+		} `json:"anti-replay"`
27
+		Blocklist struct {
28
+			Enabled             bool               `json:"enabled"`
29
+			DownloadConcurrency uint               `json:"download-concurrency"`
30
+			URLs                []TypeBlocklistURI `json:"urls"`
31
+			UpdateEach          TypeDuration       `json:"update-each"`
32
+		} `json:"blocklist"`
33
+	} `json:"defense"`
34
+	Network struct {
35
+		Timeout struct {
36
+			TCP  TypeDuration `json:"tcp"`
37
+			HTTP TypeDuration `json:"http"`
38
+			Idle TypeDuration `json:"idle"`
39
+		} `json:"timeout"`
40
+		DOHIP   TypeIP    `json:"doh-ip"`
41
+		Proxies []TypeURL `json:"proxies"`
42
+	} `json:"network"`
43
+	Stats struct {
44
+		StatsD struct {
45
+			Enabled      bool                `json:"enabled"`
46
+			Address      TypeHostPort        `json:"address"`
47
+			MetricPrefix TypeMetricPrefix    `json:"metric-prefix"`
48
+			TagFormat    TypeStatsdTagFormat `json:"tag-format"`
49
+		} `json:"statsd"`
50
+		Prometheus struct {
51
+			Enabled      bool             `json:"enabled"`
52
+			BindTo       TypeHostPort     `json:"bind-to"`
53
+			HTTPPath     TypeHTTPPath     `json:"http-path"`
54
+			MetricPrefix TypeMetricPrefix `json:"metric-prefix"`
55
+		} `json:"prometheus"`
56
+	} `json:"stats"`
57
+}
58
+
59
+func (c *Config) Validate() error {
60
+	if !c.Secret.Valid() {
61
+		return fmt.Errorf("invalid secret %s", c.Secret.String())
62
+	}
63
+
64
+	if len(c.BindTo.HostValue(nil)) == 0 || c.BindTo.PortValue(0) == 0 {
65
+		return fmt.Errorf("incorrect bind-to parameter %s", c.BindTo.String())
66
+	}
67
+
68
+	return nil
69
+}
70
+
71
+func (c *Config) String() string {
72
+	buf := &bytes.Buffer{}
73
+	encoder := json.NewEncoder(buf)
74
+
75
+	encoder.SetEscapeHTML(false)
76
+
77
+	if err := encoder.Encode(c); err != nil {
78
+		panic(err)
79
+	}
80
+
81
+	return buf.String()
82
+}
83
+
84
+type configRaw struct {
85
+	Debug                bool   `toml:"debug" json:"debug,omitempty"`
86
+	Secret               string `toml:"secret" json:"secret"`
87
+	BindTo               string `toml:"bind-to" json:"bind-to"`
88
+	TCPBuffer            string `toml:"tcp-buffer" json:"tcp-buffer,omitempty"`
89
+	PreferIP             string `toml:"prefer-ip" json:"prefer-ip,omitempty"`
90
+	DomainFrontingPort   uint   `toml:"domain-fronting-port" json:"domain-fronting-port,omitempty"`
91
+	TolerateTimeSkewness string `toml:"tolerate-time-skewness" json:"tolerate-time-skewness,omitempty"`
92
+	Concurrency          uint   `toml:"concurrency" json:"concurrency,omitempty"`
93
+	Defense              struct {
94
+		AntiReplay struct {
95
+			Enabled   bool    `toml:"enabled" json:"enabled,omitempty"`
96
+			MaxSize   string  `toml:"max-size" json:"max-size,omitempty"`
97
+			ErrorRate float64 `toml:"error-rate" json:"error-rate,omitempty"`
98
+		} `toml:"anti-replay" json:"anti-replay,omitempty"`
99
+		Blocklist struct {
100
+			Enabled             bool     `toml:"enabled" json:"enabled,omitempty"`
101
+			DownloadConcurrency uint     `toml:"download-concurrency" json:"download-concurrency,omitempty"`
102
+			URLs                []string `toml:"urls" json:"urls,omitempty"`
103
+			UpdateEach          string   `toml:"update-each" json:"update-each,omitempty"`
104
+		} `toml:"blocklist" json:"blocklist,omitempty"`
105
+	} `toml:"defense" json:"defense,omitempty"`
106
+	Network struct {
107
+		Timeout struct {
108
+			TCP  string `toml:"tcp" json:"tcp,omitempty"`
109
+			HTTP string `toml:"http" json:"http,omitempty"`
110
+			Idle string `toml:"idle" json:"idle,omitempty"`
111
+		} `toml:"timeout" json:"timeout,omitempty"`
112
+		DOHIP   string   `toml:"doh-ip" json:"doh-ip,omitempty"`
113
+		Proxies []string `toml:"proxies" json:"proxies,omitempty"`
114
+	} `toml:"network" json:"network,omitempty"`
115
+	Stats struct {
116
+		StatsD struct {
117
+			Enabled      bool   `toml:"enabled" json:"enabled,omitempty"`
118
+			Address      string `toml:"address" json:"address,omitempty"`
119
+			MetricPrefix string `toml:"metric-prefix" json:"metric-prefix,omitempty"`
120
+			TagFormat    string `toml:"tag-format" json:"tag-format,omitempty"`
121
+		} `toml:"statsd" json:"statsd,omitempty"`
122
+		Prometheus struct {
123
+			Enabled      bool   `toml:"enabled" json:"enabled,omitempty"`
124
+			BindTo       string `toml:"bind-to" json:"bind-to,omitempty"`
125
+			HTTPPath     string `toml:"http-path" json:"http-path,omitempty"`
126
+			MetricPrefix string `toml:"metric-prefix" json:"metric-prefix,omitempty"`
127
+		} `toml:"prometheus" json:"prometheus,omitempty"`
128
+	} `toml:"stats" json:"stats,omitempty"`
129
+}
130
+
131
+func Parse(rawData []byte) (*Config, error) {
132
+	rawConf := &configRaw{}
133
+	jsonBuf := &bytes.Buffer{}
134
+	conf := &Config{}
135
+
136
+	jsonEncoder := json.NewEncoder(jsonBuf)
137
+	jsonEncoder.SetEscapeHTML(false)
138
+	jsonEncoder.SetIndent("", "")
139
+
140
+	if err := toml.Unmarshal(rawData, rawConf); err != nil {
141
+		return nil, fmt.Errorf("cannot parse toml config: %w", err)
142
+	}
143
+
144
+	if err := jsonEncoder.Encode(rawConf); err != nil {
145
+		panic(err)
146
+	}
147
+
148
+	if err := json.NewDecoder(jsonBuf).Decode(conf); err != nil {
149
+		return nil, fmt.Errorf("cannot parse a config: %w", err)
150
+	}
151
+
152
+	if err := conf.Validate(); err != nil {
153
+		return nil, fmt.Errorf("cannot validate config: %w", err)
154
+	}
155
+
156
+	return conf, nil
157
+}

+ 54
- 0
internal/config/config_test.go Просмотреть файл

1
+package config_test
2
+
3
+import (
4
+	"os"
5
+	"path/filepath"
6
+	"testing"
7
+
8
+	"github.com/9seconds/mtg/v2/internal/config"
9
+	"github.com/stretchr/testify/suite"
10
+)
11
+
12
+type ConfigTestSuite struct {
13
+	suite.Suite
14
+}
15
+
16
+func (suite *ConfigTestSuite) ReadConfig(filename string) []byte {
17
+	data, err := os.ReadFile(filepath.Join("testdata", filename))
18
+	suite.NoError(err)
19
+
20
+	return data
21
+}
22
+
23
+func (suite *ConfigTestSuite) TestParseEmpty() {
24
+	_, err := config.Parse([]byte{})
25
+	suite.Error(err)
26
+}
27
+
28
+func (suite *ConfigTestSuite) TestParseBrokenToml() {
29
+	_, err := config.Parse(suite.ReadConfig("broken.toml"))
30
+	suite.Error(err)
31
+}
32
+
33
+func (suite *ConfigTestSuite) TestParseOnlySecret() {
34
+	_, err := config.Parse(suite.ReadConfig("only_secret.toml"))
35
+	suite.Error(err)
36
+}
37
+
38
+func (suite *ConfigTestSuite) TestParseMinimalConfig() {
39
+	conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
40
+	suite.NoError(err)
41
+	suite.Equal("7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t", conf.Secret.Base64())
42
+	suite.Equal("0.0.0.0:3128", conf.BindTo.String())
43
+}
44
+
45
+func (suite *ConfigTestSuite) TestString() {
46
+	conf, err := config.Parse(suite.ReadConfig("minimal.toml"))
47
+	suite.NoError(err)
48
+	suite.NotEmpty(conf.String())
49
+}
50
+
51
+func TestConfig(t *testing.T) {
52
+	t.Parallel()
53
+	suite.Run(t, &ConfigTestSuite{})
54
+}

+ 1
- 0
internal/config/testdata/broken.toml Просмотреть файл

1
+s = sdfsdfds

+ 2
- 0
internal/config/testdata/minimal.toml Просмотреть файл

1
+secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"
2
+bind-to = "0.0.0.0:3128"

+ 1
- 0
internal/config/testdata/only_secret.toml Просмотреть файл

1
+secret = "7oe1GqLy6TBc38CV3jx7q09nb29nbGUuY29t"

+ 68
- 0
internal/config/type_blocklist_uri.go Просмотреть файл

1
+package config
2
+
3
+import (
4
+	"fmt"
5
+	"net/url"
6
+	"os"
7
+	"path/filepath"
8
+)
9
+
10
+type TypeBlocklistURI struct {
11
+	value string
12
+}
13
+
14
+func (c *TypeBlocklistURI) UnmarshalText(data []byte) error {
15
+	if len(data) == 0 {
16
+		return nil
17
+	}
18
+
19
+	text := string(data)
20
+	if filepath.IsAbs(text) {
21
+		if _, err := os.Stat(text); os.IsNotExist(err) {
22
+			return fmt.Errorf("filepath %s does not exist", text)
23
+		}
24
+
25
+		c.value = text
26
+
27
+		return nil
28
+	}
29
+
30
+	parsedURL, err := url.Parse(text)
31
+	if err != nil {
32
+		return fmt.Errorf("incorrect url: %w", err)
33
+	}
34
+
35
+	switch parsedURL.Scheme {
36
+	case "http", "https": // nolint: goconst
37
+	default:
38
+		return fmt.Errorf("unknown schema %s", parsedURL.Scheme)
39
+	}
40
+
41
+	if parsedURL.Host == "" {
42
+		return fmt.Errorf("incorrect url %s", text)
43
+	}
44
+
45
+	c.value = parsedURL.String()
46
+
47
+	return nil
48
+}
49
+
50
+func (c TypeBlocklistURI) MarshalText() ([]byte, error) {
51
+	return []byte(c.value), nil
52
+}
53
+
54
+func (c TypeBlocklistURI) String() string {
55
+	return c.value
56
+}
57
+
58
+func (c TypeBlocklistURI) IsRemote() bool {
59
+	return !filepath.IsAbs(c.value)
60
+}
61
+
62
+func (c TypeBlocklistURI) Value(defaultValue string) string {
63
+	if c.value == "" {
64
+		return defaultValue
65
+	}
66
+
67
+	return c.value
68
+}

+ 177
- 0
internal/config/type_blocklist_uri_test.go Просмотреть файл

1
+package config_test
2
+
3
+import (
4
+	"crypto/rand"
5
+	"encoding/base64"
6
+	"encoding/json"
7
+	"os"
8
+	"path/filepath"
9
+	"strconv"
10
+	"testing"
11
+
12
+	"github.com/9seconds/mtg/v2/internal/config"
13
+	"github.com/stretchr/testify/assert"
14
+	"github.com/stretchr/testify/suite"
15
+)
16
+
17
+type typeBlocklistURITestStruct struct {
18
+	Value config.TypeBlocklistURI `json:"value"`
19
+}
20
+
21
+type TypeBlocklistURITestSuite struct {
22
+	suite.Suite
23
+}
24
+
25
+func (suite *TypeBlocklistURITestSuite) TestUnmarshalNil() {
26
+	typ := &config.TypeBlocklistURI{}
27
+	suite.NoError(typ.UnmarshalText(nil))
28
+	suite.Empty(typ.String())
29
+}
30
+
31
+func (suite *TypeBlocklistURITestSuite) TestUnknownSchema() {
32
+	typ := &config.TypeBlocklistURI{}
33
+	suite.Error(typ.UnmarshalText([]byte("gopher://lalala")))
34
+}
35
+
36
+func (suite *TypeBlocklistURITestSuite) TestEmptyHost() {
37
+	typ := &config.TypeBlocklistURI{}
38
+	suite.Error(typ.UnmarshalText([]byte("https:///path")))
39
+}
40
+
41
+func (suite *TypeBlocklistURITestSuite) TestIncorrectURL() {
42
+	typ := &config.TypeBlocklistURI{}
43
+	suite.Error(typ.UnmarshalText([]byte("h:/--")))
44
+}
45
+
46
+func (suite *TypeBlocklistURITestSuite) TestUnmarshalFail() {
47
+	rnd := make([]byte, 48)
48
+
49
+	rand.Read(rnd) // nolint: errcheck
50
+
51
+	unknownPath := base64.StdEncoding.EncodeToString(rnd)
52
+
53
+	testData := []string{
54
+		"1",
55
+		unknownPath,
56
+		"/" + unknownPath,
57
+		"http:/",
58
+		"gopher://lalalal",
59
+	}
60
+
61
+	for _, v := range testData {
62
+		data, err := json.Marshal(map[string]string{
63
+			"value": v,
64
+		})
65
+		suite.NoError(err)
66
+
67
+		suite.T().Run(v, func(t *testing.T) {
68
+			assert.Error(t, json.Unmarshal(data, &typeBlocklistURITestStruct{}))
69
+		})
70
+	}
71
+}
72
+
73
+func (suite *TypeBlocklistURITestSuite) TestUnmarshalOk() {
74
+	dir, _ := os.Getwd()
75
+	dir, _ = filepath.Abs(dir)
76
+
77
+	testData := []string{
78
+		"http://lalala",
79
+		filepath.Join(dir, "config.go"),
80
+		"https://lalala",
81
+	}
82
+
83
+	for _, v := range testData {
84
+		value := v
85
+
86
+		data, err := json.Marshal(map[string]string{
87
+			"value": v,
88
+		})
89
+		suite.NoError(err)
90
+
91
+		suite.T().Run(v, func(t *testing.T) {
92
+			testStruct := &typeBlocklistURITestStruct{}
93
+
94
+			assert.NoError(t, json.Unmarshal(data, testStruct))
95
+			assert.EqualValues(t, value, testStruct.Value.Value(""))
96
+		})
97
+	}
98
+}
99
+
100
+func (suite *TypeBlocklistURITestSuite) TestMarshalOk() {
101
+	dir, _ := os.Getwd()
102
+	dir, _ = filepath.Abs(dir)
103
+
104
+	testData := []string{
105
+		"http://lalalal",
106
+		filepath.Join(dir, "config.go"),
107
+	}
108
+
109
+	for _, v := range testData {
110
+		name := v
111
+
112
+		data, err := json.Marshal(map[string]string{
113
+			"value": name,
114
+		})
115
+		suite.NoError(err)
116
+
117
+		suite.T().Run(name, func(t *testing.T) {
118
+			testStruct := &typeBlocklistURITestStruct{}
119
+
120
+			assert.NoError(t, json.Unmarshal(data, testStruct))
121
+			assert.Equal(t, name, testStruct.Value.String())
122
+
123
+			marshalled, err := testStruct.Value.MarshalText()
124
+			assert.NoError(t, err)
125
+			assert.Equal(t, name, string(marshalled))
126
+		})
127
+	}
128
+}
129
+
130
+func (suite *TypeBlocklistURITestSuite) TestValue() {
131
+	testStruct := &typeBlocklistURITestStruct{}
132
+
133
+	suite.Equal("http://lalala", testStruct.Value.Value("http://lalala"))
134
+
135
+	data, err := json.Marshal(map[string]string{
136
+		"value": "http://blablabla",
137
+	})
138
+	suite.NoError(err)
139
+	suite.NoError(json.Unmarshal(data, testStruct))
140
+
141
+	suite.Equal("http://blablabla", testStruct.Value.Value(""))
142
+}
143
+
144
+func (suite *TypeBlocklistURITestSuite) TestIsRemote() {
145
+	dir, _ := os.Getwd()
146
+	dir, _ = filepath.Abs(dir)
147
+
148
+	testData := map[bool]string{
149
+		true:  "http://lalalal",
150
+		false: filepath.Join(dir, "config.go"),
151
+	}
152
+
153
+	for k, v := range testData {
154
+		ok := k
155
+
156
+		data, err := json.Marshal(map[string]string{
157
+			"value": v,
158
+		})
159
+		suite.NoError(err)
160
+
161
+		suite.T().Run(strconv.FormatBool(ok), func(t *testing.T) {
162
+			testStruct := &typeBlocklistURITestStruct{}
163
+			assert.NoError(t, json.Unmarshal(data, testStruct))
164
+
165
+			if ok {
166
+				assert.True(t, testStruct.Value.IsRemote())
167
+			} else {
168
+				assert.False(t, testStruct.Value.IsRemote())
169
+			}
170
+		})
171
+	}
172
+}
173
+
174
+func TestTypeBlocklistURI(t *testing.T) {
175
+	t.Parallel()
176
+	suite.Run(t, &TypeBlocklistURITestSuite{})
177
+}

+ 54
- 0
internal/config/type_bytes.go Просмотреть файл

1
+package config
2
+
3
+import (
4
+	"fmt"
5
+	"strings"
6
+
7
+	"github.com/alecthomas/units"
8
+)
9
+
10
+type TypeBytes struct {
11
+	value units.Base2Bytes
12
+}
13
+
14
+func (c *TypeBytes) UnmarshalText(data []byte) error {
15
+	if len(data) == 0 {
16
+		return nil
17
+	}
18
+
19
+	normalizedData := strings.ToUpper(string(data))
20
+	normalizedData = strings.ReplaceAll(normalizedData, "IB", "iB")
21
+
22
+	value, err := units.ParseBase2Bytes(normalizedData)
23
+	if err != nil {
24
+		return fmt.Errorf("incorrect bytes value: %w", err)
25
+	}
26
+
27
+	if value < 0 {
28
+		return fmt.Errorf("%d should be positive number", value)
29
+	}
30
+
31
+	c.value = value
32
+
33
+	return nil
34
+}
35
+
36
+func (c TypeBytes) MarshalText() ([]byte, error) {
37
+	return []byte(c.String()), nil
38
+}
39
+
40
+func (c TypeBytes) String() string {
41
+	if c.value == 0 {
42
+		return ""
43
+	}
44
+
45
+	return strings.ToLower(c.value.String())
46
+}
47
+
48
+func (c TypeBytes) Value(defaultValue uint) uint {
49
+	if c.value == 0 {
50
+		return defaultValue
51
+	}
52
+
53
+	return uint(c.value)
54
+}

+ 120
- 0
internal/config/type_bytes_test.go Просмотреть файл

1
+package config_test
2
+
3
+import (
4
+	"encoding/json"
5
+	"testing"
6
+
7
+	"github.com/9seconds/mtg/v2/internal/config"
8
+	"github.com/stretchr/testify/assert"
9
+	"github.com/stretchr/testify/suite"
10
+)
11
+
12
+type typeBytesTestStruct struct {
13
+	Value config.TypeBytes `json:"value"`
14
+}
15
+
16
+type TypeBytesTestSuite struct {
17
+	suite.Suite
18
+}
19
+
20
+func (suite *TypeBytesTestSuite) TestUnmarshalNil() {
21
+	typ := &config.TypeBytes{}
22
+	suite.NoError(typ.UnmarshalText(nil))
23
+	suite.Empty(typ.String())
24
+}
25
+
26
+func (suite *TypeBytesTestSuite) TestUnmarshalFail() {
27
+	testData := []string{
28
+		"1m",
29
+		"1",
30
+		"-1kb",
31
+		"-1kib",
32
+		"-1QB",
33
+	}
34
+
35
+	for _, v := range testData {
36
+		data, err := json.Marshal(map[string]string{
37
+			"value": v,
38
+		})
39
+		suite.NoError(err)
40
+
41
+		suite.T().Run(v, func(t *testing.T) {
42
+			assert.Error(t, json.Unmarshal(data, &typeBytesTestStruct{}))
43
+		})
44
+	}
45
+}
46
+
47
+func (suite *TypeBytesTestSuite) TestUnmarshalOk() {
48
+	testData := map[string]uint{
49
+		"1b":   1,
50
+		"1kb":  1024,
51
+		"1kib": 1024,
52
+		"2mb":  2 * 1024 * 1024,
53
+		"2mib": 2 * 1024 * 1024,
54
+	}
55
+
56
+	for k, v := range testData {
57
+		value := v
58
+
59
+		data, err := json.Marshal(map[string]string{
60
+			"value": k,
61
+		})
62
+		suite.NoError(err)
63
+
64
+		suite.T().Run(k, func(t *testing.T) {
65
+			testStruct := &typeBytesTestStruct{}
66
+
67
+			assert.NoError(t, json.Unmarshal(data, testStruct))
68
+			assert.EqualValues(t, value, testStruct.Value.Value(0))
69
+		})
70
+	}
71
+}
72
+
73
+func (suite *TypeBytesTestSuite) TestMarshalOk() {
74
+	testData := []string{
75
+		"1b",
76
+		"1kib",
77
+		"2mib",
78
+	}
79
+
80
+	for _, v := range testData {
81
+		name := v
82
+
83
+		data, err := json.Marshal(map[string]string{
84
+			"value": name,
85
+		})
86
+		suite.NoError(err)
87
+
88
+		suite.T().Run(name, func(t *testing.T) {
89
+			testStruct := &typeBytesTestStruct{}
90
+
91
+			assert.NoError(t, json.Unmarshal(data, testStruct))
92
+			assert.Equal(t, name, testStruct.Value.String())
93
+
94
+			marshalled, err := testStruct.Value.MarshalText()
95
+			assert.NoError(t, err)
96
+			assert.Equal(t, name, string(marshalled))
97
+		})
98
+	}
99
+}
100
+
101
+func (suite *TypeBytesTestSuite) TestValue() {
102
+	testStruct := &typeBytesTestStruct{}
103
+
104
+	suite.EqualValues(0, testStruct.Value.Value(0))
105
+	suite.EqualValues(1, testStruct.Value.Value(1))
106
+
107
+	data, err := json.Marshal(map[string]string{
108
+		"value": "1kb",
109
+	})
110
+	suite.NoError(err)
111
+	suite.NoError(json.Unmarshal(data, testStruct))
112
+
113
+	suite.EqualValues(1024, testStruct.Value.Value(0))
114
+	suite.EqualValues(1024, testStruct.Value.Value(1))
115
+}
116
+
117
+func TestTypeBytes(t *testing.T) {
118
+	t.Parallel()
119
+	suite.Run(t, &TypeBytesTestSuite{})
120
+}

+ 46
- 0
internal/config/type_duration.go Просмотреть файл

1
+package config
2
+
3
+import (
4
+	"fmt"
5
+	"strings"
6
+	"time"
7
+)
8
+
9
+type TypeDuration struct {
10
+	value time.Duration
11
+}
12
+
13
+func (c *TypeDuration) UnmarshalText(data []byte) error {
14
+	if len(data) == 0 {
15
+		return nil
16
+	}
17
+
18
+	dur, err := time.ParseDuration(strings.ToLower(string(data)))
19
+	if err != nil {
20
+		return fmt.Errorf("incorrect duration: %w", err)
21
+	}
22
+
23
+	if dur < 0 {
24
+		return fmt.Errorf("%s should be positive duration", dur)
25
+	}
26
+
27
+	c.value = dur
28
+
29
+	return nil
30
+}
31
+
32
+func (c TypeDuration) MarshalText() ([]byte, error) {
33
+	return []byte(c.value.String()), nil
34
+}
35
+
36
+func (c TypeDuration) String() string {
37
+	return c.value.String()
38
+}
39
+
40
+func (c TypeDuration) Value(defaultValue time.Duration) time.Duration {
41
+	if c.value == 0 {
42
+		return defaultValue
43
+	}
44
+
45
+	return c.value
46
+}

+ 118
- 0
internal/config/type_duration_test.go Просмотреть файл

1
+package config_test
2
+
3
+import (
4
+	"encoding/json"
5
+	"testing"
6
+	"time"
7
+
8
+	"github.com/9seconds/mtg/v2/internal/config"
9
+	"github.com/stretchr/testify/assert"
10
+	"github.com/stretchr/testify/suite"
11
+)
12
+
13
+type typeDurationTestStruct struct {
14
+	Value config.TypeDuration `json:"value"`
15
+}
16
+
17
+type TypeDurationTestSuite struct {
18
+	suite.Suite
19
+}
20
+
21
+func (suite *TypeDurationTestSuite) TestUnmarshalNil() {
22
+	typ := &config.TypeDuration{}
23
+	suite.NoError(typ.UnmarshalText(nil))
24
+	suite.EqualValues(0, typ.Value(0))
25
+}
26
+
27
+func (suite *TypeDurationTestSuite) TestUnmarshalFail() {
28
+	testData := []string{
29
+		"1t",
30
+		"1",
31
+		"-1s",
32
+		"-1h",
33
+	}
34
+
35
+	for _, v := range testData {
36
+		data, err := json.Marshal(map[string]string{
37
+			"value": v,
38
+		})
39
+		suite.NoError(err)
40
+
41
+		suite.T().Run(v, func(t *testing.T) {
42
+			assert.Error(t, json.Unmarshal(data, &typeDurationTestStruct{}))
43
+		})
44
+	}
45
+}
46
+
47
+func (suite *TypeDurationTestSuite) TestUnmarshalOk() {
48
+	testData := map[string]time.Duration{
49
+		"1s":   time.Second,
50
+		"1m":   time.Minute,
51
+		"2h1s": 2*time.Hour + time.Second,
52
+	}
53
+
54
+	for k, v := range testData {
55
+		value := v
56
+
57
+		data, err := json.Marshal(map[string]string{
58
+			"value": k,
59
+		})
60
+		suite.NoError(err)
61
+
62
+		suite.T().Run(k, func(t *testing.T) {
63
+			testStruct := &typeDurationTestStruct{}
64
+
65
+			assert.NoError(t, json.Unmarshal(data, testStruct))
66
+			assert.Equal(t, value, testStruct.Value.Value(0))
67
+		})
68
+	}
69
+}
70
+
71
+func (suite *TypeDurationTestSuite) TestMarshalOk() {
72
+	testData := []string{
73
+		"1s",
74
+		"1m0s",
75
+		"2h0m1s",
76
+	}
77
+
78
+	for _, v := range testData {
79
+		name := v
80
+
81
+		data, err := json.Marshal(map[string]string{
82
+			"value": name,
83
+		})
84
+		suite.NoError(err)
85
+
86
+		suite.T().Run(name, func(t *testing.T) {
87
+			testStruct := &typeDurationTestStruct{}
88
+
89
+			assert.NoError(t, json.Unmarshal(data, testStruct))
90
+			assert.Equal(t, name, testStruct.Value.String())
91
+
92
+			marshalled, err := testStruct.Value.MarshalText()
93
+			assert.NoError(t, err)
94
+			assert.Equal(t, name, string(marshalled))
95
+		})
96
+	}
97
+}
98
+
99
+func (suite *TypeDurationTestSuite) TestValue() {
100
+	testStruct := &typeDurationTestStruct{}
101
+
102
+	suite.EqualValues(0, testStruct.Value.Value(0))
103
+	suite.Equal(time.Second, testStruct.Value.Value(time.Second))
104
+
105
+	data, err := json.Marshal(map[string]string{
106
+		"value": "1s",
107
+	})
108
+	suite.NoError(err)
109
+	suite.NoError(json.Unmarshal(data, testStruct))
110
+
111
+	suite.Equal(time.Second, testStruct.Value.Value(0))
112
+	suite.Equal(time.Second, testStruct.Value.Value(time.Minute))
113
+}
114
+
115
+func TestTypeDuration(t *testing.T) {
116
+	t.Parallel()
117
+	suite.Run(t, &TypeDurationTestSuite{})
118
+}

+ 43
- 0
internal/config/type_error_rate.go Просмотреть файл

1
+package config
2
+
3
+import (
4
+	"fmt"
5
+	"strconv"
6
+)
7
+
8
+const typeErrorRateIgnoreLess = 1e-8
9
+
10
+type TypeErrorRate struct {
11
+	value float64
12
+}
13
+
14
+func (c *TypeErrorRate) UnmarshalJSON(data []byte) error {
15
+	value, err := strconv.ParseFloat(string(data), 64)
16
+	if err != nil {
17
+		return fmt.Errorf("incorrect float value: %w", err)
18
+	}
19
+
20
+	if value <= 0 || value >= 100 {
21
+		return fmt.Errorf("%f should be 0 < x < 100", value)
22
+	}
23
+
24
+	c.value = value
25
+
26
+	return nil
27
+}
28
+
29
+func (c *TypeErrorRate) MarshalText() ([]byte, error) {
30
+	return []byte(c.String()), nil
31
+}
32
+
33
+func (c TypeErrorRate) String() string {
34
+	return strconv.FormatFloat(c.value, 'f', -1, 64)
35
+}
36
+
37
+func (c TypeErrorRate) Value(defaultValue float64) float64 {
38
+	if c.value < typeErrorRateIgnoreLess {
39
+		return defaultValue
40
+	}
41
+
42
+	return c.value
43
+}

+ 125
- 0
internal/config/type_error_rate_test.go Просмотреть файл

1
+package config_test
2
+
3
+import (
4
+	"encoding/json"
5
+	"strconv"
6
+	"testing"
7
+
8
+	"github.com/9seconds/mtg/v2/internal/config"
9
+	"github.com/stretchr/testify/assert"
10
+	"github.com/stretchr/testify/suite"
11
+)
12
+
13
+type typeErrorRateTestStruct struct {
14
+	Value config.TypeErrorRate `json:"value"`
15
+}
16
+
17
+type TypeErrorRateTestSuite struct {
18
+	suite.Suite
19
+}
20
+
21
+func (suite *TypeErrorRateTestSuite) TestUnmarshalFail() {
22
+	testData := []float64{
23
+		1000,
24
+		-100,
25
+		-0.0001,
26
+	}
27
+
28
+	for _, v := range testData {
29
+		data, err := json.Marshal(map[string]float64{
30
+			"value": v,
31
+		})
32
+		suite.NoError(err)
33
+
34
+		suite.T().Run(strconv.FormatFloat(v, 'f', -1, 64), func(t *testing.T) {
35
+			assert.Error(t, json.Unmarshal(data, &typeErrorRateTestStruct{}))
36
+		})
37
+	}
38
+
39
+	data, err := json.Marshal(map[string]string{
40
+		"value": "hello",
41
+	})
42
+	suite.NoError(err)
43
+	suite.Error(json.Unmarshal(data, &typeErrorRateTestStruct{}))
44
+}
45
+
46
+func (suite *TypeErrorRateTestSuite) TestUnmarshalOk() {
47
+	testData := []float64{
48
+		1,
49
+		55.5,
50
+		0.0001,
51
+		1e-6,
52
+	}
53
+
54
+	for _, v := range testData {
55
+		value := v
56
+
57
+		data, err := json.Marshal(map[string]float64{
58
+			"value": v,
59
+		})
60
+		suite.NoError(err)
61
+
62
+		suite.T().Run(strconv.FormatFloat(v, 'f', -1, 64), func(t *testing.T) {
63
+			testStruct := &typeErrorRateTestStruct{}
64
+
65
+			assert.NoError(t, json.Unmarshal(data, testStruct))
66
+			assert.InEpsilon(t, value, testStruct.Value.Value(0), 1e-10)
67
+		})
68
+	}
69
+}
70
+
71
+func (suite *TypeErrorRateTestSuite) TestMarshalOk() {
72
+	testData := []float64{
73
+		1,
74
+		55.5,
75
+		0.0001,
76
+		1e-6,
77
+	}
78
+
79
+	for _, v := range testData {
80
+		value := v
81
+
82
+		data, err := json.Marshal(map[string]float64{
83
+			"value": v,
84
+		})
85
+		suite.NoError(err)
86
+
87
+		suite.T().Run(strconv.FormatFloat(v, 'f', -1, 64), func(t *testing.T) {
88
+			testStruct := &typeErrorRateTestStruct{}
89
+
90
+			assert.NoError(t, json.Unmarshal(data, testStruct))
91
+
92
+			parsed, err := strconv.ParseFloat(testStruct.Value.String(), 64)
93
+			assert.NoError(t, err)
94
+			assert.InEpsilon(t, value, parsed, 1e-10)
95
+
96
+			marshalled, err := testStruct.Value.MarshalText()
97
+			assert.NoError(t, err)
98
+
99
+			parsed, err = strconv.ParseFloat(string(marshalled), 64)
100
+			assert.NoError(t, err)
101
+			assert.InEpsilon(t, value, parsed, 1e-10)
102
+		})
103
+	}
104
+}
105
+
106
+func (suite *TypeErrorRateTestSuite) TestValue() {
107
+	testStruct := &typeErrorRateTestStruct{}
108
+
109
+	suite.InEpsilon(1, testStruct.Value.Value(1), 1e-10)
110
+	suite.InEpsilon(2, testStruct.Value.Value(2), 1e-10)
111
+
112
+	data, err := json.Marshal(map[string]float64{
113
+		"value": 1,
114
+	})
115
+	suite.NoError(err)
116
+	suite.NoError(json.Unmarshal(data, testStruct))
117
+
118
+	suite.InEpsilon(1, testStruct.Value.Value(2), 1e-10)
119
+	suite.InEpsilon(1, testStruct.Value.Value(3), 1e-10)
120
+}
121
+
122
+func TestTypeErrorRate(t *testing.T) {
123
+	t.Parallel()
124
+	suite.Run(t, &TypeErrorRateTestSuite{})
125
+}

+ 67
- 0
internal/config/type_hostport.go Просмотреть файл

1
+package config
2
+
3
+import (
4
+	"fmt"
5
+	"net"
6
+	"strconv"
7
+)
8
+
9
+type TypeHostPort struct {
10
+	host TypeIP
11
+	port TypePort
12
+}
13
+
14
+func (c *TypeHostPort) UnmarshalText(data []byte) error {
15
+	if len(data) == 0 {
16
+		return nil
17
+	}
18
+
19
+	text := string(data)
20
+
21
+	host, port, err := net.SplitHostPort(text)
22
+	if err != nil {
23
+		return fmt.Errorf("incorrect host:port syntax: %w", err)
24
+	}
25
+
26
+	if port == "" {
27
+		return fmt.Errorf("port in %s host:port pair cannot be empty", text)
28
+	}
29
+
30
+	if err := c.port.UnmarshalJSON([]byte(port)); err != nil {
31
+		return fmt.Errorf("incorrect port in host:port: %w", err)
32
+	}
33
+
34
+	if err := c.host.UnmarshalText([]byte(host)); err != nil {
35
+		return fmt.Errorf("incorrect host: %w", err)
36
+	}
37
+
38
+	return nil
39
+}
40
+
41
+func (c TypeHostPort) MarshalText() ([]byte, error) {
42
+	return []byte(c.String()), nil
43
+}
44
+
45
+func (c TypeHostPort) String() string {
46
+	return c.Value(net.IP{}, 0)
47
+}
48
+
49
+func (c TypeHostPort) HostValue(defaultValue net.IP) net.IP {
50
+	return c.host.Value(defaultValue)
51
+}
52
+
53
+func (c TypeHostPort) PortValue(defaultValue uint) uint {
54
+	return c.port.Value(defaultValue)
55
+}
56
+
57
+func (c TypeHostPort) Value(defaultHostValue net.IP, defaultPortValue uint) string {
58
+	host := c.HostValue(defaultHostValue)
59
+	port := c.PortValue(defaultPortValue)
60
+
61
+	hostStr := ""
62
+	if len(host) > 0 {
63
+		hostStr = host.String()
64
+	}
65
+
66
+	return net.JoinHostPort(hostStr, strconv.Itoa(int(port)))
67
+}

+ 115
- 0
internal/config/type_hostport_test.go Просмотреть файл

1
+package config_test
2
+
3
+import (
4
+	"encoding/json"
5
+	"net"
6
+	"testing"
7
+
8
+	"github.com/9seconds/mtg/v2/internal/config"
9
+	"github.com/stretchr/testify/assert"
10
+	"github.com/stretchr/testify/suite"
11
+)
12
+
13
+type typeHostPortTestStruct struct {
14
+	Value config.TypeHostPort `json:"value"`
15
+}
16
+
17
+type TypeHostPortTestSuite struct {
18
+	suite.Suite
19
+}
20
+
21
+func (suite *TypeHostPortTestSuite) TestUnmarshalFail() {
22
+	testData := []string{
23
+		"10.0.0.10:aaa",
24
+		"10.0.0.10:",
25
+		":",
26
+		"xxx",
27
+		"xxx:80",
28
+	}
29
+
30
+	for _, v := range testData {
31
+		data, err := json.Marshal(map[string]string{
32
+			"value": v,
33
+		})
34
+		suite.NoError(err)
35
+
36
+		suite.T().Run(v, func(t *testing.T) {
37
+			assert.Error(t, json.Unmarshal(data, &typeHostPortTestStruct{}))
38
+		})
39
+	}
40
+}
41
+
42
+func (suite *TypeHostPortTestSuite) TestUnmarshalOk() {
43
+	testData := []string{
44
+		"10.0.0.10:80",
45
+		"0.0.0.0:80",
46
+		":8000",
47
+	}
48
+
49
+	for _, v := range testData {
50
+		value := v
51
+
52
+		data, err := json.Marshal(map[string]string{
53
+			"value": v,
54
+		})
55
+		suite.NoError(err)
56
+
57
+		suite.T().Run(v, func(t *testing.T) {
58
+			testStruct := &typeHostPortTestStruct{}
59
+
60
+			assert.NoError(t, json.Unmarshal(data, testStruct))
61
+			assert.EqualValues(t, value, testStruct.Value.Value(nil, 0))
62
+		})
63
+	}
64
+}
65
+
66
+func (suite *TypeHostPortTestSuite) TestMarshalOk() {
67
+	testData := []string{
68
+		"10.0.0.10:80",
69
+		"0.0.0.0:80",
70
+		":8000",
71
+	}
72
+
73
+	for _, v := range testData {
74
+		value := v
75
+
76
+		data, err := json.Marshal(map[string]string{
77
+			"value": v,
78
+		})
79
+		suite.NoError(err)
80
+
81
+		suite.T().Run(v, func(t *testing.T) {
82
+			testStruct := &typeHostPortTestStruct{}
83
+
84
+			assert.NoError(t, json.Unmarshal(data, testStruct))
85
+			assert.Equal(t, value, testStruct.Value.String())
86
+
87
+			marshalled, err := testStruct.Value.MarshalText()
88
+			assert.NoError(t, err)
89
+			assert.Equal(t, value, string(marshalled))
90
+		})
91
+	}
92
+}
93
+
94
+func (suite *TypeHostPortTestSuite) TestValue() {
95
+	testStruct := &typeHostPortTestStruct{}
96
+
97
+	suite.EqualValues("127.0.0.1:80",
98
+		testStruct.Value.Value(net.ParseIP("127.0.0.1"), 80))
99
+	suite.EqualValues("127.1.0.1:80",
100
+		testStruct.Value.Value(net.ParseIP("127.1.0.1"), 80))
101
+
102
+	data, err := json.Marshal(map[string]string{
103
+		"value": "127.0.0.1:80",
104
+	})
105
+	suite.NoError(err)
106
+	suite.NoError(json.Unmarshal(data, testStruct))
107
+
108
+	suite.EqualValues("127.0.0.1:80", testStruct.Value.Value(nil, 0))
109
+	suite.EqualValues("127.0.0.1:80", testStruct.Value.Value(net.ParseIP("10.0.0.10"), 3000))
110
+}
111
+
112
+func TestTypeHostPort(t *testing.T) {
113
+	t.Parallel()
114
+	suite.Run(t, &TypeHostPortTestSuite{})
115
+}

+ 35
- 0
internal/config/type_http_path.go Просмотреть файл

1
+package config
2
+
3
+import "strings"
4
+
5
+type TypeHTTPPath struct {
6
+	value string
7
+}
8
+
9
+func (c *TypeHTTPPath) UnmarshalText(data []byte) error {
10
+	if len(data) > 0 {
11
+		c.value = "/" + strings.Trim(string(data), "/")
12
+	}
13
+
14
+	return nil
15
+}
16
+
17
+func (c TypeHTTPPath) MarshalText() ([]byte, error) {
18
+	return []byte(c.String()), nil
19
+}
20
+
21
+func (c TypeHTTPPath) String() string {
22
+	if c.value == "" {
23
+		return "/"
24
+	}
25
+
26
+	return c.value
27
+}
28
+
29
+func (c TypeHTTPPath) Value(defaultValue string) string {
30
+	if c.value == "" {
31
+		return defaultValue
32
+	}
33
+
34
+	return c.value
35
+}

+ 91
- 0
internal/config/type_http_path_test.go Просмотреть файл

1
+package config_test
2
+
3
+import (
4
+	"encoding/json"
5
+	"testing"
6
+
7
+	"github.com/9seconds/mtg/v2/internal/config"
8
+	"github.com/stretchr/testify/assert"
9
+	"github.com/stretchr/testify/suite"
10
+)
11
+
12
+type typeHTTPPathTestStruct struct {
13
+	Value config.TypeHTTPPath `json:"value"`
14
+}
15
+
16
+type TypeHTTPPathTestSuite struct {
17
+	suite.Suite
18
+}
19
+
20
+func (suite *TypeHTTPPathTestSuite) TestUnmarshal() {
21
+	testData := []string{
22
+		"/hello",
23
+		"hello",
24
+		"hello/",
25
+		"/hello/",
26
+	}
27
+
28
+	for _, v := range testData {
29
+		data, err := json.Marshal(map[string]string{
30
+			"value": v,
31
+		})
32
+		suite.NoError(err)
33
+
34
+		suite.T().Run(v, func(t *testing.T) {
35
+			testStruct := &typeHTTPPathTestStruct{}
36
+
37
+			assert.NoError(t, json.Unmarshal(data, testStruct))
38
+			assert.Equal(t, "/hello", testStruct.Value.Value(""))
39
+		})
40
+	}
41
+}
42
+
43
+func (suite *TypeHTTPPathTestSuite) TestMarshalOk() {
44
+	testData := map[string]string{
45
+		"":        "/",
46
+		"/hello":  "/hello",
47
+		"/hello/": "/hello",
48
+		"hello/":  "/hello",
49
+		"hello":   "/hello",
50
+	}
51
+
52
+	for k, v := range testData {
53
+		toPass := k
54
+		compareWith := v
55
+
56
+		data, err := json.Marshal(map[string]string{
57
+			"value": toPass,
58
+		})
59
+		suite.NoError(err)
60
+
61
+		suite.T().Run(toPass, func(t *testing.T) {
62
+			testStruct := &typeHTTPPathTestStruct{}
63
+
64
+			assert.NoError(t, json.Unmarshal(data, testStruct))
65
+			assert.Equal(t, compareWith, testStruct.Value.String())
66
+
67
+			marshalled, err := testStruct.Value.MarshalText()
68
+			assert.NoError(t, err)
69
+			assert.Equal(t, compareWith, string(marshalled))
70
+		})
71
+	}
72
+}
73
+
74
+func (suite *TypeHTTPPathTestSuite) TestValue() {
75
+	testStruct := &typeHTTPPathTestStruct{}
76
+
77
+	suite.Equal("/hello", testStruct.Value.Value("/hello"))
78
+
79
+	data, err := json.Marshal(map[string]string{
80
+		"value": "/map",
81
+	})
82
+	suite.NoError(err)
83
+	suite.NoError(json.Unmarshal(data, testStruct))
84
+
85
+	suite.Equal("/map", testStruct.Value.Value("/hello"))
86
+}
87
+
88
+func TestTypeHTTPPath(t *testing.T) {
89
+	t.Parallel()
90
+	suite.Run(t, &TypeHTTPPathTestSuite{})
91
+}

+ 45
- 0
internal/config/type_ip.go Просмотреть файл

1
+package config
2
+
3
+import (
4
+	"fmt"
5
+	"net"
6
+)
7
+
8
+type TypeIP struct {
9
+	value net.IP
10
+}
11
+
12
+func (c *TypeIP) UnmarshalText(data []byte) error {
13
+	if len(data) == 0 {
14
+		return nil
15
+	}
16
+
17
+	ip := net.ParseIP(string(data))
18
+	if ip == nil {
19
+		return fmt.Errorf("incorrect ip address: %s", string(data))
20
+	}
21
+
22
+	c.value = ip
23
+
24
+	return nil
25
+}
26
+
27
+func (c *TypeIP) MarshalText() ([]byte, error) {
28
+	return []byte(c.String()), nil
29
+}
30
+
31
+func (c TypeIP) String() string {
32
+	if len(c.value) > 0 {
33
+		return c.value.String()
34
+	}
35
+
36
+	return ""
37
+}
38
+
39
+func (c TypeIP) Value(defaultValue net.IP) net.IP {
40
+	if c.value == nil {
41
+		return defaultValue
42
+	}
43
+
44
+	return c.value
45
+}

+ 115
- 0
internal/config/type_ip_test.go Просмотреть файл

1
+package config_test
2
+
3
+import (
4
+	"encoding/json"
5
+	"net"
6
+	"testing"
7
+
8
+	"github.com/9seconds/mtg/v2/internal/config"
9
+	"github.com/stretchr/testify/assert"
10
+	"github.com/stretchr/testify/suite"
11
+)
12
+
13
+type typeIPTestStruct struct {
14
+	Value config.TypeIP `json:"value"`
15
+}
16
+
17
+type TypeIPTestSuite struct {
18
+	suite.Suite
19
+}
20
+
21
+func (suite *TypeIPTestSuite) TestUnmarshalFail() {
22
+	testData := []string{
23
+		"0.0.10",
24
+		"10.0.0.10:",
25
+		"xxx:80",
26
+		"2001:0db8:85a3:0000:0000:8a2e:4",
27
+	}
28
+
29
+	for _, v := range testData {
30
+		data, err := json.Marshal(map[string]string{
31
+			"value": v,
32
+		})
33
+		suite.NoError(err)
34
+
35
+		suite.T().Run(v, func(t *testing.T) {
36
+			assert.Error(t, json.Unmarshal(data, &typeIPTestStruct{}))
37
+		})
38
+	}
39
+}
40
+
41
+func (suite *TypeIPTestSuite) TestUnmarshalOk() {
42
+	testData := []string{
43
+		"0.0.0.0",
44
+		"10.0.0.10",
45
+		"2001:0db8:85a3:0000:0000:8a2e:0370:7334",
46
+	}
47
+
48
+	for _, v := range testData {
49
+		value := v
50
+
51
+		data, err := json.Marshal(map[string]string{
52
+			"value": v,
53
+		})
54
+		suite.NoError(err)
55
+
56
+		suite.T().Run(v, func(t *testing.T) {
57
+			testStruct := &typeIPTestStruct{}
58
+
59
+			assert.NoError(t, json.Unmarshal(data, testStruct))
60
+			assert.Equal(t,
61
+				net.ParseIP(value).String(),
62
+				testStruct.Value.Value(nil).String())
63
+		})
64
+	}
65
+}
66
+
67
+func (suite *TypeIPTestSuite) TestMarshalOk() {
68
+	testData := []string{
69
+		"0.0.0.0",
70
+		"10.0.0.10",
71
+		"2001:0db8:85a3:0000:0000:8a2e:0370:7334",
72
+	}
73
+
74
+	for _, v := range testData {
75
+		value := net.ParseIP(v).String()
76
+
77
+		data, err := json.Marshal(map[string]string{
78
+			"value": v,
79
+		})
80
+		suite.NoError(err)
81
+
82
+		suite.T().Run(v, func(t *testing.T) {
83
+			testStruct := &typeIPTestStruct{}
84
+
85
+			assert.NoError(t, json.Unmarshal(data, testStruct))
86
+			assert.Equal(t, value, testStruct.Value.String())
87
+
88
+			marshalled, err := testStruct.Value.MarshalText()
89
+			assert.NoError(t, err)
90
+			assert.Equal(t, value, string(marshalled))
91
+		})
92
+	}
93
+}
94
+
95
+func (suite *TypeIPTestSuite) TestValue() {
96
+	testStruct := &typeIPTestStruct{}
97
+	suite.Empty(testStruct.Value.String())
98
+
99
+	suite.Nil(testStruct.Value.Value(nil))
100
+	suite.Equal("127.1.0.1", testStruct.Value.Value(net.ParseIP("127.1.0.1")).String())
101
+
102
+	data, err := json.Marshal(map[string]string{
103
+		"value": "127.0.0.1",
104
+	})
105
+	suite.NoError(err)
106
+	suite.NoError(json.Unmarshal(data, testStruct))
107
+
108
+	suite.Equal("127.0.0.1", testStruct.Value.Value(nil).String())
109
+	suite.Equal("127.0.0.1", testStruct.Value.Value(net.ParseIP("10.0.0.10")).String())
110
+}
111
+
112
+func TestTypeIP(t *testing.T) {
113
+	t.Parallel()
114
+	suite.Run(t, &TypeIPTestSuite{})
115
+}

+ 41
- 0
internal/config/type_metric_prefix.go Просмотреть файл

1
+package config
2
+
3
+import (
4
+	"fmt"
5
+	"regexp"
6
+)
7
+
8
+type TypeMetricPrefix struct {
9
+	value string
10
+}
11
+
12
+func (c *TypeMetricPrefix) UnmarshalText(data []byte) error {
13
+	if len(data) == 0 {
14
+		return nil
15
+	}
16
+
17
+	prefix := string(data)
18
+	if ok, err := regexp.MatchString("^[a-z0-9]+$", prefix); !ok || err != nil {
19
+		return fmt.Errorf("incorrect metric prefix: %s", prefix)
20
+	}
21
+
22
+	c.value = prefix
23
+
24
+	return nil
25
+}
26
+
27
+func (c TypeMetricPrefix) MarshalText() ([]byte, error) {
28
+	return []byte(c.String()), nil
29
+}
30
+
31
+func (c TypeMetricPrefix) String() string {
32
+	return c.value
33
+}
34
+
35
+func (c TypeMetricPrefix) Value(defaultValue string) string {
36
+	if c.value == "" {
37
+		return defaultValue
38
+	}
39
+
40
+	return c.value
41
+}

+ 115
- 0
internal/config/type_metric_prefix_test.go Просмотреть файл

1
+package config_test
2
+
3
+import (
4
+	"encoding/json"
5
+	"testing"
6
+
7
+	"github.com/9seconds/mtg/v2/internal/config"
8
+	"github.com/stretchr/testify/assert"
9
+	"github.com/stretchr/testify/suite"
10
+)
11
+
12
+type typeMetricPrefixTestStruct struct {
13
+	Value config.TypeMetricPrefix `json:"value"`
14
+}
15
+
16
+type TypeMetricPrefixTestSuite struct {
17
+	suite.Suite
18
+}
19
+
20
+func (suite *TypeMetricPrefixTestSuite) TestUnmarshalNil() {
21
+	typ := &config.TypeMetricPrefix{}
22
+	suite.NoError(typ.UnmarshalText(nil))
23
+	suite.Empty(typ.String())
24
+}
25
+
26
+func (suite *TypeMetricPrefixTestSuite) TestUnmarshalFail() {
27
+	testData := []string{
28
+		"aaa.aaa",
29
+		"aaa-bbb",
30
+		"aaa:ccc",
31
+		"metric prefix",
32
+	}
33
+
34
+	for _, v := range testData {
35
+		data, err := json.Marshal(map[string]string{
36
+			"value": v,
37
+		})
38
+		suite.NoError(err)
39
+
40
+		suite.T().Run(v, func(t *testing.T) {
41
+			assert.Error(t, json.Unmarshal(data, &typeMetricPrefixTestStruct{}))
42
+		})
43
+	}
44
+}
45
+
46
+func (suite *TypeMetricPrefixTestSuite) TestUnmarshalOk() {
47
+	testData := []string{
48
+		"mtg",
49
+		"mtg111",
50
+	}
51
+
52
+	for _, v := range testData {
53
+		value := v
54
+
55
+		data, err := json.Marshal(map[string]string{
56
+			"value": v,
57
+		})
58
+		suite.NoError(err)
59
+
60
+		suite.T().Run(v, func(t *testing.T) {
61
+			testStruct := &typeMetricPrefixTestStruct{}
62
+
63
+			assert.NoError(t, json.Unmarshal(data, testStruct))
64
+			assert.Equal(t, value, testStruct.Value.Value(""))
65
+		})
66
+	}
67
+}
68
+
69
+func (suite *TypeMetricPrefixTestSuite) TestMarshalOk() {
70
+	testData := []string{
71
+		"mtg",
72
+		"mtg111",
73
+	}
74
+
75
+	for _, v := range testData {
76
+		value := v
77
+
78
+		data, err := json.Marshal(map[string]string{
79
+			"value": v,
80
+		})
81
+		suite.NoError(err)
82
+
83
+		suite.T().Run(v, func(t *testing.T) {
84
+			testStruct := &typeMetricPrefixTestStruct{}
85
+
86
+			assert.NoError(t, json.Unmarshal(data, testStruct))
87
+			assert.Equal(t, value, testStruct.Value.String())
88
+
89
+			marshalled, err := testStruct.Value.MarshalText()
90
+			assert.NoError(t, err)
91
+			assert.Equal(t, value, string(marshalled))
92
+		})
93
+	}
94
+}
95
+
96
+func (suite *TypeMetricPrefixTestSuite) TestValue() {
97
+	testStruct := &typeMetricPrefixTestStruct{}
98
+
99
+	suite.Equal("mtg", testStruct.Value.Value("mtg"))
100
+	suite.Equal("vvv", testStruct.Value.Value("vvv"))
101
+
102
+	data, err := json.Marshal(map[string]string{
103
+		"value": "aaa",
104
+	})
105
+	suite.NoError(err)
106
+	suite.NoError(json.Unmarshal(data, testStruct))
107
+
108
+	suite.Equal("aaa", testStruct.Value.Value("mtg"))
109
+	suite.Equal("aaa", testStruct.Value.Value("vvv"))
110
+}
111
+
112
+func TestTypeMetricPrefix(t *testing.T) {
113
+	t.Parallel()
114
+	suite.Run(t, &TypeMetricPrefixTestSuite{})
115
+}

+ 45
- 0
internal/config/type_port.go Просмотреть файл

1
+package config
2
+
3
+import (
4
+	"fmt"
5
+	"strconv"
6
+)
7
+
8
+type TypePort struct {
9
+	value uint
10
+}
11
+
12
+func (c *TypePort) UnmarshalJSON(data []byte) error {
13
+	if len(data) == 0 {
14
+		return nil
15
+	}
16
+
17
+	intValue, err := strconv.ParseUint(string(data), 10, 64)
18
+	if err != nil {
19
+		return fmt.Errorf("port number is not a number: %w", err)
20
+	}
21
+
22
+	if intValue == 0 || intValue >= 65536 {
23
+		return fmt.Errorf("port number should be 0 < portNo < 65536: %d", intValue)
24
+	}
25
+
26
+	c.value = uint(intValue)
27
+
28
+	return nil
29
+}
30
+
31
+func (c *TypePort) MarshalJSON() ([]byte, error) {
32
+	return []byte(c.String()), nil
33
+}
34
+
35
+func (c TypePort) String() string {
36
+	return strconv.Itoa(int(c.value))
37
+}
38
+
39
+func (c TypePort) Value(defaultValue uint) uint {
40
+	if c.value == 0 {
41
+		return defaultValue
42
+	}
43
+
44
+	return c.value
45
+}

+ 117
- 0
internal/config/type_port_test.go Просмотреть файл

1
+package config_test
2
+
3
+import (
4
+	"encoding/json"
5
+	"strconv"
6
+	"testing"
7
+
8
+	"github.com/9seconds/mtg/v2/internal/config"
9
+	"github.com/stretchr/testify/assert"
10
+	"github.com/stretchr/testify/suite"
11
+)
12
+
13
+type typePortTestStruct struct {
14
+	Value config.TypePort `json:"value"`
15
+}
16
+
17
+type TypePortTestSuite struct {
18
+	suite.Suite
19
+}
20
+
21
+func (suite *TypePortTestSuite) TestUnmarshalNil() {
22
+	typ := &config.TypePort{}
23
+	suite.NoError(typ.UnmarshalJSON(nil))
24
+	suite.Equal("0", typ.String())
25
+}
26
+
27
+func (suite *TypePortTestSuite) TestUnmarshalFail() {
28
+	testData := []int{
29
+		-1,
30
+		1_000_000,
31
+	}
32
+
33
+	for _, v := range testData {
34
+		data, err := json.Marshal(map[string]int{
35
+			"value": v,
36
+		})
37
+		suite.NoError(err)
38
+
39
+		suite.T().Run(strconv.Itoa(v), func(t *testing.T) {
40
+			assert.Error(t, json.Unmarshal(data, &typePortTestStruct{}))
41
+		})
42
+	}
43
+}
44
+
45
+func (suite *TypePortTestSuite) TestUnmarshalOk() {
46
+	testData := []int{
47
+		1,
48
+		1_000,
49
+		65535,
50
+	}
51
+
52
+	for _, v := range testData {
53
+		value := v
54
+
55
+		data, err := json.Marshal(map[string]int{
56
+			"value": v,
57
+		})
58
+		suite.NoError(err)
59
+
60
+		suite.T().Run(strconv.Itoa(v), func(t *testing.T) {
61
+			testStruct := &typePortTestStruct{}
62
+
63
+			assert.NoError(t, json.Unmarshal(data, testStruct))
64
+			assert.EqualValues(t, value, testStruct.Value.Value(0))
65
+		})
66
+	}
67
+}
68
+
69
+func (suite *TypePortTestSuite) TestMarshalOk() {
70
+	testData := map[string]int{
71
+		"1":     1,
72
+		"1000":  1000,
73
+		"65535": 65535,
74
+	}
75
+
76
+	for k, v := range testData {
77
+		name := k
78
+		value := v
79
+
80
+		data, err := json.Marshal(map[string]int{
81
+			"value": value,
82
+		})
83
+		suite.NoError(err)
84
+
85
+		suite.T().Run(name, func(t *testing.T) {
86
+			testStruct := &typePortTestStruct{}
87
+
88
+			assert.NoError(t, json.Unmarshal(data, testStruct))
89
+			assert.Equal(t, name, testStruct.Value.String())
90
+
91
+			marshalled, err := testStruct.Value.MarshalJSON()
92
+			assert.NoError(t, err)
93
+			assert.Equal(t, name, string(marshalled))
94
+		})
95
+	}
96
+}
97
+
98
+func (suite *TypePortTestSuite) TestValue() {
99
+	testStruct := &typePortTestStruct{}
100
+
101
+	suite.EqualValues(0, testStruct.Value.Value(0))
102
+	suite.EqualValues(1, testStruct.Value.Value(1))
103
+
104
+	data, err := json.Marshal(map[string]int{
105
+		"value": 5,
106
+	})
107
+	suite.NoError(err)
108
+	suite.NoError(json.Unmarshal(data, testStruct))
109
+
110
+	suite.EqualValues(5, testStruct.Value.Value(0))
111
+	suite.EqualValues(5, testStruct.Value.Value(1))
112
+}
113
+
114
+func TestTypePort(t *testing.T) {
115
+	t.Parallel()
116
+	suite.Run(t, &TypePortTestSuite{})
117
+}

+ 50
- 0
internal/config/type_prefer_ip.go Просмотреть файл

1
+package config
2
+
3
+import (
4
+	"fmt"
5
+	"strings"
6
+)
7
+
8
+const (
9
+	TypePreferIPPreferIPv4 = "prefer-ipv4"
10
+	TypePreferIPPreferIPv6 = "prefer-ipv6"
11
+	TypePreferOnlyIPv4     = "only-ipv4"
12
+	TypePreferOnlyIPv6     = "only-ipv6"
13
+)
14
+
15
+type TypePreferIP struct {
16
+	value string
17
+}
18
+
19
+func (c *TypePreferIP) UnmarshalText(data []byte) error {
20
+	if len(data) == 0 {
21
+		return nil
22
+	}
23
+
24
+	text := strings.ToLower(string(data))
25
+
26
+	switch text {
27
+	case TypePreferIPPreferIPv4, TypePreferIPPreferIPv6, TypePreferOnlyIPv4, TypePreferOnlyIPv6:
28
+		c.value = text
29
+	default:
30
+		return fmt.Errorf("incorrect prefer-ip value: %s", string(data))
31
+	}
32
+
33
+	return nil
34
+}
35
+
36
+func (c TypePreferIP) MarshalText() ([]byte, error) {
37
+	return []byte(c.value), nil
38
+}
39
+
40
+func (c *TypePreferIP) String() string {
41
+	return c.value
42
+}
43
+
44
+func (c *TypePreferIP) Value(defaultValue string) string {
45
+	if c.value == "" {
46
+		return defaultValue
47
+	}
48
+
49
+	return c.value
50
+}

+ 142
- 0
internal/config/type_prefer_ip_test.go Просмотреть файл

1
+package config_test
2
+
3
+import (
4
+	"encoding/json"
5
+	"strings"
6
+	"testing"
7
+
8
+	"github.com/9seconds/mtg/v2/internal/config"
9
+	"github.com/stretchr/testify/assert"
10
+	"github.com/stretchr/testify/suite"
11
+)
12
+
13
+type typePreferIPTestStruct struct {
14
+	Value config.TypePreferIP `json:"value"`
15
+}
16
+
17
+type TypePreferIPTestSuite struct {
18
+	suite.Suite
19
+}
20
+
21
+func (suite *TypePreferIPTestSuite) TestUnmarshalNil() {
22
+	typ := &config.TypePreferIP{}
23
+	suite.NoError(typ.UnmarshalText(nil))
24
+	suite.Empty(typ.String())
25
+}
26
+
27
+func (suite *TypePreferIPTestSuite) TestUnmarshalFail() {
28
+	testData := []string{
29
+		"p",
30
+		"ipv4",
31
+		"onlyipv4",
32
+		"ipv6prefer",
33
+	}
34
+
35
+	for _, v := range testData {
36
+		data, err := json.Marshal(map[string]string{
37
+			"value": v,
38
+		})
39
+		suite.NoError(err)
40
+
41
+		suite.T().Run(v, func(t *testing.T) {
42
+			assert.Error(t, json.Unmarshal(data, &typePreferIPTestStruct{}))
43
+		})
44
+	}
45
+}
46
+
47
+func (suite *TypePreferIPTestSuite) TestUnmarshalOk() {
48
+	testData := []string{
49
+		config.TypePreferIPPreferIPv4,
50
+		config.TypePreferIPPreferIPv6,
51
+		config.TypePreferOnlyIPv4,
52
+		config.TypePreferOnlyIPv6,
53
+		strings.ToUpper(config.TypePreferIPPreferIPv4),
54
+		strings.ToUpper(config.TypePreferIPPreferIPv6),
55
+		strings.ToUpper(config.TypePreferOnlyIPv4),
56
+		strings.ToUpper(config.TypePreferOnlyIPv6),
57
+		strings.ToLower(config.TypePreferIPPreferIPv4),
58
+		strings.ToLower(config.TypePreferIPPreferIPv6),
59
+		strings.ToLower(config.TypePreferOnlyIPv4),
60
+		strings.ToLower(config.TypePreferOnlyIPv6),
61
+	}
62
+
63
+	for _, v := range testData {
64
+		value := v
65
+
66
+		data, err := json.Marshal(map[string]string{
67
+			"value": v,
68
+		})
69
+		suite.NoError(err)
70
+
71
+		suite.T().Run(v, func(t *testing.T) {
72
+			testStruct := &typePreferIPTestStruct{}
73
+
74
+			assert.NoError(t, json.Unmarshal(data, testStruct))
75
+			assert.EqualValues(t,
76
+				strings.ToLower(value),
77
+				testStruct.Value.Value(config.TypePreferIPPreferIPv4))
78
+		})
79
+	}
80
+}
81
+
82
+func (suite *TypePreferIPTestSuite) TestMarshalOk() {
83
+	testData := []string{
84
+		config.TypePreferIPPreferIPv4,
85
+		config.TypePreferIPPreferIPv6,
86
+		config.TypePreferOnlyIPv4,
87
+		config.TypePreferOnlyIPv6,
88
+		strings.ToUpper(config.TypePreferIPPreferIPv4),
89
+		strings.ToUpper(config.TypePreferIPPreferIPv6),
90
+		strings.ToUpper(config.TypePreferOnlyIPv4),
91
+		strings.ToUpper(config.TypePreferOnlyIPv6),
92
+		strings.ToLower(config.TypePreferIPPreferIPv4),
93
+		strings.ToLower(config.TypePreferIPPreferIPv6),
94
+		strings.ToLower(config.TypePreferOnlyIPv4),
95
+		strings.ToLower(config.TypePreferOnlyIPv6),
96
+	}
97
+
98
+	for _, v := range testData {
99
+		value := v
100
+
101
+		data, err := json.Marshal(map[string]string{
102
+			"value": v,
103
+		})
104
+		suite.NoError(err)
105
+
106
+		suite.T().Run(v, func(t *testing.T) {
107
+			testStruct := &typePreferIPTestStruct{}
108
+
109
+			assert.NoError(t, json.Unmarshal(data, testStruct))
110
+			assert.Equal(t, strings.ToLower(value), testStruct.Value.String())
111
+
112
+			marshalled, err := testStruct.Value.MarshalText()
113
+			assert.NoError(t, err)
114
+			assert.Equal(t, strings.ToLower(value), string(marshalled))
115
+		})
116
+	}
117
+}
118
+
119
+func (suite *TypePreferIPTestSuite) TestValue() {
120
+	testStruct := &typePreferIPTestStruct{}
121
+
122
+	suite.EqualValues(config.TypePreferIPPreferIPv4,
123
+		testStruct.Value.Value(config.TypePreferIPPreferIPv4))
124
+	suite.EqualValues(config.TypePreferIPPreferIPv6,
125
+		testStruct.Value.Value(config.TypePreferIPPreferIPv6))
126
+
127
+	data, err := json.Marshal(map[string]string{
128
+		"value": config.TypePreferOnlyIPv4,
129
+	})
130
+	suite.NoError(err)
131
+	suite.NoError(json.Unmarshal(data, testStruct))
132
+
133
+	suite.EqualValues(config.TypePreferOnlyIPv4,
134
+		testStruct.Value.Value(config.TypePreferOnlyIPv6))
135
+	suite.EqualValues(config.TypePreferOnlyIPv4,
136
+		testStruct.Value.Value(config.TypePreferIPPreferIPv6))
137
+}
138
+
139
+func TestTypePreferIP(t *testing.T) {
140
+	t.Parallel()
141
+	suite.Run(t, &TypePreferIPTestSuite{})
142
+}

+ 49
- 0
internal/config/type_statsd_tag_format.go Просмотреть файл

1
+package config
2
+
3
+import (
4
+	"fmt"
5
+	"strings"
6
+)
7
+
8
+const (
9
+	TypeStatsdTagFormatInfluxdb = "influxdb"
10
+	TypeStatsdTagFormatDatadog  = "datadog"
11
+	TypeStatsdTagFormatGraphite = "graphite"
12
+)
13
+
14
+type TypeStatsdTagFormat struct {
15
+	value string
16
+}
17
+
18
+func (c *TypeStatsdTagFormat) UnmarshalText(data []byte) error {
19
+	if len(data) == 0 {
20
+		return nil
21
+	}
22
+
23
+	text := strings.ToLower(string(data))
24
+
25
+	switch text {
26
+	case TypeStatsdTagFormatInfluxdb, TypeStatsdTagFormatDatadog, TypeStatsdTagFormatGraphite:
27
+		c.value = text
28
+	default:
29
+		return fmt.Errorf("incorrect tag format value: %s", string(data))
30
+	}
31
+
32
+	return nil
33
+}
34
+
35
+func (c TypeStatsdTagFormat) MarshalText() ([]byte, error) {
36
+	return []byte(c.value), nil
37
+}
38
+
39
+func (c *TypeStatsdTagFormat) String() string {
40
+	return c.value
41
+}
42
+
43
+func (c *TypeStatsdTagFormat) Value(defaultValue string) string {
44
+	if c.value == "" {
45
+		return defaultValue
46
+	}
47
+
48
+	return c.value
49
+}

+ 136
- 0
internal/config/type_statsd_tag_format_test.go Просмотреть файл

1
+package config_test
2
+
3
+import (
4
+	"encoding/json"
5
+	"strings"
6
+	"testing"
7
+
8
+	"github.com/9seconds/mtg/v2/internal/config"
9
+	"github.com/stretchr/testify/assert"
10
+	"github.com/stretchr/testify/suite"
11
+)
12
+
13
+type typeStatsdTagFormatTestStruct struct {
14
+	Value config.TypeStatsdTagFormat `json:"value"`
15
+}
16
+
17
+type TypeStatsdTagFormatTestSuite struct {
18
+	suite.Suite
19
+}
20
+
21
+func (suite *TypeStatsdTagFormatTestSuite) TestUnmarshalNil() {
22
+	typ := &config.TypeStatsdTagFormat{}
23
+	suite.NoError(typ.UnmarshalText(nil))
24
+	suite.Equal("lalala", typ.Value("lalala"))
25
+}
26
+
27
+func (suite *TypeStatsdTagFormatTestSuite) TestUnmarshalFail() {
28
+	testData := []string{
29
+		"p",
30
+		"ipv4",
31
+		"onlyipv4",
32
+		"ipv6prefer",
33
+	}
34
+
35
+	for _, v := range testData {
36
+		data, err := json.Marshal(map[string]string{
37
+			"value": v,
38
+		})
39
+		suite.NoError(err)
40
+
41
+		suite.T().Run(v, func(t *testing.T) {
42
+			assert.Error(t, json.Unmarshal(data, &typeStatsdTagFormatTestStruct{}))
43
+		})
44
+	}
45
+}
46
+
47
+func (suite *TypeStatsdTagFormatTestSuite) TestUnmarshalOk() {
48
+	testData := []string{
49
+		config.TypeStatsdTagFormatDatadog,
50
+		config.TypeStatsdTagFormatInfluxdb,
51
+		config.TypeStatsdTagFormatGraphite,
52
+		strings.ToUpper(config.TypeStatsdTagFormatDatadog),
53
+		strings.ToUpper(config.TypeStatsdTagFormatInfluxdb),
54
+		strings.ToUpper(config.TypeStatsdTagFormatGraphite),
55
+		strings.ToLower(config.TypeStatsdTagFormatDatadog),
56
+		strings.ToLower(config.TypeStatsdTagFormatInfluxdb),
57
+		strings.ToLower(config.TypeStatsdTagFormatGraphite),
58
+	}
59
+
60
+	for _, v := range testData {
61
+		value := v
62
+
63
+		data, err := json.Marshal(map[string]string{
64
+			"value": v,
65
+		})
66
+		suite.NoError(err)
67
+
68
+		suite.T().Run(v, func(t *testing.T) {
69
+			testStruct := &typeStatsdTagFormatTestStruct{}
70
+
71
+			assert.NoError(t, json.Unmarshal(data, testStruct))
72
+			assert.EqualValues(t,
73
+				strings.ToLower(value),
74
+				testStruct.Value.Value(config.TypeStatsdTagFormatDatadog))
75
+		})
76
+	}
77
+}
78
+
79
+func (suite *TypeStatsdTagFormatTestSuite) TestMarshalOk() {
80
+	testData := []string{
81
+		config.TypeStatsdTagFormatDatadog,
82
+		config.TypeStatsdTagFormatInfluxdb,
83
+		config.TypeStatsdTagFormatGraphite,
84
+		strings.ToUpper(config.TypeStatsdTagFormatDatadog),
85
+		strings.ToUpper(config.TypeStatsdTagFormatInfluxdb),
86
+		strings.ToUpper(config.TypeStatsdTagFormatGraphite),
87
+		strings.ToLower(config.TypeStatsdTagFormatDatadog),
88
+		strings.ToLower(config.TypeStatsdTagFormatInfluxdb),
89
+		strings.ToLower(config.TypeStatsdTagFormatGraphite),
90
+	}
91
+
92
+	for _, v := range testData {
93
+		value := v
94
+
95
+		data, err := json.Marshal(map[string]string{
96
+			"value": v,
97
+		})
98
+		suite.NoError(err)
99
+
100
+		suite.T().Run(v, func(t *testing.T) {
101
+			testStruct := &typeStatsdTagFormatTestStruct{}
102
+
103
+			assert.NoError(t, json.Unmarshal(data, testStruct))
104
+			assert.Equal(t, strings.ToLower(value), testStruct.Value.String())
105
+
106
+			marshalled, err := testStruct.Value.MarshalText()
107
+			assert.NoError(t, err)
108
+			assert.Equal(t, strings.ToLower(value), string(marshalled))
109
+		})
110
+	}
111
+}
112
+
113
+func (suite *TypeStatsdTagFormatTestSuite) TestValue() {
114
+	testStruct := &typePreferIPTestStruct{}
115
+
116
+	suite.EqualValues(config.TypePreferIPPreferIPv4,
117
+		testStruct.Value.Value(config.TypePreferIPPreferIPv4))
118
+	suite.EqualValues(config.TypePreferIPPreferIPv6,
119
+		testStruct.Value.Value(config.TypePreferIPPreferIPv6))
120
+
121
+	data, err := json.Marshal(map[string]string{
122
+		"value": config.TypePreferOnlyIPv4,
123
+	})
124
+	suite.NoError(err)
125
+	suite.NoError(json.Unmarshal(data, testStruct))
126
+
127
+	suite.EqualValues(config.TypePreferOnlyIPv4,
128
+		testStruct.Value.Value(config.TypePreferOnlyIPv6))
129
+	suite.EqualValues(config.TypePreferOnlyIPv4,
130
+		testStruct.Value.Value(config.TypePreferIPPreferIPv6))
131
+}
132
+
133
+func TestTypeStatsdTagFormat(t *testing.T) {
134
+	t.Parallel()
135
+	suite.Run(t, &TypeStatsdTagFormatTestSuite{})
136
+}

+ 71
- 0
internal/config/type_url.go Просмотреть файл

1
+package config
2
+
3
+import (
4
+	"fmt"
5
+	"net"
6
+	"net/url"
7
+)
8
+
9
+type TypeURL struct {
10
+	value *url.URL
11
+}
12
+
13
+func (c *TypeURL) UnmarshalText(data []byte) error { // nolint: cyclop
14
+	if len(data) == 0 {
15
+		return nil
16
+	}
17
+
18
+	value, err := url.Parse(string(data))
19
+	if err != nil {
20
+		return fmt.Errorf("incorrect URL: %w", err)
21
+	}
22
+
23
+	switch value.Scheme {
24
+	case "http", "https", "socks5":
25
+	case "":
26
+		return fmt.Errorf("url %s has to have a schema", value)
27
+	default:
28
+		return fmt.Errorf("unsupported schema %s", value.Scheme)
29
+	}
30
+
31
+	if value.Host == "" {
32
+		return fmt.Errorf("url %s has to have a host", value)
33
+	}
34
+
35
+	if _, _, err := net.SplitHostPort(value.Host); err != nil {
36
+		switch value.Scheme {
37
+		case "http":
38
+			value.Host = net.JoinHostPort(value.Host, "80")
39
+		case "https":
40
+			value.Host = net.JoinHostPort(value.Host, "443")
41
+		case "socks5":
42
+			value.Host = net.JoinHostPort(value.Host, "1080")
43
+		default:
44
+			return fmt.Errorf("cannot set a default port for %s", value)
45
+		}
46
+	}
47
+
48
+	c.value = value
49
+
50
+	return nil
51
+}
52
+
53
+func (c *TypeURL) MarshalText() ([]byte, error) {
54
+	return []byte(c.String()), nil
55
+}
56
+
57
+func (c TypeURL) String() string {
58
+	if c.value == nil {
59
+		return ""
60
+	}
61
+
62
+	return c.value.String()
63
+}
64
+
65
+func (c TypeURL) Value(defaultValue *url.URL) *url.URL {
66
+	if c.value == nil {
67
+		return defaultValue
68
+	}
69
+
70
+	return c.value
71
+}

+ 107
- 0
internal/config/type_url_test.go Просмотреть файл

1
+package config_test
2
+
3
+import (
4
+	"encoding/json"
5
+	"net/url"
6
+	"testing"
7
+
8
+	"github.com/9seconds/mtg/v2/internal/config"
9
+	"github.com/stretchr/testify/assert"
10
+	"github.com/stretchr/testify/suite"
11
+)
12
+
13
+type typeURLTestStruct struct {
14
+	Value config.TypeURL `json:"value"`
15
+}
16
+
17
+type TypeURLTestSuite struct {
18
+	suite.Suite
19
+}
20
+
21
+func (suite *TypeURLTestSuite) TestUnmarshalNil() {
22
+	u, _ := url.Parse("https://google.com")
23
+
24
+	typ := &config.TypeURL{}
25
+	suite.NoError(typ.UnmarshalText(nil))
26
+	suite.Empty(typ.String())
27
+	suite.Equal("https://google.com", typ.Value(u).String())
28
+}
29
+
30
+func (suite *TypeURLTestSuite) TestUnmarshalFail() {
31
+	testData := []string{
32
+		"http:/aaa.com",
33
+		"ipv4",
34
+		"111",
35
+		"://111",
36
+		"http://aaa.com:xxx",
37
+		"gopher://aaa.com:888",
38
+		"gopher://aaa.com",
39
+	}
40
+
41
+	for _, v := range testData {
42
+		data, err := json.Marshal(map[string]string{
43
+			"value": v,
44
+		})
45
+		suite.NoError(err)
46
+
47
+		suite.T().Run(v, func(t *testing.T) {
48
+			assert.Error(t, json.Unmarshal(data, &typeURLTestStruct{}))
49
+		})
50
+	}
51
+}
52
+
53
+func (suite *TypeURLTestSuite) TestUnmarshalOk() {
54
+	testData := map[string]string{
55
+		"https://10.0.0.10:80":    "https://10.0.0.10:80",
56
+		"https://10.0.0.10:443":   "https://10.0.0.10",
57
+		"http://10.0.0.10:8":      "http://10.0.0.10:8",
58
+		"http://10.0.0.10:80":     "http://10.0.0.10",
59
+		"socks5://10.0.0.10:1080": "socks5://10.0.0.10",
60
+		"socks5://10.0.0.10:888":  "socks5://10.0.0.10:888",
61
+	}
62
+
63
+	for k, v := range testData {
64
+		expected := k
65
+		actual := v
66
+
67
+		data, err := json.Marshal(map[string]string{
68
+			"value": actual,
69
+		})
70
+		suite.NoError(err)
71
+
72
+		suite.T().Run(actual, func(t *testing.T) {
73
+			testStruct := &typeURLTestStruct{}
74
+
75
+			assert.NoError(t, json.Unmarshal(data, testStruct))
76
+			assert.Equal(t, expected, testStruct.Value.Value(nil).String())
77
+
78
+			marshalled, err := testStruct.Value.MarshalText()
79
+			assert.NoError(t, err)
80
+			assert.Equal(t, expected, string(marshalled))
81
+		})
82
+	}
83
+}
84
+
85
+func (suite *TypeURLTestSuite) TestValue() {
86
+	testStruct := &typeURLTestStruct{}
87
+
88
+	u1, _ := url.Parse("https://10.0.0.10:80")
89
+	u2, _ := url.Parse("https://10.1.0.10:80")
90
+
91
+	suite.Equal("https://10.0.0.10:80", testStruct.Value.Value(u1).String())
92
+	suite.Equal("https://10.1.0.10:80", testStruct.Value.Value(u2).String())
93
+
94
+	data, err := json.Marshal(map[string]string{
95
+		"value": "http://127.0.0.1:80",
96
+	})
97
+	suite.NoError(err)
98
+	suite.NoError(json.Unmarshal(data, testStruct))
99
+
100
+	suite.Equal("http://127.0.0.1:80", testStruct.Value.Value(u1).String())
101
+	suite.Equal("http://127.0.0.1:80", testStruct.Value.Value(u2).String())
102
+}
103
+
104
+func TestTypeURL(t *testing.T) {
105
+	t.Parallel()
106
+	suite.Run(t, &TypeURLTestSuite{})
107
+}

+ 42
- 0
internal/testlib/capture_output.go Просмотреть файл

1
+package testlib
2
+
3
+import (
4
+	"bytes"
5
+	"io"
6
+	"os"
7
+	"strings"
8
+)
9
+
10
+func CaptureStdout(callback func()) string {
11
+	return captureOutput(&os.Stdout, callback)
12
+}
13
+
14
+func CaptureStderr(callback func()) string {
15
+	return captureOutput(&os.Stderr, callback)
16
+}
17
+
18
+func captureOutput(filefp **os.File, callback func()) string {
19
+	oldFp := *filefp
20
+
21
+	defer func() {
22
+		*filefp = oldFp
23
+	}()
24
+
25
+	reader, writer, _ := os.Pipe()
26
+	buf := &bytes.Buffer{}
27
+	closeChan := make(chan bool)
28
+
29
+	go func() {
30
+		io.Copy(buf, reader) // nolint: errcheck
31
+		close(closeChan)
32
+	}()
33
+
34
+	*filefp = writer
35
+
36
+	callback()
37
+
38
+	writer.Close()
39
+	<-closeChan
40
+
41
+	return strings.TrimSpace(buf.String())
42
+}

+ 7
- 0
internal/testlib/events_observer_mock.go Просмотреть файл

1
+package testlib
2
+
3
+import "github.com/stretchr/testify/mock"
4
+
5
+type EventsObserverMock struct {
6
+	mock.Mock
7
+}

+ 11
- 0
internal/testlib/mtglib_antireplay_cache_mock.go Просмотреть файл

1
+package testlib
2
+
3
+import "github.com/stretchr/testify/mock"
4
+
5
+type MtglibAntiReplayCacheMock struct {
6
+	mock.Mock
7
+}
8
+
9
+func (m *MtglibAntiReplayCacheMock) SeenBefore(data []byte) bool {
10
+	return m.Called(data).Bool(0)
11
+}

+ 30
- 0
internal/testlib/mtglib_network_mock.go Просмотреть файл

1
+package testlib
2
+
3
+import (
4
+	"context"
5
+	"net"
6
+	"net/http"
7
+
8
+	"github.com/stretchr/testify/mock"
9
+)
10
+
11
+type MtglibNetworkMock struct {
12
+	mock.Mock
13
+}
14
+
15
+func (m *MtglibNetworkMock) Dial(network, address string) (net.Conn, error) {
16
+	args := m.Called(network, address)
17
+
18
+	return args.Get(0).(net.Conn), args.Error(1)
19
+}
20
+
21
+func (m *MtglibNetworkMock) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
22
+	args := m.Called(ctx, network, address)
23
+
24
+	return args.Get(0).(net.Conn), args.Error(1)
25
+}
26
+
27
+func (m *MtglibNetworkMock) MakeHTTPClient(dialFunc func(ctx context.Context,
28
+	network, address string) (net.Conn, error)) *http.Client {
29
+	return m.Called(dialFunc).Get(0).(*http.Client)
30
+}

+ 48
- 0
internal/testlib/net_conn_mock.go Просмотреть файл

1
+package testlib
2
+
3
+import (
4
+	"net"
5
+	"time"
6
+
7
+	"github.com/stretchr/testify/mock"
8
+)
9
+
10
+type NetConnMock struct {
11
+	mock.Mock
12
+}
13
+
14
+func (n *NetConnMock) Read(b []byte) (int, error) {
15
+	args := n.Called(b)
16
+
17
+	return args.Int(0), args.Error(1)
18
+}
19
+
20
+func (n *NetConnMock) Write(b []byte) (int, error) {
21
+	args := n.Called(b)
22
+
23
+	return args.Int(0), args.Error(1)
24
+}
25
+
26
+func (n *NetConnMock) Close() error {
27
+	return n.Called().Error(0)
28
+}
29
+
30
+func (n *NetConnMock) LocalAddr() net.Addr {
31
+	return n.Called().Get(0).(net.Addr)
32
+}
33
+
34
+func (n *NetConnMock) RemoteAddr() net.Addr {
35
+	return n.Called().Get(0).(net.Addr)
36
+}
37
+
38
+func (n *NetConnMock) SetDeadline(t time.Time) error {
39
+	return n.Called(t).Error(0)
40
+}
41
+
42
+func (n *NetConnMock) SetReadDeadline(t time.Time) error {
43
+	return n.Called(t).Error(0)
44
+}
45
+
46
+func (n *NetConnMock) SetWriteDeadline(t time.Time) error {
47
+	return n.Called(t).Error(0)
48
+}

utils/rlimit.go → internal/utils/rlimit.go Просмотреть файл


utils/rlimit_windows.go → internal/utils/rlimit_windows.go Просмотреть файл


utils/signal_context.go → internal/utils/root_context.go Просмотреть файл

9
 	"syscall"
9
 	"syscall"
10
 )
10
 )
11
 
11
 
12
-func GetSignalContext() context.Context {
12
+func RootContext() context.Context {
13
 	ctx, cancel := context.WithCancel(context.Background())
13
 	ctx, cancel := context.WithCancel(context.Background())
14
 	sigChan := make(chan os.Signal, 1)
14
 	sigChan := make(chan os.Signal, 1)
15
 
15
 

utils/signal_context_windows.go → internal/utils/root_context_windows.go Просмотреть файл

8
 	"os/signal"
8
 	"os/signal"
9
 )
9
 )
10
 
10
 
11
-func GetSignalContext() context.Context {
11
+func RootContext() context.Context {
12
 	ctx, cancel := context.WithCancel(context.Background())
12
 	ctx, cancel := context.WithCancel(context.Background())
13
 	sigChan := make(chan os.Signal, 1)
13
 	sigChan := make(chan os.Signal, 1)
14
 
14
 

+ 373
- 0
ipblocklist/firehol.go Просмотреть файл

1
+package ipblocklist
2
+
3
+import (
4
+	"bufio"
5
+	"context"
6
+	"fmt"
7
+	"io"
8
+	"net"
9
+	"net/http"
10
+	"net/url"
11
+	"os"
12
+	"regexp"
13
+	"strings"
14
+	"sync"
15
+	"time"
16
+
17
+	"github.com/9seconds/mtg/v2/mtglib"
18
+	"github.com/kentik/patricia"
19
+	"github.com/kentik/patricia/bool_tree"
20
+	"github.com/panjf2000/ants/v2"
21
+)
22
+
23
+const (
24
+	fireholIPv4DefaultCIDR = 32
25
+	fireholIPv6DefaultCIDR = 128
26
+)
27
+
28
+var fireholRegexpComment = regexp.MustCompile(`\s*#.*?$`)
29
+
30
+// Firehol is IPBlocklist which uses lists from FireHOL:
31
+// https://iplists.firehol.org/
32
+//
33
+// It can use both local files and remote URLs. This is not necessary
34
+// that blocklists should be taken from this website, we expect only
35
+// compatible formats here.
36
+//
37
+// Example of the format:
38
+//
39
+//     # this is a comment
40
+//     # to ignore
41
+//     127.0.0.1   # you can specify an IP
42
+//     10.0.0.0/8  # or cidr
43
+type Firehol struct {
44
+	ctx       context.Context
45
+	ctxCancel context.CancelFunc
46
+	logger    mtglib.Logger
47
+
48
+	rwMutex sync.RWMutex
49
+
50
+	remoteURLs []string
51
+	localFiles []string
52
+
53
+	httpClient *http.Client
54
+	workerPool *ants.Pool
55
+
56
+	treeV4 *bool_tree.TreeV4
57
+	treeV6 *bool_tree.TreeV6
58
+}
59
+
60
+// Shutdown stop a background update process.
61
+func (f *Firehol) Shutdown() {
62
+	f.ctxCancel()
63
+}
64
+
65
+// Contains is given IP list can be found in FireHOL blocklists.
66
+func (f *Firehol) Contains(ip net.IP) bool {
67
+	if ip == nil {
68
+		return true
69
+	}
70
+
71
+	ip4 := ip.To4()
72
+
73
+	f.rwMutex.RLock()
74
+	defer f.rwMutex.RUnlock()
75
+
76
+	if ip4 != nil {
77
+		return f.containsIPv4(ip4)
78
+	}
79
+
80
+	return f.containsIPv6(ip.To16())
81
+}
82
+
83
+// Run starts a background update process.
84
+//
85
+// This is a blocking method so you probably want to run it in a
86
+// goroutine.
87
+func (f *Firehol) Run(updateEach time.Duration) {
88
+	if updateEach == 0 {
89
+		updateEach = DefaultFireholUpdateEach
90
+	}
91
+
92
+	ticker := time.NewTicker(updateEach)
93
+
94
+	defer func() {
95
+		ticker.Stop()
96
+
97
+		select {
98
+		case <-ticker.C:
99
+		default:
100
+		}
101
+	}()
102
+
103
+	if err := f.update(); err != nil {
104
+		f.logger.WarningError("cannot update blocklist", err)
105
+	} else {
106
+		f.logger.Info("blocklist was updated")
107
+	}
108
+
109
+	for {
110
+		select {
111
+		case <-f.ctx.Done():
112
+			return
113
+		case <-ticker.C:
114
+			if err := f.update(); err != nil {
115
+				f.logger.WarningError("cannot update blocklist", err)
116
+			} else {
117
+				f.logger.Info("blocklist was updated")
118
+			}
119
+		}
120
+	}
121
+}
122
+
123
+func (f *Firehol) containsIPv4(addr net.IP) bool {
124
+	ip := patricia.NewIPv4AddressFromBytes(addr, 32)
125
+
126
+	if ok, _, err := f.treeV4.FindDeepestTag(ip); ok && err == nil {
127
+		return true
128
+	}
129
+
130
+	return false
131
+}
132
+
133
+func (f *Firehol) containsIPv6(addr net.IP) bool {
134
+	ip := patricia.NewIPv6Address(addr, 128)
135
+
136
+	if ok, _, err := f.treeV6.FindDeepestTag(ip); ok && err == nil {
137
+		return true
138
+	}
139
+
140
+	return false
141
+}
142
+
143
+func (f *Firehol) update() error { // nolint: funlen, cyclop
144
+	ctx, cancel := context.WithCancel(f.ctx)
145
+	defer cancel()
146
+
147
+	wg := &sync.WaitGroup{}
148
+	wg.Add(len(f.remoteURLs) + len(f.localFiles))
149
+
150
+	treeMutex := &sync.Mutex{}
151
+	v4tree := bool_tree.NewTreeV4()
152
+	v6tree := bool_tree.NewTreeV6()
153
+
154
+	errorChan := make(chan error, 1)
155
+	defer close(errorChan)
156
+
157
+	for _, v := range f.localFiles {
158
+		go func(filename string) {
159
+			defer wg.Done()
160
+
161
+			if err := f.updateLocalFile(ctx, filename, treeMutex, v4tree, v6tree); err != nil {
162
+				cancel()
163
+				f.logger.BindStr("filename", filename).WarningError("cannot update", err)
164
+
165
+				select {
166
+				case errorChan <- err:
167
+				default:
168
+				}
169
+			}
170
+		}(v)
171
+	}
172
+
173
+	for _, v := range f.remoteURLs {
174
+		value := v
175
+
176
+		f.workerPool.Submit(func() { // nolint: errcheck
177
+			defer wg.Done()
178
+
179
+			if err := f.updateRemoteURL(ctx, value, treeMutex, v4tree, v6tree); err != nil {
180
+				cancel()
181
+				f.logger.BindStr("url", value).WarningError("cannot update", err)
182
+
183
+				select {
184
+				case errorChan <- err:
185
+				default:
186
+				}
187
+			}
188
+		})
189
+	}
190
+
191
+	wg.Wait()
192
+
193
+	select {
194
+	case err := <-errorChan:
195
+		return fmt.Errorf("cannot update trees: %w", err)
196
+	default:
197
+	}
198
+
199
+	f.rwMutex.Lock()
200
+	defer f.rwMutex.Unlock()
201
+
202
+	f.treeV4 = v4tree
203
+	f.treeV6 = v6tree
204
+
205
+	return nil
206
+}
207
+
208
+func (f *Firehol) updateLocalFile(ctx context.Context, filename string,
209
+	mutex sync.Locker,
210
+	v4tree *bool_tree.TreeV4, v6tree *bool_tree.TreeV6) error {
211
+	filefp, err := os.Open(filename)
212
+	if err != nil {
213
+		return fmt.Errorf("cannot open file: %w", err)
214
+	}
215
+
216
+	go func(ctx context.Context, closer io.Closer) {
217
+		<-ctx.Done()
218
+		closer.Close()
219
+	}(ctx, filefp)
220
+
221
+	defer filefp.Close()
222
+
223
+	return f.updateTrees(mutex, filefp, v4tree, v6tree)
224
+}
225
+
226
+func (f *Firehol) updateRemoteURL(ctx context.Context, url string,
227
+	mutex sync.Locker,
228
+	v4tree *bool_tree.TreeV4, v6tree *bool_tree.TreeV6) error {
229
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
230
+	if err != nil {
231
+		return fmt.Errorf("cannot build a request: %w", err)
232
+	}
233
+
234
+	resp, err := f.httpClient.Do(req) // nolint: bodyclose
235
+	if err != nil {
236
+		return fmt.Errorf("cannot request a remote URL %s: %w", url, err)
237
+	}
238
+
239
+	go func(ctx context.Context, closer io.Closer) {
240
+		<-ctx.Done()
241
+		closer.Close()
242
+	}(ctx, resp.Body)
243
+
244
+	defer func(rc io.ReadCloser) {
245
+		io.Copy(io.Discard, rc) // nolint: errcheck
246
+		rc.Close()
247
+	}(resp.Body)
248
+
249
+	return f.updateTrees(mutex, resp.Body, v4tree, v6tree)
250
+}
251
+
252
+func (f *Firehol) updateTrees(mutex sync.Locker,
253
+	reader io.Reader,
254
+	v4tree *bool_tree.TreeV4,
255
+	v6tree *bool_tree.TreeV6) error {
256
+	scanner := bufio.NewScanner(reader)
257
+
258
+	for scanner.Scan() {
259
+		text := scanner.Text()
260
+		text = fireholRegexpComment.ReplaceAllLiteralString(text, "")
261
+		text = strings.TrimSpace(text)
262
+
263
+		if text == "" {
264
+			continue
265
+		}
266
+
267
+		ip, cidr, err := f.updateParseLine(text)
268
+		if err != nil {
269
+			return fmt.Errorf("cannot parse a line: %w", err)
270
+		}
271
+
272
+		if err := f.updateAddToTrees(ip, cidr, mutex, v4tree, v6tree); err != nil {
273
+			return fmt.Errorf("cannot add a node to the tree: %w", err)
274
+		}
275
+	}
276
+
277
+	if scanner.Err() != nil {
278
+		return fmt.Errorf("cannot parse a response: %w", scanner.Err())
279
+	}
280
+
281
+	return nil
282
+}
283
+
284
+func (f *Firehol) updateParseLine(text string) (net.IP, uint, error) {
285
+	_, ipnet, err := net.ParseCIDR(text)
286
+	if err != nil {
287
+		ipaddr := net.ParseIP(text)
288
+		if ipaddr == nil {
289
+			return nil, 0, fmt.Errorf("incorrect ip address %s", text)
290
+		}
291
+
292
+		ip4 := ipaddr.To4()
293
+		if ip4 != nil {
294
+			return ip4, fireholIPv4DefaultCIDR, nil
295
+		}
296
+
297
+		return ipaddr.To16(), fireholIPv6DefaultCIDR, nil
298
+	}
299
+
300
+	ones, _ := ipnet.Mask.Size()
301
+
302
+	return ipnet.IP, uint(ones), nil
303
+}
304
+
305
+func (f *Firehol) updateAddToTrees(ip net.IP, cidr uint,
306
+	mutex sync.Locker,
307
+	v4tree *bool_tree.TreeV4, v6tree *bool_tree.TreeV6) error {
308
+	mutex.Lock()
309
+	defer mutex.Unlock()
310
+
311
+	if ip.To4() != nil {
312
+		addr := patricia.NewIPv4AddressFromBytes(ip, cidr)
313
+
314
+		if _, _, err := v4tree.Set(addr, true); err != nil {
315
+			return err // nolint: wrapcheck
316
+		}
317
+	} else {
318
+		addr := patricia.NewIPv6Address(ip, cidr)
319
+
320
+		if _, _, err := v6tree.Set(addr, true); err != nil {
321
+			return err // nolint: wrapcheck
322
+		}
323
+	}
324
+
325
+	return nil
326
+}
327
+
328
+// NewFirehol creates a new instance of FireHOL IP blocklist.
329
+//
330
+// This method does not start an update process so please execute Run
331
+// when it is necessary.
332
+func NewFirehol(logger mtglib.Logger, network mtglib.Network,
333
+	downloadConcurrency uint,
334
+	remoteURLs []string,
335
+	localFiles []string) (*Firehol, error) {
336
+	for _, v := range remoteURLs {
337
+		parsed, err := url.Parse(v)
338
+		if err != nil {
339
+			return nil, fmt.Errorf("incorrect url %s: %w", v, err)
340
+		}
341
+
342
+		switch parsed.Scheme {
343
+		case "http", "https":
344
+		default:
345
+			return nil, fmt.Errorf("unsupported url %s", v)
346
+		}
347
+	}
348
+
349
+	for _, v := range localFiles {
350
+		if stat, err := os.Stat(v); os.IsNotExist(err) || stat.IsDir() || stat.Mode().Perm()&0o400 == 0 {
351
+			return nil, fmt.Errorf("%s is not a readable file", v)
352
+		}
353
+	}
354
+
355
+	if downloadConcurrency == 0 {
356
+		downloadConcurrency = DefaultFireholDownloadConcurrency
357
+	}
358
+
359
+	workerPool, _ := ants.NewPool(int(downloadConcurrency))
360
+	ctx, cancel := context.WithCancel(context.Background())
361
+
362
+	return &Firehol{
363
+		ctx:        ctx,
364
+		ctxCancel:  cancel,
365
+		logger:     logger.Named("firehol"),
366
+		httpClient: network.MakeHTTPClient(nil),
367
+		treeV4:     bool_tree.NewTreeV4(),
368
+		treeV6:     bool_tree.NewTreeV6(),
369
+		workerPool: workerPool,
370
+		remoteURLs: remoteURLs,
371
+		localFiles: localFiles,
372
+	}, nil
373
+}

+ 0
- 0
ipblocklist/firehol_test.go Просмотреть файл


Некоторые файлы не были показаны из-за большого количества измененных файлов

Загрузка…
Отмена
Сохранить