├── .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 | quic-tun 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 | quic-tun 25 | quic-tun 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 | quic-tun 64 | quic-tun 65 | 66 | * quic-tun 67 | 68 | quic-tun 69 | quic-tun 70 | 71 | ### Packet loss rate: 0.1% 72 | 73 | * TCP 74 | 75 | quic-tun 76 | quic-tun 77 | 78 | * quic-tun 79 | 80 | quic-tun 81 | quic-tun 82 | 83 | ### Packet loss rate: 0.5% 84 | 85 | * TCP 86 | 87 | quic-tun 88 | quic-tun 89 | 90 | * quic-tun 91 | 92 | quic-tun 93 | quic-tun 94 | 95 | ### Packet loss rate: 1.0% 96 | 97 | * TCP 98 | 99 | quic-tun 100 | quic-tun 101 | 102 | * quic-tun 103 | 104 | quic-tun 105 | quic-tun 106 | 107 | ### Packet loss rate: 5.0% 108 | 109 | * TCP 110 | 111 | quic-tun 112 | quic-tun 113 | 114 | * quic-tun 115 | 116 | quic-tun 117 | quic-tun 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 | quic-tun 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 | --------------------------------------------------------------------------------