├── .github
└── workflows
│ ├── golangci-lint.yml
│ └── release.yml
├── .gitignore
├── .goreleaser.yaml
├── LICENSE
├── README.md
├── client
├── client.go
└── cmd
│ └── main.go
├── configs
├── quictun-client.yaml
└── quictun-server.yaml
├── docs
└── test-report
│ ├── README.md
│ ├── bandwidth.md
│ ├── images
│ ├── bandwidth-direct-0.0.png
│ ├── bandwidth-direct-0.1.png
│ ├── bandwidth-direct-0.5.png
│ ├── bandwidth-direct-1.0.png
│ ├── bandwidth-direct-5.0.png
│ ├── bandwidth-proxy-0.0.png
│ ├── bandwidth-proxy-0.1.png
│ ├── bandwidth-proxy-0.5.png
│ ├── bandwidth-proxy-1.0.png
│ ├── bandwidth-proxy-5.0.png
│ ├── cpu-direct-0.0.png
│ ├── cpu-direct-0.1.png
│ ├── cpu-direct-0.5.png
│ ├── cpu-direct-1.0.png
│ ├── cpu-direct-5.0.png
│ ├── cpu-flame.png
│ ├── cpu-proxy-0.0.png
│ ├── cpu-proxy-0.1.png
│ ├── cpu-proxy-0.5.png
│ ├── cpu-proxy-1.0.png
│ ├── cpu-proxy-5.0.png
│ ├── vmware-setting1.png
│ └── vmware-setting2.png
│ └── latency.md
├── go.mod
├── go.sum
├── pkg
├── classifier
│ ├── headercache.go
│ ├── interface.go
│ └── spice.go
├── constants
│ └── constants.go
├── log
│ ├── log.go
│ └── options.go
├── options
│ ├── client.go
│ ├── config.go
│ ├── restfulapi.go
│ ├── secure.go
│ ├── server.go
│ └── utils.go
├── restfulapi
│ └── api.go
├── token
│ ├── cleartext_token_parser.go
│ ├── file_token_source.go
│ ├── fixed_token_source.go
│ ├── http_token_source.go
│ └── interface.go
└── tunnel
│ ├── datastore.go
│ ├── handshake.go
│ └── tunnel.go
├── quic-tun.png
├── server
├── cmd
│ └── main.go
└── server.go
└── tests
└── latency
├── README.md
├── client
└── server
/.github/workflows/golangci-lint.yml:
--------------------------------------------------------------------------------
1 | name: golangci-lint
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request:
7 | permissions:
8 | contents: read
9 | pull-requests: read
10 | jobs:
11 | golangci:
12 | name: lint
13 | runs-on: ubuntu-latest
14 | steps:
15 | -
16 | uses: actions/setup-go@v3
17 | with:
18 | go-version: 1.18
19 | -
20 | uses: actions/checkout@v3
21 | -
22 | name: golangci-lint
23 | uses: golangci/golangci-lint-action@v3
24 | with:
25 | version: latest
26 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: goreleaser
2 |
3 | on:
4 | pull_request:
5 | push:
6 | tags:
7 | - "v**"
8 | permissions:
9 | contents: write
10 |
11 | jobs:
12 | goreleaser:
13 | runs-on: ubuntu-latest
14 | steps:
15 | -
16 | name: Checkout
17 | uses: actions/checkout@v3
18 | -
19 | name: Fetch all tags
20 | run: git fetch --force --tags
21 | -
22 | name: Set up Go
23 | uses: actions/setup-go@v2
24 | with:
25 | go-version: 1.18
26 | -
27 | name: Run GoReleaser
28 | uses: goreleaser/goreleaser-action@v2
29 | with:
30 | distribution: goreleaser
31 | version: latest
32 | args: release --clean
33 | env:
34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 |
36 | - name: Upload assets
37 | uses: actions/upload-artifact@v4
38 | with:
39 | name: quic-tun
40 | path: dist/*
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 |
17 | dist/
18 | .vscode/
19 |
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | # This is an example .goreleaser.yml file with some sensible defaults.
2 | # Make sure to check the documentation at https://goreleaser.com
3 | before:
4 | hooks:
5 | # You may remove this if you don't use go modules.
6 | - go mod tidy
7 | builds:
8 | - main: ./client/cmd
9 | id: client
10 | binary: quictun-client
11 | goos:
12 | - linux
13 | - windows
14 | goarch:
15 | - amd64
16 | - arm64
17 | - main: ./server/cmd
18 | id: server
19 | binary: quictun-server
20 | goos:
21 | - linux
22 | - windows
23 | goarch:
24 | - amd64
25 | - arm64
26 | checksum:
27 | name_template: 'checksums.txt'
28 | snapshot:
29 | name_template: "{{ incpatch .Version }}"
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 JeffYang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # quic-tun
2 |
3 | [![Release][1]][2] [![MIT licensed][3]][4]
4 |
5 | [1]: https://img.shields.io/github/v/release/kungze/quic-tun?color=orange
6 | [2]: https://github.com/kungze/quic-tun/releases/latest
7 | [3]: https://img.shields.io/github/license/kungze/quic-tun
8 | [4]: LICENSE
9 |
10 | **WARNING**
11 |
12 | This project is no longer maintained. If you're looking for a replacement, please check out [wovenet](https://github.com/kungze/wovenet). It not only fully implements all the core features of quic-tun, but also provides many additional interesting and useful functionalities.
13 |
14 | Establish a fast&security tunnel, make you can access remote TCP/UNIX
15 | application like local application.
16 |
17 | ## Overview
18 |
19 | ``Quic-tun`` contains two command tools: ``quictun-server`` and ``quictun-client``,
20 | ``quictun-server`` used to translate application's transport layer protocol from
21 | TCP/UNIX to [QUIC](https://en.wikipedia.org/wiki/QUIC), and ``quictun-client``
22 | translate back to TCP/UNIX protocol at client side. The schematic diagram like below:
23 |
24 |
25 |
26 | ### Performance
27 |
28 | We wrote a [test report](docs/test-report/) about the difference of performance between transport
29 | packets by TCP directly and transport packets by `quic-tun`. If you want to know more informations,
30 | please consult it.
31 |
32 | ### Concerned issues
33 |
34 | If you are hesitating whether or not to study this document deeply or to play
35 | attention to this project. Maybe the below issues will help you to make your decision.
36 |
37 | * Whether you often encounter packet loss or high latency issues? And this issues influenced
38 | your application's performance or stabililty.
39 | * You have multiple applications and listen on multiple ports, but you don't want expose
40 | too much ports to internet.
41 | * Your application don't support TLS but the security issue is your concerned thing.
42 | * Your application listen on local UNIX socket, but you need to access it at other machines.
43 |
44 | If you encounter one or more above scenarios. Congratulations, you find the correct place!
45 |
46 | ## QuickStart
47 |
48 | Increase the maximum buffer size, read the
49 | [docs](https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size) for details
50 |
51 | ```shell
52 | sysctl -w net.core.rmem_max=2500000
53 | ```
54 |
55 | Download a corresponding one from precompiled from [releases](https://github.com/kungze/quic-tun/releases) and decompression it.
56 |
57 | ```shell
58 | wget https://github.com/kungze/quic-tun/releases/download/v0.0.1/quic-tun_0.0.1_linux_amd64.tar.gz
59 | ```
60 |
61 | ```shell
62 | tar xvfz quic-tun_0.0.1_linux_amd64.tar.gz
63 | ```
64 |
65 | Start up server side endpoint
66 |
67 | ```console
68 | ./quictun-server --listen-on 172.18.31.36:7500
69 | ```
70 |
71 | Start up client side endpoint
72 |
73 | ```console
74 | ./quictun-client --listen-on tcp:127.0.0.1:6500 --server-endpoint 172.18.31.36:7500 --token-source tcp:172.18.30.117:22
75 | ```
76 |
77 | **Note:** The value specified by `--token-source` used to tell `quictun-server` the application address that the client want to access.
78 |
79 | Use `ssh` command to test
80 |
81 | ```console
82 | $ ssh root@127.0.0.1 -p 6500
83 | root@127.0.0.1's password:
84 | ```
85 |
86 | ## Concepts
87 |
88 | * **client endpoint:** A service run on client side, used to accept the client applications' connection request and convert the transport layer protocol from TCP/UNIX-SOCKET to QUIC.
89 | * **server endpoint:** A service run on server side, used to accept the data from client endpoint and forward these data to server application by TCP/UNIX-SOCKET protocol.
90 | * **token:** When a client endpoint receive a new connection request, the client endpoint will retrieve a token according to the request's source address and send the token to server endpoint, the server endpoint will parse and verify the token and get the server application socket address from parsed result. ``quic-tun`` provide multiple type token plugin in order to adapt different use cases.
91 | * **tunnel** ``quic-tun`` will create a tunnel for each TCP/UNIX-SOCKET connection, one tunnel corresponding with one QUIC stream.
92 |
93 | ## Token plugin
94 |
95 | ### quictun-client
96 |
97 | At client side, We address the token plugin as token source plugin, related command options ``--token-source-plugin``, ``--token-source``. Currently, ``quic-tun`` provide three type token source plugin: ``Fixed``, ``File`` and ``Http``.
98 |
99 | #### Fixed
100 |
101 | ``Fixed`` token source plugin always provide one same token, this mean that all of client applications just only connect to one fixed server application.
102 |
103 | Example:
104 |
105 | ```console
106 | ./quictun-client --listen-on tcp:127.0.0.1:6500 --server-endpoint 172.18.31.36:7500 --token-source-plugin Fixed --token-source tcp:172.18.30.117:22
107 | ```
108 |
109 | #### File
110 |
111 | ``File`` token source plugin will read token from a file and return different token according to the client application's source address. The file path specified by ``--token-source``.
112 |
113 | The file's contents like below:
114 |
115 | ```text
116 | 172.26.106.191 tcp:10.20.30.5:2256
117 | 172.26.106.192 tcp:10.20.30.6:3306
118 | 172.26.106.193 tcp:10.20.30.6:3306
119 | ```
120 |
121 | The first column are the client application's IP addresses, the second column are the token(The server application's socket addresses which the client application want to access.)
122 |
123 | Example:
124 |
125 | ```console
126 | ./quictun-client --server-endpoint 127.0.0.1:7500 --token-source-plugin File --token-source /etc/quictun/tokenfile --listen-on tcp:172.18.31.36:6622
127 | ```
128 |
129 | #### Http
130 |
131 | ``Http`` token source plugin will get the token based on the URL configured by ``--token-source``.
132 |
133 | The ``Http`` token source plugin is requested as follows.
134 |
135 | ```text
136 | http://172.18.31.36:8081/get?addr=192.168.110.116:61313
137 | ```
138 |
139 | The ``--token-source`` should return data in the following format.
140 |
141 | ```json
142 | {
143 | "token": "tcp:172.27.130.202:5913"
144 | }
145 | ```
146 |
147 | Example:
148 |
149 | ```console
150 | ./quictun-client --listen-on tcp:127.0.0.1:6500 --server-endpoint 172.18.31.36:7500 --token-source-plugin Http --token-source http://172.18.31.36:8081/get
151 | ```
152 |
153 | ### quictun-server
154 |
155 | At server side, we address the token plugin as token parser plugin, it used to parse and verify the token and get the server application socket address from the parse result, related command option ``--token-parser-plugin``, ``--token-parser-key``. Currently, ``quic-tun`` just provide one token parser plugin: ``Cleartext``.
156 |
157 | #### Cleartext
158 |
159 | ``Cleartext`` token parser plugin require the token mustn't be encrypted. But you can use ``base64`` to encode token.
160 |
161 | Example:
162 |
163 | If the client endpoint token is not encoded.
164 |
165 | ```console
166 | ./quictun-server --listen-on 172.18.31.36:7500 --token-parser-plugin Cleartext
167 | ```
168 |
169 | If the client endpoint token is encoded by ``base64``
170 |
171 | ```console
172 | ./quictun-server --listen-on 172.18.31.36:7500 --token-parser-plugin Cleartext --token-parser-key base64
173 | ```
174 |
175 | ## Restful API
176 |
177 | ``quic-tun`` also provide some restful API. By these APIs, you can query the information of the tunnels which are active.
178 | You can set address of the API server listen on by ``--httpd-listen-on`` when you start server/client endpoint server, like below:
179 |
180 | ```console
181 | ./quictun-server --httpd-listen-on 127.0.0.1:18086
182 | ```
183 |
184 | Then you can use ``curl`` command to query all active tunnels, like below:
185 |
186 | ```console
187 | $ curl http://127.0.0.1:18086/tunnels | jq .
188 | % Total % Received % Xferd Average Speed Time Time Time Current
189 | Dload Upload Total Spent Left Speed
190 | 100 227 100 227 0 0 221k 0 --:--:-- --:--:-- --:--:-- 221k
191 | [
192 | {
193 | "uuid": "2e1ce596-8357-4a46-aef1-0c4871b893cd",
194 | "streamId": 4,
195 | "endpoint": "server",
196 | "serverAppAddr": "172.18.31.97:22",
197 | "remoteEndpointAddr": "172.18.29.161:46706",
198 | "createdAt": "2022-06-21 11:44:05.074778434 +0800 CST m=+86.092908233",
199 | "serverTotalBytes": 1545,
200 | "clientTotalBytes": 2221,
201 | "serverSendRate": "0.00 kB/s",
202 | "clientSendRate": "0.00 kB/s",
203 | "protocol": "",
204 | "protocolProperties": null
205 | }
206 | ]
207 | ```
208 |
209 | Additionally, we implement a [Spice protocol](https://www.spice-space.org/spice-protocol.html) discriminator,
210 | it can extract more properties about spice from the traffic pass through the tunnel. So, for spice application,
211 | call the query API, you can get the below response:
212 |
213 | ```console
214 | # curl http://172.18.29.161:18086/tunnels | jq .
215 | % Total % Received % Xferd Average Speed Time Time Time Current
216 | Dload Upload Total Spent Left Speed
217 | 100 5137 0 5137 0 0 2508k 0 --:--:-- --:--:-- --:--:-- 5016k
218 | [
219 | {
220 | "uuid": "9eb73491-ef38-463d-85c3-d4512152d224",
221 | "streamId": 0,
222 | "endpoint": "server",
223 | "serverAppAddr": "172.18.11.2:5915",
224 | "remoteEndpointAddr": "172.18.29.161:56465",
225 | "createdAt": "2022-06-21 11:41:28.85774404 +0800 CST m=+47.535828999",
226 | "serverTotalBytes": 1545,
227 | "clientTotalBytes": 2221,
228 | "serverSendRate": "0.00 kB/s",
229 | "clientSendRate": "0.00 kB/s",
230 | "protocol": "spice",
231 | "protocolProperties": {
232 | "version": "2.2",
233 | "sessionId": "d0306d75",
234 | "channelType": "main",
235 | "serverName": "instance-e548a827-8937-4047-a756-e56937017128",
236 | "serverUUID": "e548a827-8937-4047-a756-e56937017128"
237 | }
238 | },
239 | {
240 | "uuid": "66bad84d-318c-4e14-b3be-a5cb796e7f61",
241 | "streamId": 44,
242 | "endpoint": "server",
243 | "serverAppAddr": "172.18.11.2:5915",
244 | "remoteEndpointAddr": "172.18.29.161:56465",
245 | "createdAt": "2022-06-21 11:41:28.937090895 +0800 CST m=+47.615175866",
246 | "serverTotalBytes": 1545,
247 | "clientTotalBytes": 2221,
248 | "serverSendRate": "0.00 kB/s",
249 | "clientSendRate": "0.00 kB/s",
250 | "protocol": "spice",
251 | "protocolProperties": {
252 | "version": "2.2",
253 | "sessionId": "d0306d75",
254 | "channelType": "record"
255 | }
256 | },
257 | {
258 | "uuid": "ff93728e-38fb-435c-8728-3ece51077b95",
259 | "streamId": 56,
260 | "endpoint": "server",
261 | "serverAppAddr": "172.18.11.2:5915",
262 | "remoteEndpointAddr": "172.18.29.161:56465",
263 | "createdAt": "2022-06-21 11:41:29.224234488 +0800 CST m=+47.902319441",
264 | "serverTotalBytes": 1545,
265 | "clientTotalBytes": 2221,
266 | "serverSendRate": "0.00 kB/s",
267 | "clientSendRate": "0.00 kB/s",
268 | "protocol": "spice",
269 | "protocolProperties": {
270 | "version": "2.2",
271 | "sessionId": "d0306d75",
272 | "channelType": "inputs"
273 | }
274 | },
275 | {
276 | "uuid": "fbfd963c-e6b9-4c13-bfec-8965b1c56851",
277 | "streamId": 12,
278 | "endpoint": "server",
279 | "serverAppAddr": "172.18.11.2:5915",
280 | "remoteEndpointAddr": "172.18.29.161:56465",
281 | "createdAt": "2022-06-21 11:41:28.93269002 +0800 CST m=+47.610774997",
282 | "serverTotalBytes": 1545,
283 | "clientTotalBytes": 2221,
284 | "serverSendRate": "0.00 kB/s",
285 | "clientSendRate": "0.00 kB/s",
286 | "protocol": "spice",
287 | "protocolProperties": {
288 | "version": "2.2",
289 | "sessionId": "d0306d75",
290 | "channelType": "usbredir"
291 | }
292 | },
293 | {
294 | "uuid": "62fa355c-9c0d-4dbb-9e84-2b0c354cf8cc",
295 | "streamId": 48,
296 | "endpoint": "server",
297 | "serverAppAddr": "172.18.11.2:5915",
298 | "remoteEndpointAddr": "172.18.29.161:56465",
299 | "createdAt": "2022-06-21 11:41:28.937563866 +0800 CST m=+47.615648836",
300 | "serverTotalBytes": 1545,
301 | "clientTotalBytes": 2221,
302 | "serverSendRate": "0.00 kB/s",
303 | "clientSendRate": "0.00 kB/s",
304 | "protocol": "spice",
305 | "protocolProperties": {
306 | "version": "2.2",
307 | "sessionId": "d0306d75",
308 | "channelType": "display"
309 | }
310 | },
311 | {
312 | "uuid": "ce2e0bef-0ccb-4325-ab65-a6d3783c47ae",
313 | "streamId": 52,
314 | "endpoint": "server",
315 | "serverAppAddr": "172.18.11.2:5915",
316 | "remoteEndpointAddr": "172.18.29.161:56465",
317 | "createdAt": "2022-06-21 11:41:29.223947759 +0800 CST m=+47.902032695",
318 | "protocol": "spice",
319 | "serverTotalBytes": 1545,
320 | "clientTotalBytes": 2221,
321 | "serverSendRate": "0.00 kB/s",
322 | "clientSendRate": "0.00 kB/s",
323 | "protocolProperties": {
324 | "version": "2.2",
325 | "sessionId": "d0306d75",
326 | "channelType": "cursor"
327 | }
328 | },
329 | {
330 | "uuid": "c5169c4a-ab69-406b-b36a-68c0ab7d9d7f",
331 | "streamId": 40,
332 | "endpoint": "server",
333 | "serverAppAddr": "172.18.11.2:5915",
334 | "remoteEndpointAddr": "172.18.29.161:56465",
335 | "createdAt": "2022-06-21 11:41:28.936673702 +0800 CST m=+47.614758657",
336 | "serverTotalBytes": 1545,
337 | "clientTotalBytes": 2221,
338 | "serverSendRate": "0.00 kB/s",
339 | "clientSendRate": "0.00 kB/s",
340 | "protocol": "spice",
341 | "protocolProperties": {
342 | "version": "2.2",
343 | "sessionId": "d0306d75",
344 | "channelType": "playback"
345 | }
346 | }
347 | ]
348 | ```
349 |
--------------------------------------------------------------------------------
/client/client.go:
--------------------------------------------------------------------------------
1 | package client
2 |
3 | import (
4 | "context"
5 | "crypto/tls"
6 | "fmt"
7 | "io"
8 | "net"
9 | "strings"
10 |
11 | "github.com/kungze/quic-tun/pkg/constants"
12 | "github.com/kungze/quic-tun/pkg/log"
13 | "github.com/kungze/quic-tun/pkg/token"
14 | "github.com/kungze/quic-tun/pkg/tunnel"
15 | "github.com/lucas-clemente/quic-go"
16 | )
17 |
18 | type ClientEndpoint struct {
19 | LocalSocket string
20 | ServerEndpointSocket string
21 | TokenSource token.TokenSourcePlugin
22 | TlsConfig *tls.Config
23 | }
24 |
25 | func (c *ClientEndpoint) Start() {
26 | // Dial server endpoint
27 | session, err := quic.DialAddr(c.ServerEndpointSocket, c.TlsConfig, &quic.Config{KeepAlive: true})
28 | if err != nil {
29 | panic(err)
30 | }
31 | parent_ctx := context.WithValue(context.TODO(), constants.CtxRemoteEndpointAddr, session.RemoteAddr().String())
32 | // Listen on a TCP or UNIX socket, wait client application's connection request.
33 | localSocket := strings.Split(c.LocalSocket, ":")
34 | listener, err := net.Listen(strings.ToLower(localSocket[0]), strings.Join(localSocket[1:], ":"))
35 | if err != nil {
36 | panic(err)
37 | }
38 | defer listener.Close()
39 | log.Infow("Client endpoint start up successful", "listen address", listener.Addr())
40 | for {
41 | // Accept client application connectin request
42 | conn, err := listener.Accept()
43 | if err != nil {
44 | log.Errorw("Client app connect failed", "error", err.Error())
45 | } else {
46 | logger := log.WithValues(constants.ClientAppAddr, conn.RemoteAddr().String())
47 | logger.Info("Client connection accepted, prepare to entablish tunnel with server endpint for this connection.")
48 | go func() {
49 | defer func() {
50 | conn.Close()
51 | logger.Info("Tunnel closed")
52 | }()
53 | // Open a quic stream for each client application connection.
54 | stream, err := session.OpenStreamSync(context.Background())
55 | if err != nil {
56 | logger.Errorw("Failed to open stream to server endpoint.", "error", err.Error())
57 | return
58 | }
59 | defer stream.Close()
60 | logger = logger.WithValues(constants.StreamID, stream.StreamID())
61 | // Create a context argument for each new tunnel
62 | ctx := context.WithValue(
63 | logger.WithContext(parent_ctx),
64 | constants.CtxClientAppAddr, conn.RemoteAddr().String())
65 | hsh := tunnel.NewHandshakeHelper(constants.TokenLength, handshake)
66 | hsh.TokenSource = &c.TokenSource
67 | // Create a new tunnel for the new client application connection.
68 | tun := tunnel.NewTunnel(&stream, constants.ClientEndpoint)
69 | tun.Conn = &conn
70 | tun.Hsh = &hsh
71 | if !tun.HandShake(ctx) {
72 | return
73 | }
74 | tun.Establish(ctx)
75 | }()
76 | }
77 | }
78 | }
79 |
80 | func handshake(ctx context.Context, stream *quic.Stream, hsh *tunnel.HandshakeHelper) (bool, *net.Conn) {
81 | logger := log.FromContext(ctx)
82 | logger.Info("Starting handshake with server endpoint")
83 | token, err := (*hsh.TokenSource).GetToken(fmt.Sprint(ctx.Value(constants.CtxClientAppAddr)))
84 | if err != nil {
85 | logger.Errorw("Encounter error.", "erros", err.Error())
86 | return false, nil
87 | }
88 | hsh.SetSendData([]byte(token))
89 | _, err = io.CopyN(*stream, hsh, constants.TokenLength)
90 | if err != nil {
91 | logger.Errorw("Failed to send token", err.Error())
92 | return false, nil
93 | }
94 | _, err = io.CopyN(hsh, *stream, constants.AckMsgLength)
95 | if err != nil {
96 | logger.Errorw("Failed to receive ack", err.Error())
97 | return false, nil
98 | }
99 | switch hsh.ReceiveData[0] {
100 | case constants.HandshakeSuccess:
101 | logger.Info("Handshake successful")
102 | return true, nil
103 | case constants.ParseTokenError:
104 | logger.Errorw("handshake error!", "error", "server endpoint can not parser token")
105 | return false, nil
106 | case constants.CannotConnServer:
107 | logger.Errorw("handshake error!", "error", "server endpoint can not connect to server application")
108 | return false, nil
109 | default:
110 | logger.Errorw("handshake error!", "error", "received an unknow ack info")
111 | return false, nil
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/client/cmd/main.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2022 NAME HERE
3 |
4 | */
5 | package main
6 |
7 | import (
8 | "crypto/tls"
9 | "crypto/x509"
10 | "flag"
11 | "fmt"
12 | "os"
13 | "strings"
14 |
15 | "github.com/kungze/quic-tun/client"
16 | "github.com/kungze/quic-tun/pkg/log"
17 | "github.com/kungze/quic-tun/pkg/options"
18 | "github.com/kungze/quic-tun/pkg/restfulapi"
19 | "github.com/kungze/quic-tun/pkg/token"
20 | "github.com/spf13/cobra"
21 | "github.com/spf13/viper"
22 | )
23 |
24 | var (
25 | clientOptions *options.ClientOptions
26 | apiOptions *options.RestfulAPIOptions
27 | secOptions *options.SecureOptions
28 | logOptions *log.Options
29 | )
30 |
31 | func buildCommand(basename string) *cobra.Command {
32 | rootCmd := &cobra.Command{
33 | Use: basename,
34 | Short: "Start up the client side endpoint",
35 | Long: `Establish a fast&security tunnel,
36 | make you can access remote TCP/UNIX application like local application.
37 |
38 | Find more quic-tun information at:
39 | https://github.com/kungze/quic-tun/blob/master/README.md`,
40 | RunE: runCommand,
41 | }
42 | // Initialize the flags needed to start the server
43 | rootCmd.Flags().AddGoFlagSet(flag.CommandLine)
44 | clientOptions.AddFlags(rootCmd.Flags())
45 | apiOptions.AddFlags(rootCmd.Flags())
46 | secOptions.AddFlags(rootCmd.Flags())
47 | options.AddConfigFlag(basename, rootCmd.Flags())
48 | logOptions.AddFlags(rootCmd.Flags())
49 |
50 | return rootCmd
51 | }
52 |
53 | func runCommand(cmd *cobra.Command, args []string) error {
54 | options.PrintWorkingDir()
55 | options.PrintFlags(cmd.Flags())
56 | options.PrintConfig()
57 |
58 | if err := viper.BindPFlags(cmd.Flags()); err != nil {
59 | return err
60 | }
61 |
62 | if err := viper.Unmarshal(clientOptions); err != nil {
63 | return err
64 | }
65 |
66 | if err := viper.Unmarshal(apiOptions); err != nil {
67 | return err
68 | }
69 |
70 | if err := viper.Unmarshal(secOptions); err != nil {
71 | return err
72 | }
73 |
74 | if err := viper.Unmarshal(logOptions); err != nil {
75 | return err
76 | }
77 |
78 | // run server
79 | runFunc(clientOptions, apiOptions, secOptions)
80 | return nil
81 | }
82 |
83 | func runFunc(co *options.ClientOptions, ao *options.RestfulAPIOptions, seco *options.SecureOptions) {
84 | log.Init(logOptions)
85 | defer log.Flush()
86 |
87 | localSocket := co.ListenOn
88 | serverEndpointSocket := co.ServerEndpointSocket
89 | tokenPlugin := co.TokenPlugin
90 | tokenSource := co.TokenSource
91 | certFile := seco.CertFile
92 | keyFile := seco.KeyFile
93 | caFile := seco.CaFile
94 | verifyServer := seco.VerifyRemoteEndpoint
95 | apiListenOn := ao.HttpdListenOn
96 |
97 | tlsConfig := &tls.Config{
98 | InsecureSkipVerify: !verifyServer,
99 | NextProtos: []string{"quic-tun"},
100 | }
101 | if certFile != "" && keyFile != "" {
102 | tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile)
103 | if err != nil {
104 | log.Errorw("Certificate file or private key file is invalid.", "error", err.Error())
105 | return
106 | }
107 | tlsConfig.Certificates = []tls.Certificate{tlsCert}
108 | }
109 | if caFile != "" {
110 | caPemBlock, err := os.ReadFile(caFile)
111 | if err != nil {
112 | log.Errorw("Failed to read ca file.", "error", err.Error())
113 | }
114 | certPool := x509.NewCertPool()
115 | certPool.AppendCertsFromPEM(caPemBlock)
116 | tlsConfig.RootCAs = certPool
117 | } else {
118 | certPool, err := x509.SystemCertPool()
119 | if err != nil {
120 | log.Errorw("Failed to load system cert pool", "error", err.Error())
121 | return
122 | }
123 | tlsConfig.ClientCAs = certPool
124 | }
125 |
126 | // Start API server
127 | httpd := restfulapi.NewHttpd(apiListenOn)
128 | go httpd.Start()
129 |
130 | // Start client endpoint
131 | c := client.ClientEndpoint{
132 | LocalSocket: localSocket,
133 | ServerEndpointSocket: serverEndpointSocket,
134 | TokenSource: loadTokenSourcePlugin(tokenPlugin, tokenSource),
135 | TlsConfig: tlsConfig,
136 | }
137 | c.Start()
138 | }
139 |
140 | func loadTokenSourcePlugin(plugin string, source string) token.TokenSourcePlugin {
141 | switch strings.ToLower(plugin) {
142 | case "fixed":
143 | return token.NewFixedTokenPlugin(source)
144 | case "file":
145 | return token.NewFileTokenSourcePlugin(source)
146 | case "http":
147 | return token.NewHttpTokenPlugin(source)
148 | default:
149 | panic(fmt.Sprintf("The token source plugin %s is invalid", plugin))
150 | }
151 | }
152 |
153 | func main() {
154 | // Initialize the options needed to start the server
155 | clientOptions = options.GetDefaultClientOptions()
156 | apiOptions = options.GetDefaultRestfulAPIOptions()
157 | secOptions = options.GetDefaultSecureOptions()
158 | logOptions = log.NewOptions()
159 |
160 | rootCmd := buildCommand("quictun-client")
161 | if err := rootCmd.Execute(); err != nil {
162 | os.Exit(1)
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/configs/quictun-client.yaml:
--------------------------------------------------------------------------------
1 | # quic-tun-client config
2 |
3 | # Client
4 | listen-on: "tcp:127.0.0.1:6500" # (default "tcp:127.0.0.1:6500")
5 | server-endpoint: "192.168.110.116:7501" # The address to connect to the QUIC-TUN server. (eg 192.168.xxx.xxx:7500)
6 | token-source-plugin: "Fixed" # (default "Fixed")
7 | token-source: "tcp:192.168.110.116:22" # (eg tcp:192.168.110.116:22)
8 |
9 | # TLS
10 | cert-file: "" # x509 certificate
11 | key-file: "" # TLS private key
12 | verify-remote-endpoint: false # (default false)
13 | ca-file: ""
14 |
15 | # RestfulAPI
16 | httpd-listen-on: "0.0.0.0:8086" # (default 0.0.0.0:8086)
17 |
18 | # LOG
19 | log-name: quictun-client # Logger's name
20 | log-development: false # Is it in development mode. If it is in development mode, it will DPanicLevel for stack traces.
21 | log-level: info # Log level, the priority from low to high is:debug, info, warn, error, dpanic, panic, fatal。
22 | log-format: console # The supported log output formats currently support console and json. console is actually text format.
23 | log-disable-caller: false # Whether to enable caller, if enabled, the file, function and line number where the call log is located will be displayed in the log
24 | log-disable-stacktrace: false # Whether to disable printing stack information at panic and above levels
25 | log-output-paths: ./quictun-client.log,stdout # Supports output to multiple outputs, separated by commas. Supports output to standard output (stdout) and files.
26 | log-error-output-paths: ./quictun-client.error.log # Zap internal (non business) error log output path, multiple outputs, separated by commas
27 |
--------------------------------------------------------------------------------
/configs/quictun-server.yaml:
--------------------------------------------------------------------------------
1 | # quic-tun-server config
2 |
3 | # Server
4 | listen-on: "0.0.0.0:7500" # (default "0.0.0.0:7500")
5 | token-parser-plugin: "Cleartext" # (default "Cleartext")
6 | token-parser-key: "" # (default "")
7 |
8 | # TLS
9 | cert-file: "" # x509 certificate
10 | key-file: "" # TLS private key
11 | verify-remote-endpoint: false # (default false)
12 | ca-file: ""
13 |
14 | # RestfulAPI
15 | httpd-listen-on: "0.0.0.0:8086" # (default 0.0.0.0:8086)
16 |
17 | # LOG
18 | log-name: quictun-server # Logger's name
19 | log-development: false # Is it in development mode. If it is in development mode, it will DPanicLevel for stack traces.
20 | log-level: info # Log level, the priority from low to high is:debug, info, warn, error, dpanic, panic, fatal。
21 | log-format: console # The supported log output formats currently support console and json. console is actually text format.
22 | log-disable-caller: false # Whether to enable caller, if enabled, the file, function and line number where the call log is located will be displayed in the log
23 | log-disable-stacktrace: false # Whether to disable printing stack information at panic and above levels
24 | log-output-paths: ./quictun-server.log,stdout # Supports output to multiple outputs, separated by commas. Supports output to standard output (stdout) and files.
25 | log-error-output-paths: ./quictun-server.error.log # Zap internal (non business) error log output path, multiple outputs, separated by commas
26 |
--------------------------------------------------------------------------------
/docs/test-report/README.md:
--------------------------------------------------------------------------------
1 | # Performance test
2 |
3 | For verify the effect of `quic-tun` in network performance, I make some tests and wrote
4 | this report.
5 |
6 | The following three metrics should be focused during testing: Network Bandwidth, Network Latency, CPU usage. We
7 | will compare the these performance metrics between transport traffic directly by TCP and transport traffic by
8 | `quic-tun`. We will also make different packet loss rate network environment.
9 |
10 | ## Test environment and tools
11 |
12 | * Two local vmware workstation virtual machines, One as server machine, anthor one as client machine.
13 | * One remote machine that can be access by public network, used to test network latency in WAN.
14 | * Operating-system: ubuntu 20.02
15 | * Network Bandwidth test tools: iperf3
16 | * Use `top` command to watch the CPU useage
17 |
18 | Additionally, I haven't find good tool to test Network Latency. So, I use python wrote a [test tool](../../tests/latency/README.md) myself.
19 |
20 | ## How to set the packet loss rate
21 |
22 | VMware workstation support set the packet loss rate for a specified virtual machine, like below:
23 |
24 |
25 |
26 |
27 | In all test cases, I just change the client machine's packet loss rate, moreover the
28 | incoming packet loss rate and outgoing packet loss rate are same. The server machine
29 | don't set packet loss rate.
30 |
31 | ## Test results
32 |
33 | * [Network Bandwidth and CPU Usage](bandwidth.md)
34 | * [Network Latency](latency.md)
35 |
--------------------------------------------------------------------------------
/docs/test-report/bandwidth.md:
--------------------------------------------------------------------------------
1 | # Network Bandwidth and CPU Usage Test
2 |
3 | ## Preparation
4 |
5 | * In server machine:
6 |
7 | Start iperf3 server
8 |
9 | ```console
10 | $ iperf3 -s
11 | -----------------------------------------------------------
12 | Server listening on 5201
13 | -----------------------------------------------------------
14 | ```
15 |
16 | Open a new terminal, start `quictun-server`:
17 |
18 | ```console
19 | $ ./quictun-server
20 | I0624 09:15:29.223140 1515 server.go:30] "Server endpoint start up successful" listen address="[::]:7500"
21 | ```
22 |
23 | * In client machine
24 |
25 | Start `quictun-client`
26 |
27 | ```console
28 | $ ./quictun-client --server-endpoint 192.168.26.129:7500 --token-source tcp:127.0.0.1:5201 --insecure-skip-verify true
29 | I0624 09:17:30.926905 1679 client.go:35] "Client endpoint start up successful" listen address="127.0.0.1:6500"
30 | ```
31 |
32 | The `192.168.26.129` is the server machine's IP address.
33 |
34 | Open a new terminal, execute `iperf3 -c` command to start test:
35 |
36 | * Test the performance by TCP directly:
37 |
38 | ```console
39 | iperf3 -c 192.168.26.129 -p 5201 -t 20
40 | ```
41 |
42 | * Test the performance by `quic-tun` forward the traffic:
43 |
44 | ```console
45 | iperf3 -c 127.0.0.1 -p 6500 -t 20
46 | ```
47 |
48 | Open a new terminal, execute `top` command to watch the CPU usage:
49 |
50 | ```console
51 | top
52 | ```
53 |
54 | ## Test results
55 |
56 | For show results more clearly, I just only remain the client machine's screenshots,
57 | That's enough for us.
58 |
59 | ### Packet loss rate: 0.0%
60 |
61 | * TCP
62 |
63 |
64 |
65 |
66 | * quic-tun
67 |
68 |
69 |
70 |
71 | ### Packet loss rate: 0.1%
72 |
73 | * TCP
74 |
75 |
76 |
77 |
78 | * quic-tun
79 |
80 |
81 |
82 |
83 | ### Packet loss rate: 0.5%
84 |
85 | * TCP
86 |
87 |
88 |
89 |
90 | * quic-tun
91 |
92 |
93 |
94 |
95 | ### Packet loss rate: 1.0%
96 |
97 | * TCP
98 |
99 |
100 |
101 |
102 | * quic-tun
103 |
104 |
105 |
106 |
107 | ### Packet loss rate: 5.0%
108 |
109 | * TCP
110 |
111 |
112 |
113 |
114 | * quic-tun
115 |
116 |
117 |
118 |
119 | ### Summarize Table
120 |
121 | * TCP
122 |
123 | |packet loss rate(%)|sender bitRate(Mbits/sec)|reveiver bitRate(Mbits/sec)|CPU(ksoftirqd)|
124 | |:------------------|:-----|:-----|:-----|
125 | | 0 | 562 | 561 | 18.7 |
126 | | 0.1 | 307 | 306 | 10.0 |
127 | | 0.5 | 80.2 | 79.9 | 1.0 |
128 | | 1 |60.7 | 60.3 | 2.0 |
129 | | 5 |13.0 | 12.8 | 1.3 |
130 |
131 | * quic-tun
132 |
133 | |packet loss rate(%)|sender bitRate(Mbits/sec)|reveiver bitRate(Mbits/sec)|CPU(ksoftirqd)|CPU(quictun-client)|
134 | |:------------------|:-----|:-----|:-----|:-----|
135 | | 0 | 604 | 601 | 2.7 | 75.1 |
136 | | 0.1 | 348 | 344 | 2.0 | 66.3 |
137 | | 0.5 | 239 | 235 | 1.0 | 49.8 |
138 | | 1 | 138 | 135 | 1.0 | 35.7 |
139 | | 5 | 38.8 | 36.4 | 1.3 | 22.9 |
140 |
141 | ## CPU flame graph
142 |
143 | In order to analy that why the `quictun` CPU usage is so high, especially when the traffic is high. I draw a CPU flame graph.
144 |
145 |
146 |
147 | By above graph, we can find that the `udp_sendmsg` occupy vast CPU. I look up some documents on
148 | internet find the below document maybe useful for decrease the CPU usage, but I haven't learn it
149 | in deep yet.
150 |
151 | https://conferences.sigcomm.org/sigcomm/2020/files/slides/epiq/0%20QUIC%20and%20HTTP_3%20CPU%20Performance.pdf
152 |
--------------------------------------------------------------------------------
/docs/test-report/images/bandwidth-direct-0.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/bandwidth-direct-0.0.png
--------------------------------------------------------------------------------
/docs/test-report/images/bandwidth-direct-0.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/bandwidth-direct-0.1.png
--------------------------------------------------------------------------------
/docs/test-report/images/bandwidth-direct-0.5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/bandwidth-direct-0.5.png
--------------------------------------------------------------------------------
/docs/test-report/images/bandwidth-direct-1.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/bandwidth-direct-1.0.png
--------------------------------------------------------------------------------
/docs/test-report/images/bandwidth-direct-5.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/bandwidth-direct-5.0.png
--------------------------------------------------------------------------------
/docs/test-report/images/bandwidth-proxy-0.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/bandwidth-proxy-0.0.png
--------------------------------------------------------------------------------
/docs/test-report/images/bandwidth-proxy-0.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/bandwidth-proxy-0.1.png
--------------------------------------------------------------------------------
/docs/test-report/images/bandwidth-proxy-0.5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/bandwidth-proxy-0.5.png
--------------------------------------------------------------------------------
/docs/test-report/images/bandwidth-proxy-1.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/bandwidth-proxy-1.0.png
--------------------------------------------------------------------------------
/docs/test-report/images/bandwidth-proxy-5.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/bandwidth-proxy-5.0.png
--------------------------------------------------------------------------------
/docs/test-report/images/cpu-direct-0.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/cpu-direct-0.0.png
--------------------------------------------------------------------------------
/docs/test-report/images/cpu-direct-0.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/cpu-direct-0.1.png
--------------------------------------------------------------------------------
/docs/test-report/images/cpu-direct-0.5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/cpu-direct-0.5.png
--------------------------------------------------------------------------------
/docs/test-report/images/cpu-direct-1.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/cpu-direct-1.0.png
--------------------------------------------------------------------------------
/docs/test-report/images/cpu-direct-5.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/cpu-direct-5.0.png
--------------------------------------------------------------------------------
/docs/test-report/images/cpu-flame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/cpu-flame.png
--------------------------------------------------------------------------------
/docs/test-report/images/cpu-proxy-0.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/cpu-proxy-0.0.png
--------------------------------------------------------------------------------
/docs/test-report/images/cpu-proxy-0.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/cpu-proxy-0.1.png
--------------------------------------------------------------------------------
/docs/test-report/images/cpu-proxy-0.5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/cpu-proxy-0.5.png
--------------------------------------------------------------------------------
/docs/test-report/images/cpu-proxy-1.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/cpu-proxy-1.0.png
--------------------------------------------------------------------------------
/docs/test-report/images/cpu-proxy-5.0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/cpu-proxy-5.0.png
--------------------------------------------------------------------------------
/docs/test-report/images/vmware-setting1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/vmware-setting1.png
--------------------------------------------------------------------------------
/docs/test-report/images/vmware-setting2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/docs/test-report/images/vmware-setting2.png
--------------------------------------------------------------------------------
/docs/test-report/latency.md:
--------------------------------------------------------------------------------
1 | # Network Latency Test
2 |
3 | In the test scenario, we will have some public network test cases, in these test
4 | cases we place the server machine in a remoter location.
5 |
6 | ## Preparation
7 |
8 | * In server machine
9 |
10 | Start TCP service
11 |
12 | ```console
13 | $ ./server
14 | The server listen on 0.0.0.0:15676
15 | ```
16 |
17 | Open a new terminal, start `quictun-server`:
18 |
19 | ```console
20 | $ ./quictun-server
21 | I0624 09:15:29.223140 1515 server.go:30] "Server endpoint start up successful" listen address="[::]:7500"
22 | ```
23 |
24 | * In client machine
25 |
26 | Start `quictun-client`
27 |
28 | ```console
29 | $ ./quictun-client --server-endpoint 192.168.26.129:7500 --token-source tcp:127.0.0.1:5201 --insecure-skip-verify true
30 | I0624 09:17:30.926905 1679 client.go:35] "Client endpoint start up successful" listen address="127.0.0.1:6500"
31 | ```
32 |
33 | The `192.168.26.129` is the server machine's IP address, in public network test cases this is a public IP address.
34 |
35 | Open a new terminal, run `client` python script to test network latency.
36 |
37 | * Test the performance by TCP directly:
38 |
39 | ```console
40 | ./client --server-host 192.168.26.131
41 | ```
42 |
43 | * Test the performance by `quic-tun` forward the traffic:
44 |
45 | ```console
46 | ./client --server-host 127.0.0.1 --server-port 6500
47 | ```
48 |
49 | Please refer to the [doc](../../tests/latency/) to learn more informations about test tools `server` and `client`.
50 |
51 | ## Test results
52 |
53 | ### Packet loss rate: 0.0% (LAN)
54 |
55 | * TCP
56 |
57 | ```console
58 | $ ./client --server-host 192.168.26.131
59 | First packet latency: 0.7572174072265625 ms
60 | Total latency: 499.89843368530273 ms
61 | ```
62 |
63 | * quic-tun
64 |
65 | ```console
66 | $ ./client --server-host 127.0.0.1 --server-port 6500
67 | First packet latency: 7.899284362792969 ms
68 | Total latency: 591.1564826965332 ms
69 | ```
70 |
71 | ### Packet loss rate: 1.0% (LAN)
72 |
73 | * TCP
74 |
75 | ```console
76 | $ ./client --server-host 192.168.26.131
77 | First packet latency: 0.6091594696044922 ms
78 | Total latency: 4290.04430770874 ms
79 | ```
80 |
81 | * quic-tun
82 |
83 | ```console
84 | $ ./client --server-host 127.0.0.1 --server-port 6500
85 | First packet latency: 8.286714553833008 ms
86 | Total latency: 1201.0939121246338 ms
87 | ```
88 |
89 | ### Packet loss rate: 0.0% (WAN)
90 |
91 | * TCP
92 |
93 | ```console
94 | $ ./client --server-host 47.111.149.1 --server-port 5201
95 | First packet latency: 24.95884895324707 ms
96 | Total latency: 25493.980407714844 ms
97 | ```
98 |
99 | * quic-tun
100 |
101 | ```console
102 | $ ./client --server-host 127.0.0.1 --server-port 6500
103 | First packet latency: 100.67296028137207 ms
104 | Total latency: 24987.539291381836 ms
105 | ```
106 |
107 | ### Packet loss rate: 1.0% (WAN)
108 |
109 | * TCP
110 |
111 | ```console
112 | $ ./client --server-host 47.111.149.1 --server-port 5201
113 | First packet latency: 23.789167404174805 ms
114 | Total latency: 28489.194869995117 ms
115 | ```
116 |
117 | * quic-tun
118 |
119 | ```console
120 | $ ./client --server-host 127.0.0.1 --server-port 6500
121 | First packet latency: 103.04689407348633 ms
122 | Total latency: 27247.18403816223 ms
123 | ```
124 |
125 | ### Packet loss rate: 2.0% (WAN)
126 |
127 | * TCP
128 |
129 | ```console
130 | $ ./client --server-host 47.111.149.1 --server-port 5201
131 | First packet latency: 36.420583724975586 ms
132 | Total latency: 33720.38745880127 ms
133 | ```
134 |
135 | * quic-tun
136 |
137 | ```console
138 | $ ./client --server-host 127.0.0.1 --server-port 6500
139 | First packet latency: 100.87323188781738 ms
140 | Total latency: 27528.08117866516 ms
141 | ```
142 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/kungze/quic-tun
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/spf13/cobra v1.4.0
7 | github.com/spf13/pflag v1.0.5
8 | )
9 |
10 | require (
11 | github.com/google/uuid v1.3.0
12 | github.com/lucas-clemente/quic-go v0.26.0
13 | github.com/spf13/viper v1.12.0
14 | go.uber.org/zap v1.17.0
15 | )
16 |
17 | require (
18 | github.com/fatih/color v1.13.0 // indirect
19 | github.com/mattn/go-colorable v0.1.12 // indirect
20 | github.com/mattn/go-isatty v0.0.14 // indirect
21 | github.com/mattn/go-runewidth v0.0.13 // indirect
22 | github.com/rivo/uniseg v0.2.0 // indirect
23 | go.uber.org/atomic v1.7.0 // indirect
24 | go.uber.org/multierr v1.6.0 // indirect
25 | )
26 |
27 | require (
28 | github.com/cheekybits/genny v1.0.0 // indirect
29 | github.com/fsnotify/fsnotify v1.5.4 // indirect
30 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
31 | github.com/gosuri/uitable v0.0.4
32 | github.com/hashicorp/hcl v1.0.0 // indirect
33 | github.com/inconshreveable/mousetrap v1.0.0 // indirect
34 | github.com/magiconair/properties v1.8.6 // indirect
35 | github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
36 | github.com/marten-seemann/qtls-go1-17 v0.1.1 // indirect
37 | github.com/marten-seemann/qtls-go1-18 v0.1.1 // indirect
38 | github.com/mitchellh/mapstructure v1.5.0 // indirect
39 | github.com/nxadm/tail v1.4.8 // indirect
40 | github.com/onsi/ginkgo v1.16.4 // indirect
41 | github.com/pelletier/go-toml v1.9.5 // indirect
42 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect
43 | github.com/spf13/afero v1.8.2 // indirect
44 | github.com/spf13/cast v1.5.0 // indirect
45 | github.com/spf13/jwalterweatherman v1.1.0 // indirect
46 | github.com/subosito/gotenv v1.3.0 // indirect
47 | golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
48 | golang.org/x/mod v0.4.2 // indirect
49 | golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect
50 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
51 | golang.org/x/text v0.3.7 // indirect
52 | golang.org/x/tools v0.1.1 // indirect
53 | golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
54 | gopkg.in/ini.v1 v1.66.4 // indirect
55 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
56 | gopkg.in/yaml.v2 v2.4.0 // indirect
57 | gopkg.in/yaml.v3 v3.0.0 // indirect
58 | )
59 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
4 | cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
5 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
6 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
7 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
8 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
9 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
10 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
11 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
12 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
13 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
14 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
15 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
16 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
17 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
18 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
19 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
20 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
21 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
22 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
23 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
24 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
25 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
26 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
27 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
28 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
29 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
30 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
31 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
32 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
33 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
34 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
35 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
36 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
37 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
38 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
39 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
40 | dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
41 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
42 | dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
43 | dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
44 | dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
45 | git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
46 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
47 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
48 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
49 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
50 | github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
51 | github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
52 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
53 | github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
54 | github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
55 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
56 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
57 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
58 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
59 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
60 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
61 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
62 | github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
63 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
64 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
65 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
66 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
67 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
68 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
69 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
70 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
71 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
72 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
73 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
74 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
75 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
76 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
77 | github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
78 | github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
79 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
80 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
81 | github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
82 | github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
83 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
84 | github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
85 | github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
86 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
87 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
88 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
89 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
90 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
91 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
92 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
93 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
94 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
95 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
96 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
97 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
98 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
99 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
100 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
101 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
102 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
103 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
104 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
105 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
106 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
107 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
108 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
109 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
110 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
111 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
112 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
113 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
114 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
115 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
116 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
117 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
118 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
119 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
120 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
121 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
122 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
123 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
124 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
125 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
126 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
127 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
128 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
129 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
130 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
131 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
132 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
133 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
134 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
135 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
136 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
137 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
138 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
139 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
140 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
141 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
142 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
143 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
144 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
145 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
146 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
147 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
148 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
149 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
150 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
151 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
152 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
153 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
154 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
155 | github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
156 | github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
157 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
158 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
159 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
160 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
161 | github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
162 | github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
163 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
164 | github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
165 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
166 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
167 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
168 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
169 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
170 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
171 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
172 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
173 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
174 | github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
175 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
176 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
177 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
178 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
179 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
180 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
181 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
182 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
183 | github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
184 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
185 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
186 | github.com/lucas-clemente/quic-go v0.26.0 h1:ALBQXr9UJ8A1LyzvceX4jd9QFsHvlI0RR6BkV16o00A=
187 | github.com/lucas-clemente/quic-go v0.26.0/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI=
188 | github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
189 | github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
190 | github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
191 | github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
192 | github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
193 | github.com/marten-seemann/qtls-go1-16 v0.1.5 h1:o9JrYPPco/Nukd/HpOHMHZoBDXQqoNtUCmny98/1uqQ=
194 | github.com/marten-seemann/qtls-go1-16 v0.1.5/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
195 | github.com/marten-seemann/qtls-go1-17 v0.1.1 h1:DQjHPq+aOzUeh9/lixAGunn6rIOQyWChPSI4+hgW7jc=
196 | github.com/marten-seemann/qtls-go1-17 v0.1.1/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s=
197 | github.com/marten-seemann/qtls-go1-18 v0.1.1 h1:qp7p7XXUFL7fpBvSS1sWD+uSqPvzNQK43DH+/qEkj0Y=
198 | github.com/marten-seemann/qtls-go1-18 v0.1.1/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
199 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
200 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
201 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
202 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
203 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
204 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
205 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
206 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
207 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
208 | github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
209 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
210 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
211 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
212 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
213 | github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
214 | github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
215 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
216 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
217 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
218 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
219 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
220 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
221 | github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
222 | github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
223 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
224 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
225 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
226 | github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak=
227 | github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY=
228 | github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
229 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
230 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
231 | github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
232 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
233 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
234 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
235 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
236 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
237 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
238 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
239 | github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
240 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
241 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
242 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
243 | github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
244 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
245 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
246 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
247 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
248 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
249 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
250 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
251 | github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
252 | github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
253 | github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
254 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
255 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
256 | github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
257 | github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
258 | github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
259 | github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
260 | github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
261 | github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
262 | github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
263 | github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
264 | github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
265 | github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
266 | github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
267 | github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
268 | github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
269 | github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
270 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
271 | github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
272 | github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
273 | github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
274 | github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
275 | github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
276 | github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
277 | github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
278 | github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
279 | github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
280 | github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
281 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
282 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
283 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
284 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
285 | github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
286 | github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
287 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
288 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
289 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
290 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
291 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
292 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
293 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
294 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
295 | github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI=
296 | github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs=
297 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
298 | github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
299 | github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
300 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
301 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
302 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
303 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
304 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
305 | go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
306 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
307 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
308 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
309 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
310 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
311 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
312 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
313 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
314 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
315 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
316 | go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=
317 | go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
318 | go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
319 | golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
320 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
321 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
322 | golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
323 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
324 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
325 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
326 | golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
327 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
328 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
329 | golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
330 | golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
331 | golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
332 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
333 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
334 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
335 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
336 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
337 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
338 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
339 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
340 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
341 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
342 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
343 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
344 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
345 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
346 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
347 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
348 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
349 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
350 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
351 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
352 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
353 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
354 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
355 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
356 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
357 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
358 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
359 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
360 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
361 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
362 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
363 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
364 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
365 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
366 | golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
367 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
368 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
369 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
370 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
371 | golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
372 | golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
373 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
374 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
375 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
376 | golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
377 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
378 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
379 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
380 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
381 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
382 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
383 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
384 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
385 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
386 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
387 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
388 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
389 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
390 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
391 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
392 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
393 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
394 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
395 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
396 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
397 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
398 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
399 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
400 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
401 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
402 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
403 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
404 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
405 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
406 | golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y=
407 | golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
408 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
409 | golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
410 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
411 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
412 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
413 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
414 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
415 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
416 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
417 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
418 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
419 | golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
420 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
421 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
422 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
423 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
424 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
425 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
426 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
427 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
428 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
429 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
430 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
431 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
432 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
433 | golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
434 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
435 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
436 | golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
437 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
438 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
439 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
440 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
441 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
442 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
443 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
444 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
445 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
446 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
447 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
448 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
449 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
450 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
451 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
452 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
453 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
454 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
455 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
456 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
457 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
458 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
459 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
460 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
461 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
462 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
463 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
464 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
465 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
466 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
467 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
468 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
469 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
470 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
471 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
472 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
473 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
474 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
475 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
476 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
477 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
478 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
479 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
480 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
481 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
482 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
483 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
484 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
485 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
486 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
487 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
488 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
489 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
490 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
491 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
492 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
493 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
494 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
495 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
496 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
497 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
498 | golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
499 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
500 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
501 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
502 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
503 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
504 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
505 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
506 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
507 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
508 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
509 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
510 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
511 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
512 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
513 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
514 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
515 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
516 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
517 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
518 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
519 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
520 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
521 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
522 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
523 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
524 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
525 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
526 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
527 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
528 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
529 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
530 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
531 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
532 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
533 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
534 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
535 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
536 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
537 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
538 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
539 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
540 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
541 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
542 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
543 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
544 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
545 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
546 | golang.org/x/tools v0.1.1 h1:wGiQel/hW0NnEkJUk8lbzkX2gFJU6PFxf1v5OlCfuOs=
547 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
548 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
549 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
550 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
551 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
552 | golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618=
553 | golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
554 | google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
555 | google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
556 | google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
557 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
558 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
559 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
560 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
561 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
562 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
563 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
564 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
565 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
566 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
567 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
568 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
569 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
570 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
571 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
572 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
573 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
574 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
575 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
576 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
577 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
578 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
579 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
580 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
581 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
582 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
583 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
584 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
585 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
586 | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
587 | google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
588 | google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
589 | google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
590 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
591 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
592 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
593 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
594 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
595 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
596 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
597 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
598 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
599 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
600 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
601 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
602 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
603 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
604 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
605 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
606 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
607 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
608 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
609 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
610 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
611 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
612 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
613 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
614 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
615 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
616 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
617 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
618 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
619 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
620 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
621 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
622 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
623 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
624 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
625 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
626 | google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
627 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
628 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
629 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
630 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
631 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
632 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
633 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
634 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
635 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
636 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
637 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
638 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
639 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
640 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
641 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
642 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
643 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
644 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
645 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
646 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
647 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
648 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
649 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
650 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
651 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
652 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
653 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
654 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
655 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
656 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
657 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
658 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
659 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
660 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
661 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
662 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
663 | gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
664 | gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
665 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
666 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
667 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
668 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
669 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
670 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
671 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
672 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
673 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
674 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
675 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
676 | gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
677 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
678 | grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
679 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
680 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
681 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
682 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
683 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
684 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
685 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
686 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
687 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
688 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
689 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
690 | sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
691 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
692 |
--------------------------------------------------------------------------------
/pkg/classifier/headercache.go:
--------------------------------------------------------------------------------
1 | package classifier
2 |
3 | const (
4 | // The max lenght of the traffic header data which the quic-tun will cache them and use them to classify traffic
5 | HeaderLength = 1024
6 | )
7 |
8 | type HeaderCache struct {
9 | Header []byte
10 | }
11 |
12 | func (h *HeaderCache) Write(b []byte) (int, error) {
13 | if len(h.Header) < HeaderLength {
14 | h.Header = append(h.Header, b...)
15 | return len(b), nil
16 | } else {
17 | return 0, nil
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/classifier/interface.go:
--------------------------------------------------------------------------------
1 | package classifier
2 |
3 | import "context"
4 |
5 | // The all possible results of DiscriminatorPlugin's AnalyzeHeader
6 | const (
7 | // The discriminator confirmed the protocol of the tunnel's traffic and extra all properties of the protocol.
8 | AFFIRM = 0x00
9 | // The discriminator can't confirm the protocol of the tunnel's traffic, means need more data.
10 | UNCERTAINTY = 0x01
11 | // The procotol already confirmed, but need more data to fill properties
12 | INCOMPLETE = 0x02
13 | // The discriminator confirmed the protocol of the tunnel's traffic isn't the protocol of the discriminator
14 | DENY = 0x03
15 | )
16 |
17 | type DiscriminatorPlugin interface {
18 | // Analy the header data and make a determination whether or not the protocol
19 | // of the traffic is protocol corresponding to the discriminator. client is the
20 | // header which from client application, server is the header data whih from server
21 | // application.
22 | AnalyzeHeader(ctx context.Context, client *[]byte, server *[]byte) (result int)
23 | // If the protocol is hit, discriminator will try to extra properties
24 | // related the protocol from the header.
25 | GetProperties(ctx context.Context) (properties any)
26 | }
27 |
28 | // TODO(jeffyjf) Now, we just provide a spice discriminator, so we return it directly.
29 | // If we going to add more discriminators in future, the method should be refactor in advance.
30 | func LoadDiscriminators() map[string]DiscriminatorPlugin {
31 | return map[string]DiscriminatorPlugin{"spice": &spiceDiscriminator{}}
32 | }
33 |
--------------------------------------------------------------------------------
/pkg/classifier/spice.go:
--------------------------------------------------------------------------------
1 | package classifier
2 |
3 | import (
4 | "context"
5 | "encoding/binary"
6 | "fmt"
7 | "time"
8 |
9 | "github.com/google/uuid"
10 | "github.com/kungze/quic-tun/pkg/log"
11 | )
12 |
13 | const (
14 | SPICE_MAGIC = "REDQ"
15 | MAJOR_VERSION_INDEX = 4
16 | MINOR_VERSION_INDEX = 8
17 | CHANNEL_TYPE_INDEX = 20
18 | CHANNEL_MAIN = 1
19 | CHANNEL_DISPLAY = 2
20 | CHANNEL_INPUTS = 3
21 | CHANNEL_CURSOR = 4
22 | CHANNEL_PLAYBACK = 5
23 | CHANNEL_RECORD = 6
24 | CHANNEL_TUNNEL = 7
25 | CHANNEL_SMARTCARD = 8
26 | CHANNEL_USBREDIR = 9
27 | CHANNEL_PORT = 10
28 | CHANNEL_WEBDAV = 11
29 | )
30 |
31 | const (
32 | INITIAL_OFFSET = 12 // The index of the server link message's message size
33 | SPICE_LINK_ERR_OK = 0
34 | MESSAGE_TYPE_INIT = 103
35 | MESSAGE_TYPE_SERVER_NAME = 113
36 | MESSAGE_TYPE_SERVER_UUID = 114
37 | MESSAGE_SIZE_LENGTH = 4
38 | MESSAGE_TYPE_LENGTH = 2
39 | LINK_STATUS_LENGTH = 4
40 | SESSION_ID_LENGTH = 4
41 | SERVER_NAME_LENGTH_LENGTH = 4
42 | )
43 |
44 | type spiceProperties struct {
45 | Version string `json:"version"`
46 | SessionId string `json:"sessionId"`
47 | ChannelType string `json:"channelType"`
48 | ServerName string `json:"serverName,omitempty"`
49 | ServerUUID string `json:"serverUUID,omitempty"`
50 | }
51 |
52 | type spiceDiscriminator struct {
53 | properties spiceProperties
54 | }
55 |
56 | func (s *spiceDiscriminator) analyzeServerHeader(logger log.Logger, server *[]byte) int {
57 | logger.Info("Alalyze server application header data.")
58 | // Get the first paket message size
59 | var offset int = INITIAL_OFFSET + MESSAGE_SIZE_LENGTH
60 | if len(*server) < offset {
61 | return INCOMPLETE
62 | }
63 | messageSize := binary.LittleEndian.Uint32((*server)[offset-MESSAGE_SIZE_LENGTH : offset])
64 | // We cannot extract any property from the first packet, so skip it. Set the offset to link status
65 | offset = offset + int(messageSize) + LINK_STATUS_LENGTH
66 | if len(*server) < offset {
67 | return INCOMPLETE
68 | }
69 | linkStatus := binary.LittleEndian.Uint32((*server)[offset-LINK_STATUS_LENGTH : offset])
70 | if linkStatus != SPICE_LINK_ERR_OK {
71 | logger.Info("The link status of main channel isn't 'OK'")
72 | return AFFIRM
73 | }
74 | // From different type message extract different properties.
75 | var messageTypeMap map[string]int = map[string]int{
76 | "init": MESSAGE_TYPE_INIT,
77 | "serverName": MESSAGE_TYPE_SERVER_NAME,
78 | "serverUUID": MESSAGE_TYPE_SERVER_UUID,
79 | }
80 |
81 | // Loop process the subsequent data, until we get all informations or encountered unknown packet.
82 | // Set a timer in order to avoid potential endless loop
83 | timer := time.NewTimer(10 * time.Second)
84 | for {
85 | select {
86 | case <-timer.C:
87 | logger.Errorw("The timer is timeout during spice discriminator analyze server application data for main channel.", "error", "Timeout")
88 | return AFFIRM
89 | default:
90 | offset = offset + MESSAGE_TYPE_LENGTH
91 | if len(*server) < offset {
92 | return INCOMPLETE
93 | }
94 | messageType := binary.LittleEndian.Uint16((*server)[offset-MESSAGE_TYPE_LENGTH : offset])
95 | switch messageType {
96 | case MESSAGE_TYPE_INIT:
97 | offset = offset + MESSAGE_SIZE_LENGTH
98 | if len(*server) < offset {
99 | return INCOMPLETE
100 | }
101 | messageSize := binary.LittleEndian.Uint32((*server)[offset-MESSAGE_SIZE_LENGTH : offset])
102 | offset = offset + int(messageSize)
103 | if len(*server) < offset {
104 | return INCOMPLETE
105 | }
106 | sessionIndex := offset - int(messageSize)
107 | s.properties.SessionId = fmt.Sprintf("%x", (*server)[sessionIndex:sessionIndex+SESSION_ID_LENGTH])
108 | delete(messageTypeMap, "init")
109 | case MESSAGE_TYPE_SERVER_NAME:
110 | offset = offset + MESSAGE_SIZE_LENGTH
111 | if len(*server) < offset {
112 | return INCOMPLETE
113 | }
114 | messageSize := binary.LittleEndian.Uint32((*server)[offset-MESSAGE_SIZE_LENGTH : offset])
115 | offset = offset + int(messageSize)
116 | if len(*server) < offset {
117 | return INCOMPLETE
118 | }
119 | nameLenIndex := offset - int(messageSize)
120 | nameLen := binary.LittleEndian.Uint32((*server)[nameLenIndex : nameLenIndex+SERVER_NAME_LENGTH_LENGTH])
121 | nameIndex := nameLenIndex + SERVER_NAME_LENGTH_LENGTH
122 | s.properties.ServerName = string((*server)[nameIndex : nameIndex+int(nameLen)-1])
123 | delete(messageTypeMap, "serverName")
124 | case MESSAGE_TYPE_SERVER_UUID:
125 | offset = offset + MESSAGE_SIZE_LENGTH
126 | if len(*server) < offset {
127 | return INCOMPLETE
128 | }
129 | messageSize := binary.LittleEndian.Uint32((*server)[offset-MESSAGE_SIZE_LENGTH : offset])
130 | offset = offset + int(messageSize)
131 | if len(*server) < offset {
132 | return INCOMPLETE
133 | }
134 | uuid, err := uuid.FromBytes((*server)[offset-int(messageSize) : offset])
135 | if err != nil {
136 | return AFFIRM
137 | }
138 | s.properties.ServerUUID = uuid.String()
139 | delete(messageTypeMap, "serverUUID")
140 | default:
141 | logger.Errorw("Encounter unkunown packet.", "error", "unknown")
142 | return AFFIRM
143 | }
144 | if len(messageTypeMap) == 0 {
145 | return AFFIRM
146 | }
147 | }
148 | }
149 | }
150 |
151 | // Refer docs: https://www.spice-space.org/spice-protocol.html
152 | func (s *spiceDiscriminator) AnalyzeHeader(ctx context.Context, client *[]byte, server *[]byte) int {
153 | if len(*client) < 21 {
154 | return UNCERTAINTY
155 | }
156 | logger := log.FromContext(ctx)
157 | if string((*client)[:4]) != SPICE_MAGIC {
158 | return DENY
159 | }
160 | logger.Info("The protocol of the traffic that pass through the tunnel is spice.")
161 | // This means the properties haven't be instantiated (the first time that get enough
162 | // header data to analyzed the traffic's protocol is spice)
163 | if s.properties.ChannelType == "" {
164 | s.properties = spiceProperties{
165 | Version: fmt.Sprintf("%x.%x", (*client)[MAJOR_VERSION_INDEX], (*client)[MINOR_VERSION_INDEX]),
166 | SessionId: fmt.Sprintf("%x", (*client)[16:20]),
167 | }
168 | // If the properties already instantiated, and the channel type is
169 | // main, we to analy the server header data directly.
170 | } else if s.properties.ChannelType == "main" {
171 | return s.analyzeServerHeader(logger, server)
172 | } else {
173 | return AFFIRM
174 | }
175 | switch (*client)[CHANNEL_TYPE_INDEX] {
176 | case CHANNEL_MAIN:
177 | s.properties.ChannelType = "main"
178 | // For main channel, we need analy server header data to extra more properties.
179 | logger.Info(fmt.Sprintf("The spice traffic's channel type is %s", s.properties.ChannelType))
180 | return s.analyzeServerHeader(logger, server)
181 | case CHANNEL_DISPLAY:
182 | s.properties.ChannelType = "display"
183 | case CHANNEL_INPUTS:
184 | s.properties.ChannelType = "inputs"
185 | case CHANNEL_CURSOR:
186 | s.properties.ChannelType = "cursor"
187 | case CHANNEL_PLAYBACK:
188 | s.properties.ChannelType = "playback"
189 | case CHANNEL_RECORD:
190 | s.properties.ChannelType = "record"
191 | case CHANNEL_TUNNEL:
192 | s.properties.ChannelType = "tunnel"
193 | case CHANNEL_SMARTCARD:
194 | s.properties.ChannelType = "smartcard"
195 | case CHANNEL_USBREDIR:
196 | s.properties.ChannelType = "usbredir"
197 | case CHANNEL_PORT:
198 | s.properties.ChannelType = "port"
199 | case CHANNEL_WEBDAV:
200 | s.properties.ChannelType = "webdev"
201 | default:
202 | s.properties.ChannelType = "unknow"
203 | }
204 | logger.Info(fmt.Sprintf("The spice traffic's channel type is %s", s.properties.ChannelType))
205 | return AFFIRM
206 | }
207 |
208 | func (s *spiceDiscriminator) GetProperties(ctx context.Context) any {
209 | return s.properties
210 | }
211 |
--------------------------------------------------------------------------------
/pkg/constants/constants.go:
--------------------------------------------------------------------------------
1 | package constants
2 |
3 | // context suggest that should not use built-in type key for value; define your own type to avoid collisions
4 | type keytype string
5 |
6 | const (
7 | // The length of token that client endpoint send to server endpoint
8 | TokenLength = 512
9 | // The lenght of ack message that server endpoint send to client endpoint
10 | AckMsgLength = 1
11 | )
12 |
13 | const (
14 | // Means that server endpoint accept the token which receive from client endpoint
15 | HandshakeSuccess = 0x01
16 | // Means that server endpoint cannot parse token
17 | ParseTokenError = 0x02
18 | // Means that server endpoint cannot connect server application
19 | CannotConnServer = 0x03
20 | )
21 |
22 | // The key names of log's additional key/value pairs
23 | const (
24 | ClientAppAddr = "Client-App-Addr"
25 | StreamID = "Stream-ID"
26 | ServerAppAddr = "Server-App-Addr"
27 | ClientEndpointAddr = "Client-Endpoint-Addr"
28 | )
29 |
30 | // The key names of value context
31 | const (
32 | CtxRemoteEndpointAddr keytype = "Remote-Endpoint-Addr"
33 | CtxClientAppAddr keytype = "Client-App-Addr"
34 | )
35 |
36 | const (
37 | ClientEndpoint = "client"
38 | ServerEndpoint = "server"
39 | )
40 |
--------------------------------------------------------------------------------
/pkg/log/log.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "context"
5 | "sync"
6 |
7 | "go.uber.org/zap"
8 | "go.uber.org/zap/zapcore"
9 | )
10 |
11 | // Field is an alias for the field structure in the zap log frame.
12 | type Field = zapcore.Field
13 |
14 | // Logger defines the capabilities that a logger has.
15 | type Logger interface {
16 | // Output info log
17 | Info(msg string, fields ...Field)
18 | Infof(fromat string, v ...any)
19 | Infow(msg string, keysAndValues ...any)
20 | // Output debug log
21 | Debug(msg string, fields ...Field)
22 | Debugf(fromat string, v ...any)
23 | Debugw(msg string, keysAndValues ...any)
24 | // Output warning log
25 | Warn(msg string, fields ...Field)
26 | Warnf(format string, v ...any)
27 | Warnw(msg string, keysAndValues ...any)
28 | // Output error log
29 | Error(msg string, fields ...Field)
30 | Errorf(format string, v ...any)
31 | Errorw(msg string, keysAndValues ...any)
32 | // Output panic log
33 | Panic(msg string, fields ...Field)
34 | Panicf(format string, v ...any)
35 | Panicw(msg string, keysAndValues ...any)
36 | // Output fatal log
37 | Fatal(msg string, fields ...Field)
38 | Fatalf(format string, v ...any)
39 | Fatalw(msg string, keysAndValues ...any)
40 |
41 | // Fulsh calls the underlying ZAP Core's Sync method, flushing any buffered log
42 | // entries. Applications should take care to call Sync before exiting.
43 | Flush()
44 | // WithValues adds some key-value pairs of context to a logger.
45 | WithValues(keysAndValues ...any) Logger
46 | // WithName adds a new element to the logger's name.
47 | // Successive calls with WithName continue to append
48 | // suffixes to the logger's name.
49 | WithName(name string) Logger
50 | // WithContext returns a copy of context in which the log value is set.
51 | WithContext(ctx context.Context) context.Context
52 | }
53 |
54 | type logger struct {
55 | zlogger *zap.Logger
56 | }
57 |
58 | var (
59 | std = New(NewOptions()) // Define logs that can be used directly
60 | mu sync.Mutex
61 | )
62 |
63 | // Init initializes logger with specified options.
64 | func Init(opts *Options) {
65 | mu.Lock()
66 | defer mu.Unlock()
67 | std = New(opts)
68 | }
69 |
70 | // New create logger by opts which can custmoized by command arguments and config file.
71 | func New(opts *Options) *logger {
72 | var zapLevel zapcore.Level
73 | if err := zapLevel.UnmarshalText([]byte(opts.Level)); err != nil {
74 | zapLevel = zapcore.InfoLevel
75 | }
76 |
77 | zc := zap.Config{
78 | Level: zap.NewAtomicLevelAt(zapLevel),
79 | Development: opts.Development,
80 | Sampling: &zap.SamplingConfig{
81 | Initial: 100,
82 | Thereafter: 100,
83 | },
84 | Encoding: opts.Format,
85 | DisableCaller: opts.DisableCaller,
86 | DisableStacktrace: opts.DisableStacktrace,
87 | EncoderConfig: zapcore.EncoderConfig{
88 | TimeKey: "ts",
89 | LevelKey: "level",
90 | NameKey: "logger",
91 | CallerKey: "caller",
92 | MessageKey: "msg",
93 | StacktraceKey: "stacktrace",
94 | LineEnding: zapcore.DefaultLineEnding,
95 | EncodeLevel: zapcore.CapitalLevelEncoder,
96 | EncodeTime: zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05.000"),
97 | EncodeDuration: zapcore.MillisDurationEncoder,
98 | EncodeCaller: zapcore.ShortCallerEncoder,
99 | },
100 | OutputPaths: opts.OutputPaths,
101 | ErrorOutputPaths: opts.ErrorOutputPaths,
102 | }
103 | l, err := zc.Build(zap.AddStacktrace(zapcore.PanicLevel), zap.AddCallerSkip(1))
104 | if err != nil {
105 | panic(err)
106 | }
107 | return &logger{zlogger: l.Named(opts.Name)}
108 | }
109 |
110 | func (l *logger) Flush() {
111 | _ = l.zlogger.Sync()
112 | }
113 |
114 | func Flush() { std.Flush() }
115 |
116 | // handleFields converts a bunch of arbitrary key-value pairs into Zap fields.
117 | func handleFields(l *zap.Logger, args []any) []zap.Field {
118 | if len(args) == 0 {
119 | return nil
120 | }
121 |
122 | fields := make([]zap.Field, 0, len(args)/2)
123 | for i := 0; i < len(args); {
124 | // check just in case for strongly-typed Zap fields, which is illegal (since
125 | // it breaks implementation agnosticism), so we can give a better error message.
126 | if _, ok := args[i].(zap.Field); ok {
127 | l.DPanic("strongly-typed Zap Field passed to logr", zap.Any("zap field", args[i]))
128 |
129 | break
130 | }
131 |
132 | // process a key-value pair, ensuring that the key is a string
133 | key, val := args[i], args[i+1]
134 | keyStr, isString := key.(string)
135 | if !isString {
136 | // if the key isn't a string, DPanic and stop logging
137 | l.DPanic(
138 | "non-string key argument passed to logging, ignoring all later arguments",
139 | zap.Any("invalid key", key),
140 | )
141 |
142 | break
143 | }
144 |
145 | fields = append(fields, zap.Any(keyStr, val))
146 | i += 2
147 | }
148 |
149 | return fields
150 | }
151 |
152 | func WithValues(keysAndValues ...any) Logger { return std.WithValues(keysAndValues...) }
153 |
154 | func (l *logger) WithValues(keysAndValues ...any) Logger {
155 | newLogger := l.zlogger.With(handleFields(l.zlogger, keysAndValues)...)
156 | return NewLogger(newLogger)
157 | }
158 |
159 | func WithName(name string) Logger { return std.WithName(name) }
160 |
161 | func (l *logger) WithName(name string) Logger {
162 | newLogger := l.zlogger.Named(name)
163 | return NewLogger(newLogger)
164 | }
165 |
166 | // NewLogger returns a new Logger.
167 | func NewLogger(l *zap.Logger) Logger {
168 | return &logger{
169 | zlogger: l,
170 | }
171 | }
172 |
173 | func WithContext(ctx context.Context) context.Context {
174 | return std.WithContext(ctx)
175 | }
176 |
177 | // contextKey is how we find Loggers in a context.Context.
178 | type contextKey struct{}
179 |
180 | func (l *logger) WithContext(ctx context.Context) context.Context {
181 | return context.WithValue(ctx, contextKey{}, l)
182 | }
183 |
184 | // FromContext returns the value of the log key on the ctx.
185 | func FromContext(ctx context.Context) Logger {
186 | if ctx != nil {
187 | if logger, ok := ctx.Value(contextKey{}).(Logger); ok {
188 | return logger
189 | }
190 | }
191 |
192 | return WithName("Unknown-Context")
193 | }
194 |
195 | func (l *logger) Info(msg string, fields ...Field) {
196 | l.zlogger.Info(msg, fields...)
197 | }
198 |
199 | func (l *logger) Infof(format string, v ...any) {
200 | l.zlogger.Sugar().Infof(format, v)
201 | }
202 |
203 | func (l *logger) Infow(msg string, keysAndValues ...any) {
204 | l.zlogger.Sugar().Infow(msg, keysAndValues...)
205 | }
206 |
207 | func (l *logger) Debug(msg string, fields ...Field) {
208 | l.zlogger.Debug(msg, fields...)
209 | }
210 |
211 | func (l *logger) Debugf(format string, v ...any) {
212 | l.zlogger.Sugar().Debugf(format, v)
213 | }
214 |
215 | func (l *logger) Debugw(msg string, keysAndValues ...any) {
216 | l.zlogger.Sugar().Debugw(msg, keysAndValues...)
217 | }
218 |
219 | func (l *logger) Warn(msg string, fields ...Field) {
220 | l.zlogger.Warn(msg, fields...)
221 | }
222 |
223 | func (l *logger) Warnf(format string, v ...any) {
224 | l.zlogger.Sugar().Warnf(format, v)
225 | }
226 |
227 | func (l *logger) Warnw(msg string, keysAndValues ...any) {
228 | l.zlogger.Sugar().Warnw(msg, keysAndValues...)
229 | }
230 |
231 | func (l *logger) Error(msg string, fields ...Field) {
232 | l.zlogger.Error(msg, fields...)
233 | }
234 |
235 | func (l *logger) Errorf(format string, v ...any) {
236 | l.zlogger.Sugar().Errorf(format, v)
237 | }
238 |
239 | func (l *logger) Errorw(msg string, keysAndValues ...any) {
240 | l.zlogger.Sugar().Errorw(msg, keysAndValues...)
241 | }
242 |
243 | func (l *logger) Panic(msg string, fields ...Field) {
244 | l.zlogger.Panic(msg, fields...)
245 | }
246 |
247 | func (l *logger) Panicf(format string, v ...any) {
248 | l.zlogger.Sugar().Panicf(format, v)
249 | }
250 |
251 | func (l *logger) Panicw(msg string, keysAndValues ...any) {
252 | l.zlogger.Sugar().Panicw(msg, keysAndValues...)
253 | }
254 |
255 | func (l *logger) Fatal(msg string, fields ...Field) {
256 | l.zlogger.Fatal(msg, fields...)
257 | }
258 |
259 | func (l *logger) Fatalf(format string, v ...any) {
260 | l.zlogger.Sugar().Fatalf(format, v)
261 | }
262 |
263 | func (l *logger) Fatalw(msg string, keysAndValues ...any) {
264 | l.zlogger.Sugar().Fatalw(msg, keysAndValues...)
265 | }
266 |
267 | func Info(msg string, fields ...Field) {
268 | std.zlogger.Info(msg, fields...)
269 | }
270 |
271 | func Infof(format string, v ...any) {
272 | std.zlogger.Sugar().Infof(format, v...)
273 | }
274 |
275 | func Infow(msg string, keysAndValues ...any) {
276 | std.zlogger.Sugar().Infow(msg, keysAndValues...)
277 | }
278 |
279 | func Debug(msg string, fields ...Field) {
280 | std.zlogger.Debug(msg, fields...)
281 | }
282 |
283 | func Debugf(format string, v ...any) {
284 | std.zlogger.Sugar().Debugf(format, v...)
285 | }
286 |
287 | func Debugw(msg string, keysAndValues ...any) {
288 | std.zlogger.Sugar().Debugw(msg, keysAndValues...)
289 | }
290 |
291 | func Warn(msg string, fields ...Field) {
292 | std.zlogger.Warn(msg, fields...)
293 | }
294 |
295 | func Warnf(format string, v ...any) {
296 | std.zlogger.Sugar().Warnf(format, v...)
297 | }
298 |
299 | func Warnw(msg string, keysAndValues ...any) {
300 | std.zlogger.Sugar().Warnw(msg, keysAndValues...)
301 | }
302 |
303 | func Error(msg string, fields ...Field) {
304 | std.zlogger.Error(msg, fields...)
305 | }
306 |
307 | func Errorf(format string, v ...any) {
308 | std.zlogger.Sugar().Errorf(format, v...)
309 | }
310 |
311 | func Errorw(msg string, keysAndValues ...any) {
312 | std.zlogger.Sugar().Errorw(msg, keysAndValues...)
313 | }
314 |
315 | func Panic(msg string, fields ...Field) {
316 | std.zlogger.Panic(msg, fields...)
317 | }
318 |
319 | func Panicf(format string, v ...any) {
320 | std.zlogger.Sugar().Panicf(format, v...)
321 | }
322 |
323 | func Panicw(msg string, keysAndValues ...any) {
324 | std.zlogger.Sugar().Panicw(msg, keysAndValues...)
325 | }
326 |
327 | func Fatal(msg string, fields ...Field) {
328 | std.zlogger.Fatal(msg, fields...)
329 | }
330 |
331 | func Fatalf(format string, v ...any) {
332 | std.zlogger.Sugar().Fatalf(format, v...)
333 | }
334 |
335 | func Fatalw(msg string, keysAndValues ...any) {
336 | std.zlogger.Sugar().Fatalw(msg, keysAndValues...)
337 | }
338 |
--------------------------------------------------------------------------------
/pkg/log/options.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "github.com/spf13/pflag"
5 | "go.uber.org/zap/zapcore"
6 | )
7 |
8 | // Options contains configuration items related to log.
9 | type Options struct {
10 | OutputPaths []string `json:"log-output-paths" mapstructure:"log-output-paths"`
11 | ErrorOutputPaths []string `json:"log-error-output-paths" mapstructure:"log-error-output-paths"`
12 | Level string `json:"log-level" mapstructure:"log-level"`
13 | Format string `json:"log-format" mapstructure:"log-format"`
14 | DisableCaller bool `json:"log-disable-caller" mapstructure:"log-disable-caller"`
15 | DisableStacktrace bool `json:"log-disable-stacktrace" mapstructure:"log-disable-stacktrace"`
16 | Development bool `json:"log-development" mapstructure:"log-development"`
17 | Name string `json:"log-name" mapstructure:"log-name"`
18 | }
19 |
20 | // NewOptions creates an Options object with default parameters.
21 | func NewOptions() *Options {
22 | return &Options{
23 | Level: zapcore.InfoLevel.String(),
24 | DisableCaller: false,
25 | DisableStacktrace: false,
26 | Format: "console",
27 | Development: false,
28 | OutputPaths: []string{"stdout"},
29 | ErrorOutputPaths: []string{"stderr"},
30 | }
31 | }
32 |
33 | // AddFlags adds flags for log to the specified FlagSet object.
34 | func (o *Options) AddFlags(fs *pflag.FlagSet) {
35 | fs.StringVar(&o.Level, "log-level", o.Level, "Minimum log output `LEVEL`.")
36 | fs.BoolVar(&o.DisableCaller, "log-disable-caller", o.DisableCaller, "Disable output of caller information in the log.")
37 | fs.BoolVar(&o.DisableStacktrace, "log-disable-stacktrace",
38 | o.DisableStacktrace, "Disable the log to record a stack trace for all messages at or above panic level.")
39 | fs.StringVar(&o.Format, "log-format", o.Format, "Log output `FORMAT`, support 'console' or 'json' format.")
40 | fs.StringSliceVar(&o.OutputPaths, "log-output-paths", o.OutputPaths, "Output paths of log.")
41 | fs.StringSliceVar(&o.ErrorOutputPaths, "log-error-output-paths", o.ErrorOutputPaths, "Error output paths of log.")
42 | fs.BoolVar(
43 | &o.Development,
44 | "log-development",
45 | o.Development,
46 | "Development puts the logger in development mode, which changes "+
47 | "the behavior of DPanicLevel and takes stacktraces more liberally.",
48 | )
49 | fs.StringVar(&o.Name, "log-name", o.Name, "The name of the logger.")
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/options/client.go:
--------------------------------------------------------------------------------
1 | package options
2 |
3 | import "github.com/spf13/pflag"
4 |
5 | //ClientOptions contains information for a client service.
6 | type ClientOptions struct {
7 | ListenOn string `json:"listen-on" mapstructure:"listen-on"`
8 | ServerEndpointSocket string `json:"server-endpoint" mapstructure:"server-endpoint"`
9 | TokenPlugin string `json:"token-source-plugin" mapstructure:"token-source-plugin"`
10 | TokenSource string `json:"token-source" mapstructure:"token-source"`
11 | }
12 |
13 | // GetDefaultClientOptions returns a client configuration with default values.
14 | func GetDefaultClientOptions() *ClientOptions {
15 | return &ClientOptions{
16 | ListenOn: "tcp:127.0.0.1:6500",
17 | ServerEndpointSocket: "",
18 | TokenPlugin: "Fixed",
19 | TokenSource: "",
20 | }
21 | }
22 |
23 | // AddFlags adds flags for a specific Server to the specified FlagSet.
24 | func (s *ClientOptions) AddFlags(fs *pflag.FlagSet) {
25 | fs.StringVar(&s.ListenOn, "listen-on", s.ListenOn,
26 | "The socket that the client side endpoint listen on")
27 | fs.StringVar(&s.ServerEndpointSocket, "server-endpoint", s.ServerEndpointSocket,
28 | "The server side endpoint address, example: example.com:6565")
29 | fs.StringVar(&s.TokenPlugin, "token-source-plugin", s.TokenPlugin,
30 | "Specify the token plugin. Token used to tell the server endpoint which server app we want to access. Support values: Fixed, File.")
31 | fs.StringVar(&s.TokenSource, "token-source", s.TokenSource,
32 | "An argument to be passed to the token source plugin on instantiation.")
33 | }
34 |
--------------------------------------------------------------------------------
/pkg/options/config.go:
--------------------------------------------------------------------------------
1 | package options
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "strings"
8 |
9 | "github.com/spf13/cobra"
10 | "github.com/spf13/pflag"
11 | "github.com/spf13/viper"
12 | )
13 |
14 | const configFlagName = "config"
15 |
16 | var cfgFile string
17 |
18 | func init() {
19 | pflag.StringVarP(&cfgFile, "config", "c", cfgFile, "Read configuration from specified `FILE`, "+
20 | "support JSON, TOML, YAML, HCL, or Java properties formats.")
21 | }
22 |
23 | // addConfigFlag adds flags for a specific server to the specified FlagSet
24 | // object.
25 | func AddConfigFlag(basename string, fs *pflag.FlagSet) {
26 | fs.AddFlag(pflag.Lookup(configFlagName))
27 |
28 | viper.AutomaticEnv()
29 | viper.SetEnvPrefix(strings.Replace(strings.ToUpper(basename), "-", "_", -1))
30 | viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
31 |
32 | cobra.OnInitialize(func() {
33 | if cfgFile != "" {
34 | viper.SetConfigFile(cfgFile)
35 | } else {
36 | viper.AddConfigPath(".")
37 |
38 | if names := strings.Split(basename, "-"); len(names) > 1 {
39 | viper.AddConfigPath(filepath.Join(HomeDir(), "."+names[0]))
40 | viper.AddConfigPath(filepath.Join("/etc", names[0]))
41 | }
42 |
43 | viper.SetConfigName(basename)
44 | }
45 |
46 | if err := viper.ReadInConfig(); err != nil {
47 | _, _ = fmt.Fprintf(os.Stdout, "WARN: failed to read configuration file(%s): %v\n", cfgFile, err)
48 | }
49 | })
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/options/restfulapi.go:
--------------------------------------------------------------------------------
1 | package options
2 |
3 | import "github.com/spf13/pflag"
4 |
5 | // RestfulAPIOptions contains the options while running a API server.
6 | type RestfulAPIOptions struct {
7 | HttpdListenOn string `json:"httpd-listen-on" mapstructure:"httpd-listen-on"`
8 | }
9 |
10 | func GetDefaultRestfulAPIOptions() *RestfulAPIOptions {
11 | return &RestfulAPIOptions{
12 | HttpdListenOn: "0.0.0.0:8086",
13 | }
14 | }
15 |
16 | // AddFlags adds flags for a specific Server to the specified FlagSet.
17 | func (r *RestfulAPIOptions) AddFlags(fs *pflag.FlagSet) {
18 | fs.StringVar(&r.HttpdListenOn, "httpd-listen-on", r.HttpdListenOn,
19 | "The socket of the API(httpd) server listen on")
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/options/secure.go:
--------------------------------------------------------------------------------
1 | package options
2 |
3 | import "github.com/spf13/pflag"
4 |
5 | // SecureOptions contains information for TLS.
6 | type SecureOptions struct {
7 | KeyFile string `json:"key-file" mapstructure:"key-file"`
8 | CertFile string `json:"cert-file" mapstructure:"cert-file"`
9 | CaFile string `json:"ca-file" mapstructure:"ca-file"`
10 | VerifyRemoteEndpoint bool `json:"verify-remote-endpoint" mapstructure:"verify-remote-endpoint"`
11 | }
12 |
13 | // GetDefaultSecureOptions returns a Secure configuration with default values.
14 | func GetDefaultSecureOptions() *SecureOptions {
15 | return &SecureOptions{
16 | KeyFile: "",
17 | CertFile: "",
18 | CaFile: "",
19 | VerifyRemoteEndpoint: false,
20 | }
21 | }
22 |
23 | // AddFlags adds flags for a specific Server to the specified FlagSet.
24 | func (s *SecureOptions) AddFlags(fs *pflag.FlagSet) {
25 | fs.StringVar(&s.KeyFile, "key-file", s.KeyFile,
26 | "The private key file path.")
27 | fs.StringVar(&s.CertFile, "cert-file", s.CertFile,
28 | "The certificate file path.")
29 | fs.StringVar(&s.CaFile, "ca-file", s.CaFile,
30 | "The certificate authority file path, used to verify remote endpoint certificate. "+
31 | "If not specified, quictun try to load system certificate.")
32 | fs.BoolVar(&s.VerifyRemoteEndpoint, "verify-remote-endpoint", s.VerifyRemoteEndpoint,
33 | "Whether to require remote endpoint certificate and verify it")
34 | }
35 |
--------------------------------------------------------------------------------
/pkg/options/server.go:
--------------------------------------------------------------------------------
1 | package options
2 |
3 | import "github.com/spf13/pflag"
4 |
5 | //ServerOptions contains information for a client service.
6 | type ServerOptions struct {
7 | ListenOn string `json:"listen-on" mapstructure:"listen-on"`
8 | TokenParserPlugin string `json:"token-parser-plugin" mapstructure:"token-parser-plugin"`
9 | TokenParserKey string `json:"token-parser-key" mapstructure:"token-parser-key"`
10 | }
11 |
12 | // GetDefaultServerOptions returns a server configuration with default values.
13 | func GetDefaultServerOptions() *ServerOptions {
14 | return &ServerOptions{
15 | ListenOn: "0.0.0.0:7500",
16 | TokenParserPlugin: "Cleartext",
17 | TokenParserKey: "",
18 | }
19 | }
20 |
21 | // AddFlags adds flags for a specific Server to the specified FlagSet.
22 | func (s *ServerOptions) AddFlags(fs *pflag.FlagSet) {
23 | fs.StringVar(&s.ListenOn, "listen-on", s.ListenOn,
24 | "The socket that the server side endpoint listen on")
25 | fs.StringVar(&s.TokenParserPlugin, "token-parser-plugin", s.TokenParserPlugin,
26 | "The token parser plugin.")
27 | fs.StringVar(&s.TokenParserKey, "token-parser-key", s.TokenParserKey,
28 | "An argument to be passed to the token parse plugin on instantiation.")
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/options/utils.go:
--------------------------------------------------------------------------------
1 | package options
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "runtime"
8 |
9 | "github.com/gosuri/uitable"
10 | "github.com/kungze/quic-tun/pkg/log"
11 | "github.com/spf13/pflag"
12 | "github.com/spf13/viper"
13 | )
14 |
15 | func PrintWorkingDir() {
16 | wd, _ := os.Getwd()
17 | log.Infof("WorkingDir: %s", wd)
18 | }
19 |
20 | // PrintFlags logs the flags in the flagset.
21 | func PrintFlags(flags *pflag.FlagSet) {
22 | flags.VisitAll(func(flag *pflag.Flag) {
23 | log.Infof("FLAG: --%s=%q", flag.Name, flag.Value)
24 | })
25 | }
26 |
27 | func PrintConfig() {
28 | if keys := viper.AllKeys(); len(keys) > 0 {
29 | fmt.Printf("%v Configuration items:\n", "==>")
30 | table := uitable.New()
31 | table.Separator = " "
32 | table.MaxColWidth = 80
33 | table.RightAlign(0)
34 | for _, k := range keys {
35 | table.AddRow(fmt.Sprintf("%s:", k), viper.Get(k))
36 | }
37 | fmt.Printf("%v", table)
38 | fmt.Println()
39 | }
40 | }
41 |
42 | // HomeDir returns the home directory for the current user.
43 | // On Windows:
44 | // 1. the first of %HOME%, %HOMEDRIVE%%HOMEPATH%, %USERPROFILE% containing a `.apimachinery\config` file is returned.
45 | // 2. if none of those locations contain a `.apimachinery\config` file, the first of
46 | // %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that exists and is writeable is returned.
47 | // 3. if none of those locations are writeable, the first of %HOME%, %USERPROFILE%,
48 | // %HOMEDRIVE%%HOMEPATH% that exists is returned.
49 | // 4. if none of those locations exists, the first of %HOME%, %USERPROFILE%,
50 | // %HOMEDRIVE%%HOMEPATH% that is set is returned.
51 | func HomeDir() string {
52 | if runtime.GOOS != "windows" {
53 | return os.Getenv("HOME")
54 | }
55 | home := os.Getenv("HOME")
56 | homeDriveHomePath := ""
57 | if homeDrive, homePath := os.Getenv("HOMEDRIVE"), os.Getenv("HOMEPATH"); len(homeDrive) > 0 && len(homePath) > 0 {
58 | homeDriveHomePath = homeDrive + homePath
59 | }
60 | userProfile := os.Getenv("USERPROFILE")
61 |
62 | // Return first of %HOME%, %HOMEDRIVE%/%HOMEPATH%, %USERPROFILE% that contains a `.apimachinery\config` file.
63 | // %HOMEDRIVE%/%HOMEPATH% is preferred over %USERPROFILE% for backwards-compatibility.
64 | for _, p := range []string{home, homeDriveHomePath, userProfile} {
65 | if len(p) == 0 {
66 | continue
67 | }
68 | if _, err := os.Stat(filepath.Join(p, ".apimachinery", "config")); err != nil {
69 | continue
70 | }
71 | return p
72 | }
73 |
74 | firstSetPath := ""
75 | firstExistingPath := ""
76 |
77 | // Prefer %USERPROFILE% over %HOMEDRIVE%/%HOMEPATH% for compatibility with other auth-writing tools
78 | for _, p := range []string{home, userProfile, homeDriveHomePath} {
79 | if len(p) == 0 {
80 | continue
81 | }
82 | if len(firstSetPath) == 0 {
83 | // remember the first path that is set
84 | firstSetPath = p
85 | }
86 | info, err := os.Stat(p)
87 | if err != nil {
88 | continue
89 | }
90 | if len(firstExistingPath) == 0 {
91 | // remember the first path that exists
92 | firstExistingPath = p
93 | }
94 | if info.IsDir() && info.Mode().Perm()&(1<<(uint(7))) != 0 {
95 | // return first path that is writeable
96 | return p
97 | }
98 | }
99 |
100 | // If none are writeable, return first location that exists
101 | if len(firstExistingPath) > 0 {
102 | return firstExistingPath
103 | }
104 |
105 | // If none exist, return first location that is set
106 | if len(firstSetPath) > 0 {
107 | return firstSetPath
108 | }
109 |
110 | // We've got nothing
111 | return ""
112 | }
113 |
--------------------------------------------------------------------------------
/pkg/restfulapi/api.go:
--------------------------------------------------------------------------------
1 | package restfulapi
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 |
7 | "github.com/kungze/quic-tun/pkg/log"
8 | "github.com/kungze/quic-tun/pkg/tunnel"
9 | )
10 |
11 | type errorResponse struct {
12 | Msg string `json:"message"`
13 | }
14 |
15 | type httpd struct {
16 | // The socket address of the API server listen on
17 | ListenAddr string
18 | }
19 |
20 | func (h *httpd) getAllStreams(w http.ResponseWriter, request *http.Request) {
21 | var resp_json []byte
22 | var err error
23 | if request.Method != http.MethodGet {
24 | w.WriteHeader(http.StatusMethodNotAllowed)
25 | resp_json, _ = json.Marshal(errorResponse{Msg: "Please use GET request method"})
26 | } else {
27 | allTuns := tunnel.DataStore.LoadAll()
28 | resp_json, err = json.Marshal(allTuns)
29 | if err != nil {
30 | w.WriteHeader(http.StatusInternalServerError)
31 | resp_json = []byte(err.Error())
32 | }
33 | }
34 | _, err = w.Write(resp_json)
35 | if err != nil {
36 | log.Errorw("Encounter error!", "error", err.Error())
37 | }
38 | }
39 |
40 | func (h *httpd) Start() {
41 | http.HandleFunc("/tunnels", h.getAllStreams)
42 | err := http.ListenAndServe(h.ListenAddr, nil)
43 | if err != nil {
44 | panic(err)
45 | }
46 | }
47 |
48 | func NewHttpd(listenAddr string) httpd {
49 | return httpd{ListenAddr: listenAddr}
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/token/cleartext_token_parser.go:
--------------------------------------------------------------------------------
1 | package token
2 |
3 | import (
4 | "encoding/base64"
5 | "strings"
6 | )
7 |
8 | type cleartextTokerParser struct {
9 | enctype string
10 | }
11 |
12 | func (t cleartextTokerParser) ParseToken(token string) (string, error) {
13 | switch strings.ToLower(t.enctype) {
14 | case "base64":
15 | socket, err := base64.StdEncoding.DecodeString(token)
16 | if err != nil {
17 | return "", err
18 | } else {
19 | return string(socket), nil
20 | }
21 | default:
22 | return token, nil
23 | }
24 | }
25 |
26 | // NewCleartextTokenParserPlugin return a ``Cleartext`` type token parser plugin.
27 | // The token parser plugin require the token from client endpoint mustn't be encrypted.
28 | // The key specify the token's enctype, it can be ``base64`` or a ""(null chart string).
29 | func NewCleartextTokenParserPlugin(key string) cleartextTokerParser {
30 | return cleartextTokerParser{enctype: key}
31 | }
32 |
--------------------------------------------------------------------------------
/pkg/token/file_token_source.go:
--------------------------------------------------------------------------------
1 | package token
2 |
3 | import (
4 | "bufio"
5 | "errors"
6 | "os"
7 | "strings"
8 | )
9 |
10 | type fileTokenSourcePlugin struct {
11 | filePath string
12 | }
13 |
14 | func (t fileTokenSourcePlugin) GetToken(addr string) (string, error) {
15 | ipAddr := strings.Split(addr, ":")[0]
16 | file, err := os.Open(t.filePath)
17 | if err != nil {
18 | return "", err
19 | }
20 | defer file.Close()
21 | scanner := bufio.NewScanner(file)
22 | for scanner.Scan() {
23 | line := scanner.Text()
24 | if strings.HasPrefix(line, ipAddr) {
25 | return strings.Split(line, " ")[1], nil
26 | }
27 | }
28 | return "", errors.New("don't find valid token")
29 | }
30 |
31 | // NewFileTokenSourcePlugin return a ``File`` type token source plugin.
32 | // ``File`` type token source plugin will read the token from a file.
33 | // The tokenSource is the file path.
34 | func NewFileTokenSourcePlugin(tokenSource string) fileTokenSourcePlugin {
35 | return fileTokenSourcePlugin{filePath: tokenSource}
36 | }
37 |
--------------------------------------------------------------------------------
/pkg/token/fixed_token_source.go:
--------------------------------------------------------------------------------
1 | package token
2 |
3 | type fixedTokenSourcePlugin struct {
4 | token string
5 | }
6 |
7 | func (t fixedTokenSourcePlugin) GetToken(addr string) (string, error) {
8 | return t.token, nil
9 | }
10 |
11 | // NewFixedTokenPlugin return a ``Fixed`` type token source plugin.
12 | // ``Fixed`` token source plugin will return a fixed token always, this mean that all
13 | // client applications always assess same server application.
14 | // The plugin directly return the value tokenSource
15 | func NewFixedTokenPlugin(tokenSource string) fixedTokenSourcePlugin {
16 | return fixedTokenSourcePlugin{token: tokenSource}
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/token/http_token_source.go:
--------------------------------------------------------------------------------
1 | package token
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "net/http"
7 | "net/url"
8 | )
9 |
10 | type httpTokenSourcePlugin struct {
11 | urlPath string
12 | }
13 |
14 | type result struct {
15 | Token string `json:"token"`
16 | }
17 |
18 | func (t httpTokenSourcePlugin) GetToken(addr string) (string, error) {
19 | params := url.Values{}
20 | url, err := url.Parse(t.urlPath)
21 | if err != nil {
22 | return "", err
23 | }
24 | params.Set("addr", addr)
25 | url.RawQuery = params.Encode()
26 | urlPath := url.String()
27 | resp, err := http.Get(urlPath)
28 | if err != nil {
29 | return "", err
30 | }
31 | defer resp.Body.Close()
32 | body, err := ioutil.ReadAll(resp.Body)
33 | if err != nil {
34 | return "", err
35 | }
36 | var res result
37 | _ = json.Unmarshal(body, &res)
38 | return res.Token, nil
39 | }
40 |
41 | // NewHttpTokenPlugin return a ``Http`` type token source plugin.
42 | // ``Http`` token source plugin will initiate an http request and
43 | // get the token based on addr
44 | func NewHttpTokenPlugin(tokenSource string) httpTokenSourcePlugin {
45 | return httpTokenSourcePlugin{urlPath: tokenSource}
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/token/interface.go:
--------------------------------------------------------------------------------
1 | package token
2 |
3 | // Used to provide token to client endpoint
4 | type TokenSourcePlugin interface {
5 | // GetToken return a token string according to the addr (client application address) parameter
6 | GetToken(addr string) (string, error)
7 | }
8 |
9 | // Used to parse token which form client endpoint
10 | type TokenParserPlugin interface {
11 | // ParseToken parse the token and return the parse result
12 | ParseToken(token string) (string, error)
13 | }
14 |
--------------------------------------------------------------------------------
/pkg/tunnel/datastore.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | type tunnelDataStore struct {
8 | sync.Map
9 | }
10 |
11 | func (t *tunnelDataStore) LoadAll() []tunnel {
12 | var tunnels []tunnel
13 | t.Range(func(key, value any) bool {
14 | tunnels = append(tunnels, value.(tunnel))
15 | return true
16 | })
17 | return tunnels
18 | }
19 |
20 | // Used to store all active tunnels information
21 | var DataStore = tunnelDataStore{}
22 |
--------------------------------------------------------------------------------
/pkg/tunnel/handshake.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "context"
5 | "net"
6 | "strings"
7 |
8 | "github.com/kungze/quic-tun/pkg/token"
9 | "github.com/lucas-clemente/quic-go"
10 | )
11 |
12 | type handshakefunc func(context.Context, *quic.Stream, *HandshakeHelper) (bool, *net.Conn)
13 |
14 | type HandshakeHelper struct {
15 | // The function used to process handshake, the handshake processes
16 | // are different between server endpoint and client endpoint.
17 | Handshakefunc handshakefunc `json:"-"`
18 | TokenSource *token.TokenSourcePlugin
19 | TokenParser *token.TokenParserPlugin
20 | // The data will send to remote endpoint. In client
21 | // endpoit, this means 'token'; In server endpoint,
22 | // this means ack message.
23 | SendData []byte
24 | // Used to store the data receive from remote endpoint. In
25 | // server endpoint, this store the 'token'; In client endpoint,
26 | // this store the ack message.
27 | ReceiveData string
28 | }
29 |
30 | func (h *HandshakeHelper) Write(b []byte) (int, error) {
31 | h.ReceiveData = strings.ReplaceAll(string(b), "\x00", "")
32 | return len(b), nil
33 | }
34 |
35 | func (h *HandshakeHelper) Read(p []byte) (n int, err error) {
36 | copy(p, h.SendData)
37 | return len(h.SendData), nil
38 | }
39 |
40 | // Set the data which will be send to remote endpoint.
41 | // For client endpoint, this means set a token; for server endpoint,
42 | // this means set a ack message.
43 | func (h *HandshakeHelper) SetSendData(data []byte) {
44 | // We wish that different tokens or different ack massages have same
45 | // length, so we don't use '=' operator to change the value, replace
46 | // of use 'copy' method.
47 | copy(h.SendData, data)
48 | }
49 |
50 | func NewHandshakeHelper(length int, hsf handshakefunc) HandshakeHelper {
51 | // Make a fixed length data, we wish that the message's length is
52 | // explicit and constant in handshake stage.
53 | data := make([]byte, length)
54 | return HandshakeHelper{SendData: data, Handshakefunc: hsf}
55 | }
56 |
--------------------------------------------------------------------------------
/pkg/tunnel/tunnel.go:
--------------------------------------------------------------------------------
1 | package tunnel
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "net"
9 | "sync"
10 | "time"
11 |
12 | "github.com/google/uuid"
13 | "github.com/kungze/quic-tun/pkg/classifier"
14 | "github.com/kungze/quic-tun/pkg/constants"
15 | "github.com/kungze/quic-tun/pkg/log"
16 | "github.com/lucas-clemente/quic-go"
17 | )
18 |
19 | type tunnel struct {
20 | Stream *quic.Stream `json:"-"`
21 | Conn *net.Conn `json:"-"`
22 | Hsh *HandshakeHelper `json:"-"`
23 | Uuid uuid.UUID `json:"uuid"`
24 | StreamID quic.StreamID `json:"streamId"`
25 | Endpoint string `json:"endpoint"`
26 | ClientAppAddr string `json:"clientAppAddr,omitempty"`
27 | ServerAppAddr string `json:"serverAppAddr,omitempty"`
28 | RemoteEndpointAddr string `json:"remoteEndpointAddr"`
29 | CreatedAt string `json:"createdAt"`
30 | ServerTotalBytes int64 `json:"serverTotalBytes"`
31 | ClientTotalBytes int64 `json:"clientTotalBytes"`
32 | ServerSendRate string `json:"serverSendRate"`
33 | ClientSendRate string `json:"clientSendRate"`
34 | Protocol string `json:"protocol"`
35 | ProtocolProperties any `json:"protocolProperties"`
36 | // Used to cache the header data from QUIC stream
37 | streamCache *classifier.HeaderCache
38 | // Used to cache the header data from TCP/UNIX socket connection
39 | connCache *classifier.HeaderCache
40 | }
41 |
42 | // Before the tunnel establishment, client endpoint and server endpoint need to
43 | // process handshake steps (client endpoint send token, server endpont parse and verify token)
44 | func (t *tunnel) HandShake(ctx context.Context) bool {
45 | res, conn := t.Hsh.Handshakefunc(ctx, t.Stream, t.Hsh)
46 | if conn != nil {
47 | t.Conn = conn
48 | }
49 | return res
50 | }
51 |
52 | func (t *tunnel) countTraffic(ctx context.Context, stream2conn, conn2stream <-chan int) {
53 | var s2cTotal, s2cPreTotal, c2sTotal, c2sPreTotal int64
54 | var s2cRate, c2sRate float64
55 | var tmp int
56 | timeTick := time.NewTicker(1 * time.Second)
57 | for {
58 | select {
59 | case <-ctx.Done():
60 | return
61 | case tmp = <-stream2conn:
62 | s2cTotal += int64(tmp)
63 | case tmp = <-conn2stream:
64 | c2sTotal += int64(tmp)
65 | case <-timeTick.C:
66 | s2cRate = float64((s2cTotal - s2cPreTotal)) / 1024.0
67 | s2cPreTotal = s2cTotal
68 | c2sRate = float64((c2sTotal - c2sPreTotal)) / 1024.0
69 | c2sPreTotal = c2sTotal
70 | }
71 | if t.Endpoint == constants.ClientEndpoint {
72 | t.ServerTotalBytes = s2cTotal
73 | t.ServerSendRate = fmt.Sprintf("%.2f kB/s", s2cRate)
74 | t.ClientTotalBytes = c2sTotal
75 | t.ClientSendRate = fmt.Sprintf("%.2f kB/s", c2sRate)
76 | }
77 | if t.Endpoint == constants.ServerEndpoint {
78 | t.ServerTotalBytes = c2sTotal
79 | t.ServerSendRate = fmt.Sprintf("%.2f kB/s", c2sRate)
80 | t.ClientTotalBytes = s2cTotal
81 | t.ClientSendRate = fmt.Sprintf("%.2f kB/s", s2cRate)
82 | }
83 | DataStore.Store(t.Uuid, *t)
84 | }
85 | }
86 |
87 | func (t *tunnel) Establish(ctx context.Context) {
88 | logger := log.FromContext(ctx)
89 | var wg sync.WaitGroup
90 | wg.Add(2)
91 | var (
92 | steam2conn = make(chan int, 1024)
93 | conn2stream = make(chan int, 1024)
94 | )
95 | t.fillProperties(ctx)
96 | DataStore.Store(t.Uuid, *t)
97 | go t.conn2Stream(logger, &wg, conn2stream)
98 | go t.stream2Conn(logger, &wg, steam2conn)
99 | logger.Info("Tunnel established successful")
100 | // If the tunnel already prepare to close but the analyze
101 | // process still is running, we need to cancle it by concle context.
102 | ctx, cancle := context.WithCancel(ctx)
103 | defer cancle()
104 | go t.countTraffic(ctx, steam2conn, conn2stream)
105 | go t.analyze(ctx)
106 | wg.Wait()
107 | DataStore.Delete(t.Uuid)
108 | logger.Info("Tunnel closed")
109 | }
110 |
111 | func (t *tunnel) analyze(ctx context.Context) {
112 | discrs := classifier.LoadDiscriminators()
113 | var res int
114 | // We don't know that the number and time the traffic data pass through the tunnel.
115 | // This means we cannot know what time we can get the enough data in order to we can
116 | // distinguish the protocol of the traffic that pass through the tunnel. So, we set
117 | // a time ticker, periodic to analy the header data until has discirminator affirm the
118 | // traffic or all discirminators deny the traffic.
119 | timeTick := time.NewTicker(500 * time.Millisecond)
120 | for {
121 | select {
122 | case <-ctx.Done():
123 | DataStore.Delete(t.Uuid)
124 | return
125 | case <-timeTick.C:
126 | for protocol, discr := range discrs {
127 | // In client endpoint, connCache store client application header data, streamCache
128 | // store server application header data; In server endpoint, them is inverse.
129 | if t.Endpoint == constants.ClientEndpoint {
130 | res = discr.AnalyzeHeader(ctx, &t.connCache.Header, &t.streamCache.Header)
131 | } else {
132 | res = discr.AnalyzeHeader(ctx, &t.streamCache.Header, &t.connCache.Header)
133 | }
134 | // If the discriminator deny the traffic header, we delete it.
135 | if res == classifier.DENY {
136 | delete(discrs, protocol)
137 | }
138 | // Once the traffic's protocol was confirmed, we just need remain this discriminator.
139 | if res == classifier.AFFIRM || res == classifier.INCOMPLETE {
140 | t.Protocol = protocol
141 | t.ProtocolProperties = discr.GetProperties(ctx)
142 | DataStore.Store(t.Uuid, *t)
143 | break
144 | }
145 | }
146 | // The protocol was affirmed or all discriminators deny it.
147 | if res == classifier.AFFIRM || len(discrs) == 0 {
148 | return
149 | }
150 | }
151 | }
152 | }
153 |
154 | func (t *tunnel) fillProperties(ctx context.Context) {
155 | t.StreamID = (*t.Stream).StreamID()
156 | if t.Endpoint == constants.ClientEndpoint {
157 | t.ClientAppAddr = (*t.Conn).RemoteAddr().String()
158 | }
159 | if t.Endpoint == constants.ServerEndpoint {
160 | t.ServerAppAddr = (*t.Conn).RemoteAddr().String()
161 | }
162 | t.RemoteEndpointAddr = fmt.Sprint(ctx.Value(constants.CtxRemoteEndpointAddr))
163 | t.CreatedAt = time.Now().String()
164 | }
165 |
166 | func (t *tunnel) stream2Conn(logger log.Logger, wg *sync.WaitGroup, forwardNumChan chan<- int) {
167 | defer func() {
168 | (*t.Stream).Close()
169 | (*t.Conn).Close()
170 | wg.Done()
171 | }()
172 | // Cache the first 1024 byte datas, quic-tun will use them to analy the traffic's protocol
173 | err := t.copyN(io.MultiWriter(*t.Conn, t.streamCache), *t.Stream, classifier.HeaderLength, forwardNumChan)
174 | if err == nil {
175 | err = t.copy(*t.Conn, *t.Stream, forwardNumChan)
176 | }
177 | if err != nil {
178 | logger.Errorw("Can not forward packet from QUIC stream to TCP/UNIX socket", "error", err.Error())
179 | }
180 | }
181 |
182 | func (t *tunnel) conn2Stream(logger log.Logger, wg *sync.WaitGroup, forwardNumChan chan<- int) {
183 | defer func() {
184 | (*t.Stream).Close()
185 | (*t.Conn).Close()
186 | wg.Done()
187 | }()
188 | // Cache the first 1024 byte datas, quic-tun will use them to analy the traffic's protocol
189 | err := t.copyN(io.MultiWriter(*t.Stream, t.connCache), *t.Conn, classifier.HeaderLength, forwardNumChan)
190 | if err == nil {
191 | err = t.copy(*t.Stream, *t.Conn, forwardNumChan)
192 | }
193 | if err != nil {
194 | logger.Errorw("Can not forward packet from TCP/UNIX socket to QUIC stream", "error", err.Error())
195 | }
196 | }
197 |
198 | // Rewrite io.CopyN function https://pkg.go.dev/io#CopyN
199 | func (t *tunnel) copyN(dst io.Writer, src io.Reader, n int64, copyNumChan chan<- int) error {
200 | return t.copy(dst, io.LimitReader(src, n), copyNumChan)
201 | }
202 |
203 | // Rewrite io.Copy function https://pkg.go.dev/io#Copy
204 | func (t *tunnel) copy(dst io.Writer, src io.Reader, nwChan chan<- int) (err error) {
205 | size := 32 * 1024
206 | if l, ok := src.(*io.LimitedReader); ok && int64(size) > l.N {
207 | if l.N < 1 {
208 | size = 1
209 | } else {
210 | size = int(l.N)
211 | }
212 | }
213 | buf := make([]byte, size)
214 | for {
215 | nr, er := src.Read(buf)
216 | if nr > 0 {
217 | nw, ew := dst.Write(buf[0:nr])
218 | if nw < 0 || nr < nw {
219 | nw = 0
220 | if ew == nil {
221 | ew = errors.New("invalid write result")
222 | }
223 | }
224 | nwChan <- nw
225 | if ew != nil {
226 | err = ew
227 | break
228 | }
229 | if nr != nw {
230 | err = io.ErrShortWrite
231 | break
232 | }
233 | }
234 | if er != nil {
235 | if er != io.EOF {
236 | err = er
237 | }
238 | break
239 | }
240 | }
241 | return err
242 | }
243 |
244 | func NewTunnel(stream *quic.Stream, endpoint string) tunnel {
245 | var streamCache = classifier.HeaderCache{}
246 | var connCache = classifier.HeaderCache{}
247 | return tunnel{
248 | Uuid: uuid.New(),
249 | Stream: stream,
250 | Endpoint: endpoint,
251 | streamCache: &streamCache,
252 | connCache: &connCache,
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/quic-tun.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kungze/quic-tun/a1858ea17a3dd59d9294e022bbec426f58e24923/quic-tun.png
--------------------------------------------------------------------------------
/server/cmd/main.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2022 NAME HERE
3 |
4 | */
5 | package main
6 |
7 | import (
8 | "crypto/rand"
9 | "crypto/rsa"
10 | "crypto/tls"
11 | "crypto/x509"
12 | "encoding/pem"
13 | "flag"
14 | "fmt"
15 | "math/big"
16 | "os"
17 | "strings"
18 |
19 | "github.com/kungze/quic-tun/pkg/log"
20 | "github.com/kungze/quic-tun/pkg/options"
21 | "github.com/kungze/quic-tun/pkg/restfulapi"
22 | "github.com/kungze/quic-tun/pkg/token"
23 | "github.com/kungze/quic-tun/server"
24 | "github.com/spf13/cobra"
25 | "github.com/spf13/viper"
26 | )
27 |
28 | var (
29 | serOptions *options.ServerOptions
30 | apiOptions *options.RestfulAPIOptions
31 | secOptions *options.SecureOptions
32 | logOptions *log.Options
33 | )
34 |
35 | func buildCommand(basename string) *cobra.Command {
36 | rootCmd := &cobra.Command{
37 | Use: basename,
38 | Short: "Start up the server side endpoint",
39 | Long: `Establish a fast&security tunnel,
40 | make you can access remote TCP/UNIX application like local application.
41 |
42 | Find more quic-tun information at:
43 | https://github.com/kungze/quic-tun/blob/master/README.md`,
44 | RunE: runCommand,
45 | }
46 | // Initialize the flags needed to start the server
47 | rootCmd.Flags().AddGoFlagSet(flag.CommandLine)
48 | serOptions.AddFlags(rootCmd.Flags())
49 | apiOptions.AddFlags(rootCmd.Flags())
50 | secOptions.AddFlags(rootCmd.Flags())
51 | options.AddConfigFlag(basename, rootCmd.Flags())
52 | logOptions.AddFlags(rootCmd.Flags())
53 |
54 | return rootCmd
55 | }
56 |
57 | func runCommand(cmd *cobra.Command, args []string) error {
58 | options.PrintWorkingDir()
59 | options.PrintFlags(cmd.Flags())
60 | options.PrintConfig()
61 |
62 | if err := viper.BindPFlags(cmd.Flags()); err != nil {
63 | return err
64 | }
65 |
66 | if err := viper.Unmarshal(serOptions); err != nil {
67 | return err
68 | }
69 |
70 | if err := viper.Unmarshal(apiOptions); err != nil {
71 | return err
72 | }
73 |
74 | if err := viper.Unmarshal(secOptions); err != nil {
75 | return err
76 | }
77 |
78 | if err := viper.Unmarshal(logOptions); err != nil {
79 | return err
80 | }
81 |
82 | // run server
83 | runFunc(serOptions, apiOptions, secOptions)
84 | return nil
85 | }
86 |
87 | func runFunc(so *options.ServerOptions, ao *options.RestfulAPIOptions, seco *options.SecureOptions) {
88 | log.Init(logOptions)
89 | defer log.Flush()
90 |
91 | keyFile := seco.KeyFile
92 | certFile := seco.CertFile
93 | verifyClient := seco.VerifyRemoteEndpoint
94 | caFile := seco.CaFile
95 | tokenParserPlugin := so.TokenParserPlugin
96 | tokenParserKey := so.TokenParserKey
97 |
98 | var tlsConfig *tls.Config
99 | if keyFile == "" || certFile == "" {
100 | tlsConfig = generateTLSConfig()
101 | } else {
102 | tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile)
103 | if err != nil {
104 | log.Errorw("Certificate file or private key file is invalid.", "error", err.Error())
105 | return
106 | }
107 | tlsConfig = &tls.Config{
108 | Certificates: []tls.Certificate{tlsCert},
109 | NextProtos: []string{"quic-tun"},
110 | }
111 |
112 | }
113 | if verifyClient {
114 | if caFile == "" {
115 | certPool, err := x509.SystemCertPool()
116 | if err != nil {
117 | log.Errorw("Failed to load system cert pool", "error", err.Error())
118 | return
119 | }
120 | tlsConfig.ClientCAs = certPool
121 | } else {
122 | caPemBlock, err := os.ReadFile(caFile)
123 | if err != nil {
124 | log.Errorw("Failed to read ca file.", "error", err.Error())
125 | }
126 | certPool := x509.NewCertPool()
127 | certPool.AppendCertsFromPEM(caPemBlock)
128 | tlsConfig.ClientCAs = certPool
129 | }
130 | tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
131 | }
132 |
133 | // Start API server
134 | httpd := restfulapi.NewHttpd(ao.HttpdListenOn)
135 | go httpd.Start()
136 |
137 | // Start server endpoint
138 | s := &server.ServerEndpoint{
139 | Address: so.ListenOn,
140 | TlsConfig: tlsConfig,
141 | TokenParser: loadTokenParserPlugin(tokenParserPlugin, tokenParserKey),
142 | }
143 | s.Start()
144 | }
145 |
146 | // Setup a bare-bones TLS config for the server
147 | func generateTLSConfig() *tls.Config {
148 | key, err := rsa.GenerateKey(rand.Reader, 1024)
149 | if err != nil {
150 | panic(err)
151 | }
152 | template := x509.Certificate{SerialNumber: big.NewInt(1)}
153 | certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
154 | if err != nil {
155 | panic(err)
156 | }
157 | keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
158 | certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
159 |
160 | tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
161 | if err != nil {
162 | panic(err)
163 | }
164 | return &tls.Config{
165 | Certificates: []tls.Certificate{tlsCert},
166 | NextProtos: []string{"quic-tun"},
167 | }
168 | }
169 |
170 | func loadTokenParserPlugin(plugin string, key string) token.TokenParserPlugin {
171 | switch strings.ToLower(plugin) {
172 | case "cleartext":
173 | return token.NewCleartextTokenParserPlugin(key)
174 | default:
175 | panic(fmt.Sprintf("Token parser plugin %s don't support", plugin))
176 | }
177 | }
178 |
179 | func main() {
180 | // Initialize the options needed to start the server
181 | serOptions = options.GetDefaultServerOptions()
182 | apiOptions = options.GetDefaultRestfulAPIOptions()
183 | secOptions = options.GetDefaultSecureOptions()
184 | logOptions = log.NewOptions()
185 |
186 | rootCmd := buildCommand("quictun-server")
187 | if err := rootCmd.Execute(); err != nil {
188 | os.Exit(1)
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/server/server.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "context"
5 | "crypto/tls"
6 | "io"
7 | "net"
8 | "strings"
9 |
10 | "github.com/kungze/quic-tun/pkg/constants"
11 | "github.com/kungze/quic-tun/pkg/log"
12 | "github.com/kungze/quic-tun/pkg/token"
13 | "github.com/kungze/quic-tun/pkg/tunnel"
14 | "github.com/lucas-clemente/quic-go"
15 | )
16 |
17 | type ServerEndpoint struct {
18 | Address string
19 | TlsConfig *tls.Config
20 | TokenParser token.TokenParserPlugin
21 | }
22 |
23 | func (s *ServerEndpoint) Start() {
24 | // Listen a quic(UDP) socket.
25 | listener, err := quic.ListenAddr(s.Address, s.TlsConfig, nil)
26 | if err != nil {
27 | panic(err)
28 | }
29 | defer listener.Close()
30 | log.Infow("Server endpoint start up successful", "listen address", listener.Addr())
31 | for {
32 | // Wait client endpoint connection request.
33 | session, err := listener.Accept(context.Background())
34 | if err != nil {
35 | log.Errorw("Encounter error when accept a connection.", "error", err.Error())
36 | } else {
37 | parent_ctx := context.WithValue(context.TODO(), constants.CtxRemoteEndpointAddr, session.RemoteAddr().String())
38 | logger := log.WithValues(constants.ClientEndpointAddr, session.RemoteAddr().String())
39 | logger.Info("A new client endpoint connect request accepted.")
40 | go func() {
41 | for {
42 | // Wait client endpoint open a stream (A new steam means a new tunnel)
43 | stream, err := session.AcceptStream(context.Background())
44 | if err != nil {
45 | logger.Errorw("Cannot accept a new stream.", "error", err.Error())
46 | break
47 | }
48 | logger := logger.WithValues(constants.StreamID, stream.StreamID())
49 | ctx := logger.WithContext(parent_ctx)
50 | hsh := tunnel.NewHandshakeHelper(constants.AckMsgLength, handshake)
51 | hsh.TokenParser = &s.TokenParser
52 |
53 | tun := tunnel.NewTunnel(&stream, constants.ServerEndpoint)
54 | tun.Hsh = &hsh
55 | if !tun.HandShake(ctx) {
56 | continue
57 | }
58 | // After handshake successful the server application's address is established we can add it to log
59 | ctx = logger.WithValues(constants.ServerAppAddr, (*tun.Conn).RemoteAddr().String()).WithContext(ctx)
60 | go tun.Establish(ctx)
61 | }
62 | }()
63 | }
64 | }
65 | }
66 |
67 | func handshake(ctx context.Context, stream *quic.Stream, hsh *tunnel.HandshakeHelper) (bool, *net.Conn) {
68 | logger := log.FromContext(ctx)
69 | logger.Info("Starting handshake with client endpoint")
70 | if _, err := io.CopyN(hsh, *stream, constants.TokenLength); err != nil {
71 | logger.Errorw("Can not receive token", "error", err.Error())
72 | return false, nil
73 | }
74 | addr, err := (*hsh.TokenParser).ParseToken(hsh.ReceiveData)
75 | if err != nil {
76 | logger.Errorw("Failed to parse token", "error", err.Error())
77 | hsh.SetSendData([]byte{constants.ParseTokenError})
78 | _, _ = io.Copy(*stream, hsh)
79 | return false, nil
80 | }
81 | logger = logger.WithValues(constants.ServerAppAddr, addr)
82 | logger.Info("starting connect to server app")
83 | sockets := strings.Split(addr, ":")
84 | conn, err := net.Dial(strings.ToLower(sockets[0]), strings.Join(sockets[1:], ":"))
85 | if err != nil {
86 | logger.Errorw("Failed to dial server app", "error", err.Error())
87 | hsh.SetSendData([]byte{constants.CannotConnServer})
88 | _, _ = io.Copy(*stream, hsh)
89 | return false, nil
90 | }
91 | logger.Info("Server app connect successful")
92 | hsh.SetSendData([]byte{constants.HandshakeSuccess})
93 | if _, err = io.CopyN(*stream, hsh, constants.AckMsgLength); err != nil {
94 | logger.Errorw("Faied to send ack info", "error", err.Error(), "", hsh.SendData)
95 | return false, nil
96 | }
97 | logger.Info("Handshake successful")
98 | return true, &conn
99 | }
100 |
--------------------------------------------------------------------------------
/tests/latency/README.md:
--------------------------------------------------------------------------------
1 | # Network Latency test tool
2 |
3 | The directory contain two python scripts: `server` and `client`, these used to test network latency of TCP.
4 | The `server` script will start a TCP service, the `client` script will connect to the TCP service and send
5 | some packets to the service and the service will send the received data to client. The `client` script will
6 | compute the total time that this process cost and use it as the network latency.
7 |
8 | ## Qickstart
9 |
10 | ```shell
11 | git clone https://github.com/kungze/quic-tun.git
12 | cd quic-tun/test/latency
13 | ```
14 |
15 | In server machine, start TCP service by `server` script:
16 |
17 | ```console
18 | $ ./server
19 | The server listen on 0.0.0.0:15676
20 | ```
21 |
22 | You can used command `./server --help` to learn more useage methods.
23 |
24 | In client machine, use `client` script to connect above TCP service:
25 |
26 | ```console
27 | $ ./client --server-host 127.0.0.1
28 | First packet latency: 0.30732154846191406 ms
29 | Total latency: 39.47257995605469 ms
30 | ```
31 |
32 | The `client` script printed the network latency informations as above.
33 |
--------------------------------------------------------------------------------
/tests/latency/client:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import socket
4 | import time
5 | import argparse
6 |
7 | parser = argparse.ArgumentParser()
8 | parser.add_argument('--server-host', required=True,
9 | help='The server address to connect to')
10 | parser.add_argument('--server-port', required=False,
11 | help='The server port to connect to (default: 15676)')
12 | parser.add_argument('--size', required=False,
13 | help='The size of packet send to server (default: 1000).')
14 | parser.add_argument('--number', required=False,
15 | help='The number of the packets send to server (default: 1000).')
16 | args = parser.parse_args()
17 |
18 | if not args.server_port:
19 | args.server_port = '15676'
20 | if not args.size:
21 | args.size = '1000'
22 | if not args.number:
23 | args.number = '1000'
24 |
25 | s = socket.socket()
26 | s.connect((args.server_host, int(args.server_port)))
27 | size = int(args.size)
28 | number = int(args.number)
29 | first = time.time()
30 | s.send(bytearray("a" * size, 'utf-8'))
31 | s.recv(size)
32 | start = time.time()
33 | for i in range(0, number-1):
34 | s.send(bytearray("a" * size, 'utf-8'))
35 | s.recv(size)
36 | end = time.time()
37 | print(f"First packet latency: {(start-first)*1000} ms")
38 | print(f"Total latency: {(end-first)*1000} ms")
39 | s.close()
40 |
--------------------------------------------------------------------------------
/tests/latency/server:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import socket
4 | import argparse
5 |
6 | parser = argparse.ArgumentParser()
7 | parser.add_argument('--host', required=False,
8 | help='Server host to listen on (default: 0.0.0.0).')
9 | parser.add_argument('--port', required=False,
10 | help='Server port to listen on (default: 15676).')
11 | parser.add_argument('--buffer-size', required=False,
12 | help="The size of the buffer which used to receive client's data (default: 1000).")
13 | args = parser.parse_args()
14 |
15 | if not args.host:
16 | args.host = '0.0.0.0'
17 | if not args.port:
18 | args.port = '15676'
19 | if not args.buffer_size:
20 | args.buffer_size = '1000'
21 |
22 | buffer_size = int(args.buffer_size)
23 | s = socket.socket()
24 | s.bind((args.host, int(args.port)))
25 | s.listen()
26 | print(f"The server listen on {args.host}:{args.port}")
27 | while True:
28 | client, addr = s.accept()
29 | print(f"Accent client {addr} connection")
30 | while True:
31 | try:
32 | res = client.recv(buffer_size)
33 | if not res:
34 | client.close()
35 | print(f"Client {addr} closed")
36 | break
37 | client.send(res)
38 | except BrokenPipeError:
39 | client.close()
40 | print(f"Client {addr} closed")
41 | break
42 | except ConnectionResetError:
43 | client.close()
44 | print(f"Client {addr} closed")
45 | break
46 |
--------------------------------------------------------------------------------