diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d0a4021e..56379d16 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -18,12 +18,14 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-2019]
runs-on: ${{ matrix.os }}
+ env:
+ OUTPUT_DIR: ${{ github.workspace }}/out
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- - name: Set up Go 1.20
+ - name: Set up Go
uses: actions/setup-go@v4
with:
go-version-file: '${{ github.workspace }}/go.mod'
@@ -32,10 +34,26 @@ jobs:
run: go build -v ./...
- name: Build X
- run: go build -C x -o bin/ -v ./...
+ run: go build -C x -o "${{ env.OUTPUT_DIR }}/" -v ./...
+
+ - name: Build Go Mobile
+ if: matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest'
+ run: go build -C x -o "${{ env.OUTPUT_DIR }}/" golang.org/x/mobile/cmd/gomobile golang.org/x/mobile/cmd/gobind
+
+ - name: Build Mobileproxy (Android)
+ if: matrix.os == 'ubuntu-latest'
+ run: PATH="${{ env.OUTPUT_DIR }}:$PATH" gomobile bind -ldflags='-s -w' -v -target=android -androidapi=21 -o "${{ env.OUTPUT_DIR }}/mobileproxy.aar" github.com/Jigsaw-Code/outline-sdk/x/mobileproxy
+ working-directory: ${{ github.workspace }}/x
+
+ - name: Build Mobileproxy (iOS)
+ if: matrix.os == 'macos-latest'
+ run: PATH="${{ env.OUTPUT_DIR }}:$PATH" gomobile bind -ldflags='-s -w' -v -target=ios -iosversion=11.0 -o "${{ env.OUTPUT_DIR }}/mobileproxy.xcframework" github.com/Jigsaw-Code/outline-sdk/x/mobileproxy
+ working-directory: ${{ github.workspace }}/x
- name: Test SDK
- run: go test -v -race -bench '.' ./... -benchtime=100ms
+ # Enable nettests, which executes external network requests.
+ run: go test -v -race -bench '.' ./... -benchtime=100ms -tags nettest
- name: Test X
- run: go test -C x -v -race -bench '.' ./... -benchtime=100ms
+ # Enable nettests, which executes external network requests.
+ run: go test -C x -v -race -bench '.' ./... -benchtime=100ms -tags nettest
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dffca7e0..2cbdf818 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -42,25 +42,29 @@ In Go you can compile for other target operating system and architecture by spec
Examples
MacOS example:
-```
-% GOOS=darwin go build -C x -o ./bin/ ./outline-connectivity
-% file ./x/bin/outline-connectivity
-./x/bin/outline-connectivity: Mach-O 64-bit executable x86_64
+
+```console
+% GOOS=darwin go build -C x -o ./bin/ ./examples/test-connectivity
+% file ./x/bin/test-connectivity
+./x/bin/test-connectivity: Mach-O 64-bit executable x86_64
```
Linux example:
-```
-% GOOS=linux go build -C x -o ./bin/ ./outline-connectivity
-% file ./x/bin/outline-connectivity
-./x/bin/outline-connectivity: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=n0WfUGLum4Y6OpYxZYuz/lbtEdv_kvyUCd3V_qOqb/CC_6GAQqdy_ebeYTdn99/Tk_G3WpBWi8vxqmIlIuU, with debug_info, not stripped
+
+```console
+% GOOS=linux go build -C x -o ./bin/ ./examples/test-connectivity
+% file ./x/bin/test-connectivity
+./x/bin/test-connectivity: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=n0WfUGLum4Y6OpYxZYuz/lbtEdv_kvyUCd3V_qOqb/CC_6GAQqdy_ebeYTdn99/Tk_G3WpBWi8vxqmIlIuU, with debug_info, not stripped
```
Windows example:
+
+```console
+% GOOS=windows go build -C x -o ./bin/ ./examples/test-connectivity
+% file ./x/bin/test-connectivity.exe
+./x/bin/test-connectivity.exe: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows
```
-% GOOS=windows go build -C x -o ./bin/ ./outline-connectivity
-% file ./x/bin/outline-connectivity.exe
-./x/bin/outline-connectivity.exe: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows
-```
+
## Running Linux binaries
@@ -72,41 +76,49 @@ To run Linux binaries you can use a Linux container via [Podman](https://podman.
Instructions
[Install Podman](https://podman.io/docs/installation) (once). On macOS:
+
```sh
brew install podman
```
Create the podman service VM (once) with the [`podman machine init` command](https://docs.podman.io/en/latest/markdown/podman-machine-init.1.html):
+
```sh
podman machine init
```
Start the VM with the [`podman machine start` command](https://docs.podman.io/en/latest/markdown/podman-machine-start.1.html), after every time it is stopped:
+
```sh
podman machine start
-```
+```
You can see the VM running with the [`podman machine list` command](https://docs.podman.io/en/latest/markdown/podman-machine-list.1.html):
-```
+
+```console
% podman machine list
NAME VM TYPE CREATED LAST UP CPUS MEMORY DISK SIZE
podman-machine-default* qemu 3 minutes ago Currently running 1 2.147GB 107.4GB
```
When you are done with development, you can stop the machine with the [`podman machine stop` command](https://docs.podman.io/en/latest/markdown/podman-machine-stop.1.html):
+
```sh
podman machine stop
```
+
### Run
The easiest way is to run a binary is to use the [`go run` command](https://pkg.go.dev/cmd/go#hdr-Compile_and_run_Go_program) directly with the `-exec` flag and our convenience tool `run_on_podman.sh`:
+
```sh
-GOOS=linux go run -C x -exec "$(pwd)/run_on_podman.sh" ./outline-connectivity
+GOOS=linux go run -C x -exec "$(pwd)/run_on_podman.sh" ./examples/test-connectivity
```
It also works with the [`go test` command](https://pkg.go.dev/cmd/go#hdr-Test_packages):
+
```sh
GOOS=linux go test -exec "$(pwd)/run_on_podman.sh" ./...
```
@@ -115,14 +127,16 @@ GOOS=linux go test -exec "$(pwd)/run_on_podman.sh" ./...
Details and direct invocation
The `run_on_podman.sh` script uses the [`podman run` command](https://docs.podman.io/en/latest/markdown/podman-run.1.html) and a minimal ["distroless" container image](https://github.com/GoogleContainerTools/distroless) to run the binary you want:
+
```sh
podman run --arch $(uname -m) --rm -it -v "${bin}":/outline/bin gcr.io/distroless/static-debian11 /outline/bin "$@"
```
You can also use `podman run` directly to run a pre-built binary:
-```
-% podman run --rm -it -v ./x/bin:/outline gcr.io/distroless/static-debian11 /outline/outline-connectivity
-Usage of /outline/outline-connectivity:
+
+```console
+% podman run --rm -it -v ./x/bin:/outline gcr.io/distroless/static-debian11 /outline/test-connectivity
+Usage of /outline/test-connectivity:
-domain string
Domain name to resolve in the test (default "example.com.")
-key string
@@ -153,14 +167,16 @@ This is not the same as a real Windows environment, so make sure you test on act
Follow the instructions at https://wiki.winehq.org/Download.
-On macOS:
-```
+On macOS:
+
+```sh
brew tap homebrew/cask-versions
brew install --cask --no-quarantine wine-stable
```
After installation, `wine64` should be on your `PATH`. Check with `wine64 --version`:
-```
+
+```sh
wine64 --version
```
@@ -173,10 +189,21 @@ You can pass `wine64` as the `-exec` parameter in the `go` calls.
To build:
```sh
-GOOS=windows go run -C x -exec "wine64" ./outline-connectivity
+GOOS=windows go run -C x -exec "wine64" ./examples/test-connectivity
```
For tests:
+
```sh
GOOS=windows go test -exec "wine64" ./...
```
+
+# Tests with external network dependencies
+
+Some tests are implemented talking to external services. That's undesirable, but convenient.
+We started tagging them with the `nettest` tag, so they don't run by default. To run them, you need to specify `-tags nettest`, as done in our CI.
+For example:
+
+```sh
+go test -v -race -bench '.' ./... -benchtime=100ms -tags nettest
+```
diff --git a/README.md b/README.md
index 1aa6c188..80b155db 100644
--- a/README.md
+++ b/README.md
@@ -22,8 +22,58 @@ The Outline SDK allows you to:
|:-:|:-:|:-:|
| Supports Android, iOS, Windows, macOS and Linux. | Field-tested in the Outline Client and Server, helping millions to access the internet under harsh conditions. | Designed for modularity and reuse, allowing you to craft custom transports. |
+### Interoperable and Reusable
-## Integration Methods
+The Outline SDK is built upon a simple basic concepts, defined as interoperable interfaces that allow for composition and easy reuse.
+
+**Connections** enable communication between two endpoints over an abstract transport. There are two types of connections:
+ - `transport.StreamConn`: stream-based connection, like TCP and the `SOCK_STREAM` Posix socket type.
+ - `transport.PacketConn`: datagram-based connection, like UDP and the `SOCK_DGRAM` Posix socket type. We use "Packet" instead of "Datagram" because that is the convention in the Go standard library.
+
+Connections can be wrapped to create nested connections over a new transport. For example, a `StreamConn` could be over TCP, over TLS over TCP, over HTTP over TLS over TCP, over QUIC, among oter options.
+
+**Dialers** enable the creation of connections given a host:port address while encapsulating the underlying transport or proxy protocol. The `StreamDialer` and `PacketDialer` types create `StreamConn` and `PacketConn` connections, respectively, given an address. Dialers can also be nested. For example, a TLS Stream Dialer can use a TCP dialer to create a `StreamConn` backed by a TCP connection, then create a TLS `StreamConn` backed by the TCP `StreamConn`. A SOCKS5-over-TLS Dialer could use the TLS Dialer to create the TLS `StreamConn` to the proxy before doing the SOCKS5 connection to the target address.
+
+**Resolvers** (`dns.Resolver`) enable the answering of DNS questions while encapsulating the underlying algorithm or protocol. Resolvers are primarily used to map domain names to IP addresses.
+
+
+### Bypass DNS-based Blocking
+
+The Outline SDK offers two types of strategies for evading DNS-based blocking: resillient DNS or address override.
+
+- The [dns](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/dns) package can replace the resolution based on the system resolver with more resillient options:
+ - Encrypted DNS over HTTPS (DoH) or TLS (DoT)
+ - Alternative hosts and ports for UDP and TCP resolvers, making it possible to use resolvers that are not blocked.
+- The `override` config from [x/config](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/x/config) with a `host` option can be used to force a specific address,
+ or you can implement your own Dialer that can map addresses.
+
+### Bypass SNI-based Blocking
+
+The Outline SDK offers several strategies for evading SNI-based blocking:
+
+At the TCP layer:
+
+- TCP stream fragmentation with [transport/split](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/transport/split)
+- TLS record fragmentation with [transport/tlsfrag](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/transport/tlsfrag)
+
+At the application layer:
+
+- Domain-fronting and SNI hiding with [transport/tls](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/transport/tls)
+
+### Tunnel Connections over a Proxy
+
+The Outline SDK offers two protocols to create connections over proxies:
+
+- Shadowsocks, available in [transport/shadowsocks](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks).
+ Easily create servers in the cloud using the [Outline Manager](https://getoutline.org/get-started/#step-1).
+- SOCKS5, available in [transport/socks5](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/transport/socks5). You can leverage a [local SOCKS5 proxy that tunnels connections over SSH](https://www.digitalocean.com/community/tutorials/how-to-route-web-traffic-securely-without-a-vpn-using-a-socks-tunnel).
+
+### Build a VPN
+
+Use the [network](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/network) package to create TUN-based VPNs using transport-layer proxies (often called "tun2socks").
+
+
+## Add the SDK to Your App
Choose from one of the following methods to integrate the Outline SDK into your project:
@@ -58,18 +108,17 @@ To integrate the SDK as a side service, follow these steps:
1. **Define IPC mechanism**: Choose an inter-process communication (IPC) mechanism (for example, sockets, standard I/O).
1. **Build the service**: Create a Go binary that includes the server-side of the IPC and used the SDK.
- Examples: [Outline Electron backend code](https://github.com/Jigsaw-Code/outline-go-tun2socks/blob/master/outline/electron/main.go), [Outline Windows Client backend build](https://github.com/Jigsaw-Code/outline-go-tun2socks/blob/dada2652ae2c6205f2daa3f88c805bbd6b28a713/Makefile#L67), [Outline Linux Client backend build](https://github.com/Jigsaw-Code/outline-go-tun2socks/blob/dada2652ae2c6205f2daa3f88c805bbd6b28a713/Makefile#L56).
-3. **Bundle the service**: Include the Go binary in your application bundle.
+1. **Bundle the service**: Include the Go binary in your application bundle.
- Examples: [Outline Windows Client](https://github.com/Jigsaw-Code/outline-client/blob/b06819922037230ee3ba9471097c40793af819e8/src/electron/electron-builder.json#L21), [Outline Linux Client](https://github.com/Jigsaw-Code/outline-client/blob/b06819922037230ee3ba9471097c40793af819e8/src/electron/electron-builder.json#L10)
-4. **Start the service**: Launch the Go binary as a subprocess from your application.
+1. **Start the service**: Launch the Go binary as a subprocess from your application.
- Example: [Outline Electron Clients](https://github.com/Jigsaw-Code/outline-client/blob/b06819922037230ee3ba9471097c40793af819e8/src/electron/go_vpn_tunnel.ts#L227)
-5. **Service Calls**: Add code to your app for communication with the service.
+1. **Service Calls**: Add code to your app for communication with the service.
### Go Library
To integrate the Outline SDK as a Go library, you can directly import it into your Go application. See the [API Reference](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk) for what's available.
-
This approach is suitable for both command-line and GUI-based applications. You can build GUI-based applications in Go with frameworks like [Wails](https://wails.io/), [Fyne.io](https://fyne.io/), [Qt for Go](https://therecipe.github.io/qt/), or [Go Mobile app](https://pkg.go.dev/golang.org/x/mobile/app).
For examples, see [x/examples](./x/examples/).
@@ -84,42 +133,92 @@ Steps:
1. **Generate C library**: Use `go build` with the [appropriate `-buildmode` flag](https://pkg.go.dev/cmd/go#hdr-Build_modes). Anternatively, you can use [SWIG](https://swig.org/Doc4.1/Go.html#Go).
1. **Integrate into your app**: Add the generated C library to your application, according to your build system.
+You can find detailed steps at the tutorial [Go for beginners: Getting started](https://github.com/Jigsaw-Code/outline-sdk/discussions/67).
+
+
+## Command-line Tools
+
+The Outline SDK has several command-line utilities that illustrate the usage of the SDK, but are also valuable for debugging and trying the different strategies without having to build an app.
+
+They all take a `-transport` flag with a config that specifies what transport should be used to establish connections.
+The config format can be found in [x/config](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/x/config).
+
+
+### DNS Query
+
+The [`resolve` tool](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/x/examples/resolve) resolves a domain name, similar to `dig`:
+
+```console
+$ go run github.com/Jigsaw-Code/outline-sdk/x/examples/resolve@latest -type A -transport "tls" -resolver 8.8.8.8:853 -tcp getoutline.org.
+216.239.34.21
+216.239.32.21
+216.239.38.21
+216.239.36.21
+```
+
+
+### HTTP Fetch
+
+The [`fetch` tool](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/x/examples/fetch) fetches
+a URL, similar to `curl`. The example below would bypass blocking of `meduza.io` in Russia:
+
+```console
+$ go run github.com/Jigsaw-Code/outline-sdk/x/examples/fetch@latest -transport "override:host=cloudflare.net|tlsfrag:1" -method HEAD -v https://meduza.io/
+[DEBUG] 2023/12/28 18:44:56.490836 main.go:105: Cf-Ray: [83cdac8ecdccc40e-EWR]
+[DEBUG] 2023/12/28 18:44:56.491231 main.go:105: Alt-Svc: [h3=":443"; ma=86400]
+[DEBUG] 2023/12/28 18:44:56.491237 main.go:105: Date: [Thu, 28 Dec 2023 23:44:56 GMT]
+[DEBUG] 2023/12/28 18:44:56.491241 main.go:105: Connection: [keep-alive]
+[DEBUG] 2023/12/28 18:44:56.491247 main.go:105: Strict-Transport-Security: [max-age=31536000; includeSubDomains; preload]
+[DEBUG] 2023/12/28 18:44:56.491251 main.go:105: Cache-Control: [max-age=0 no-cache, no-store]
+[DEBUG] 2023/12/28 18:44:56.491257 main.go:105: X-Content-Type-Options: [nosniff]
+[DEBUG] 2023/12/28 18:44:56.491262 main.go:105: Server: [cloudflare]
+[DEBUG] 2023/12/28 18:44:56.491266 main.go:105: Content-Type: [text/html; charset=utf-8]
+[DEBUG] 2023/12/28 18:44:56.491270 main.go:105: Expires: [Thu, 28 Dec 2023 23:44:56 GMT]
+[DEBUG] 2023/12/28 18:44:56.491273 main.go:105: Cf-Cache-Status: [DYNAMIC]
+```
+
+### Local Proxy Forwarder
+
+The [`http2transport` tool](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/x/examples/http2transport) runs a local proxy that creates connections according to the transport. It's effectively a circumvention tool.
+
+The example below is analogous to the previous fetch example.
+
+Start the local proxy:
+
+```console
+$ go run github.com/Jigsaw-Code/outline-sdk/x/examples/http2transport@latest -transport "override:host=cloudflare.net|tlsfrag:1" -localAddr localhost:8080
+2023/12/28 18:50:48 Proxy listening on 127.0.0.1:8080
+```
+
+Using the proxy with `curl`:
+
+```console
+$ curl -p -x http://localhost:8080 https://meduza.io --head
+HTTP/1.1 200 Connection established
+
+HTTP/2 200
+date: Thu, 28 Dec 2023 23:51:01 GMT
+content-type: text/html; charset=utf-8
+strict-transport-security: max-age=31536000; includeSubDomains; preload
+expires: Thu, 28 Dec 2023 23:51:01 GMT
+cache-control: max-age=0
+cache-control: no-cache, no-store
+cf-cache-status: DYNAMIC
+x-content-type-options: nosniff
+server: cloudflare
+cf-ray: 83cdb579bbec4376-EWR
+alt-svc: h3=":443"; ma=86400
+```
+
+### Proxy Connectivity Test
+
+The [`test-connectivity` tool](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/x/examples/test-connectivity) is useful to test connectivity to a proxy. It uses DNS resolutions over TCP and UDP using the transport to test if there is stream and datagram connectivity.
-## Tentative Roadmap
-
-This launch is currently in Beta. Most of the code is not new. It's the same code that is currently being used by the production Outline Client and Server. The SDK repackages code from [outline-ss-server](https://github.com/Jigsaw-Code/outline-ss-server) and [outline-go-tun2socks](https://github.com/Jigsaw-Code/outline-go-tun2socks) in a way that is easier to reuse and extend.
-
-### Features
-
-- Network-level libraries
- - [x] IP Device abstraction (v0.0.2)
- - [x] IP Device implementation based on go-tun2socks (LWIP) (v0.0.2)
- - [x] UDP handler to fallback to DNS-over-TCP (v0.0.2)
- - [x] DelegatePacketProxy for runtime PacketProxy replacement (v0.0.2)
-
-- Proxy protocols
- - [x] TCP and UDP Dialers (v0.0.2)
- - [x] Shadowsocks wrappers and Dialers ([reference](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks)) (v0.0.2)
- - [x] SOCKS5 StreamDialer ([reference](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/transport/socks5)) (v0.0.6)
- - [ ] SOCKS5 PacketDialer (coming soon)
- - [ ] HTTP Connect (coming soon)
-
-- Transport protocols
- - [x] Stream (TCP) split ([reference](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/transport/split)) (v0.0.6)
- - [x] TLS connection wrapper and StreamDialer ([reference](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/transport/tls))
-
-- Name resolution
- - [ ] Resilient DNS (coming soon)
- - [ ] Encrypted DNS (coming soon)
-
-- Integration resources
- - For Mobile apps
- - [x] Library to run a local SOCKS5 or HTTP-Connect proxy ([source](./x/mobileproxy/mobileproxy.go), [example Go usage](./x/examples/fetch-proxy/main.go), [example mobile usage](./x/examples/mobileproxy)).
- - [x] Documentation on how to integrate the SDK into mobile apps
- - [x] Connectivity Test mobile app (iOS and Android) using [Capacitor](https://capacitorjs.com/)
- - For Go apps
- - [x] Connectivity Test example [Wails](https://wails.io/) graphical app
- - [x] Connectivity Test example command-line app ([source](./x/examples/outline-connectivity/))
- - [x] Outline Client example command-line app ([source](./x/examples/outline-cli/))
- - [x] Page fetch example command-line app ([source](./x/examples/outline-fetch/))
- - [x] Local proxy example command-line app ([source](./x/examples/http2transport/))
+```console
+$ go run github.com/Jigsaw-Code/outline-sdk/x/examples/test-connectivity@latest -transport "$OUTLINE_KEY" && echo success || echo failure
+{"resolver":"8.8.8.8:53","proto":"tcp","time":"2023-12-28T23:57:45Z","duration_ms":39,"error":null}
+{"resolver":"8.8.8.8:53","proto":"udp","time":"2023-12-28T23:57:45Z","duration_ms":17,"error":null}
+{"resolver":"[2001:4860:4860::8888]:53","proto":"tcp","time":"2023-12-28T23:57:45Z","duration_ms":31,"error":null}
+{"resolver":"[2001:4860:4860::8888]:53","proto":"udp","time":"2023-12-28T23:57:45Z","duration_ms":16,"error":null}
+success
+```
diff --git a/dns/doc.go b/dns/doc.go
new file mode 100644
index 00000000..96e5726b
--- /dev/null
+++ b/dns/doc.go
@@ -0,0 +1,41 @@
+// Copyright 2023 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/*
+Package dns provides utilities to interact with the Domain Name System (DNS).
+
+The [Domain Name System] (DNS) is responsible for mapping domain names to IP addresses.
+Because domain resolution gatekeeps connections and is predominantly done in plaintext, it is [commonly used
+for network-level filtering].
+
+# Transports
+
+The main concept in this library is that of a [Resolver], which allows code to query the DNS. Different implementations are provided
+to perform DNS resolution over different transports:
+
+ - [DNS-over-UDP]: the standard mechanism of querying resolvers. Communication is done in plaintext, using port 53.
+ - [DNS-over-TCP]: alternative to UDP that allows for more reliable delivery and larger responses, but requires establishing a connection. Communication is done in plaintext, using port 53.
+ - [DNS-over-TLS] (DoT): uses the TCP protocol, but over a connection encrypted with TLS. Is uses port 853, which
+ makes it very easy to block using the port number, as no other protocol is assigned to that port.
+ - [DNS-over-HTTPS] (DoH): uses HTTP exchanges for querying the resolver and communicates over a connection encrypted with TLS. It uses
+ port 443. That makes the DoH traffic undistinguishable from web traffic, making it harder to block.
+
+[Domain Name System]: https://datatracker.ietf.org/doc/html/rfc1034
+[commonly used for network-level filtering]: https://datatracker.ietf.org/doc/html/rfc9505#section-5.1.1
+[DNS-over-UDP]: https://datatracker.ietf.org/doc/html/rfc1035#section-4.2.1
+[DNS-over-TCP]: https://datatracker.ietf.org/doc/html/rfc7766
+[DNS-over-TLS]: https://datatracker.ietf.org/doc/html/rfc7858
+[DNS-over-HTTPS]: https://datatracker.ietf.org/doc/html/rfc8484
+*/
+package dns
diff --git a/dns/resolver.go b/dns/resolver.go
new file mode 100644
index 00000000..92b83a97
--- /dev/null
+++ b/dns/resolver.go
@@ -0,0 +1,393 @@
+// Copyright 2023 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dns
+
+import (
+ "bytes"
+ "context"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "math/rand"
+ "net"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/Jigsaw-Code/outline-sdk/transport"
+ "github.com/Jigsaw-Code/outline-sdk/transport/tls"
+ "golang.org/x/net/dns/dnsmessage"
+)
+
+var (
+ ErrBadRequest = errors.New("request input is invalid")
+ ErrDial = errors.New("dial DNS resolver failed")
+ ErrSend = errors.New("send DNS message failed")
+ ErrReceive = errors.New("receive DNS message failed")
+ ErrBadResponse = errors.New("response message is invalid")
+)
+
+// nestedError allows us to use errors.Is and still preserve the error cause.
+// This is unlike fmt.Errorf, which creates a new error and preserves the cause,
+// but you can't specify the type of the resulting top-level error.
+type nestedError struct {
+ is error
+ wrapped error
+}
+
+func (e *nestedError) Is(target error) bool { return target == e.is }
+
+func (e *nestedError) Unwrap() error { return e.wrapped }
+
+func (e *nestedError) Error() string { return e.is.Error() + ": " + e.wrapped.Error() }
+
+// Resolver can query the DNS with a question, and obtain a DNS message as response.
+// This abstraction helps hide the underlying transport protocol.
+type Resolver interface {
+ Query(ctx context.Context, q dnsmessage.Question) (*dnsmessage.Message, error)
+}
+
+// FuncResolver is a [Resolver] that uses the given function to query DNS.
+type FuncResolver func(ctx context.Context, q dnsmessage.Question) (*dnsmessage.Message, error)
+
+// Query implements the [Resolver] interface.
+func (f FuncResolver) Query(ctx context.Context, q dnsmessage.Question) (*dnsmessage.Message, error) {
+ return f(ctx, q)
+}
+
+// NewQuestion is a convenience function to create a [dnsmessage.Question].
+// The input domain is interpreted as fully-qualified. If the end "." is missing, it's added.
+func NewQuestion(domain string, qtype dnsmessage.Type) (*dnsmessage.Question, error) {
+ fullDomain := domain
+ if len(domain) == 0 || domain[len(domain)-1] != '.' {
+ fullDomain += "."
+ }
+ name, err := dnsmessage.NewName(fullDomain)
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse domain name: %w", err)
+ }
+ return &dnsmessage.Question{
+ Name: name,
+ Type: qtype,
+ Class: dnsmessage.ClassINET,
+ }, nil
+}
+
+// Maximum UDP message size that we support.
+// The value is taken from https://dnsflagday.net/2020/, which says:
+// "An EDNS buffer size of 1232 bytes will avoid fragmentation on nearly all current networks.
+// This is based on an MTU of 1280, which is required by the IPv6 specification, minus 48 bytes
+// for the IPv6 and UDP headers".
+const maxUDPMessageSize = 1232
+
+// appendRequest appends the bytes a DNS request using the id and question to buf.
+func appendRequest(id uint16, q dnsmessage.Question, buf []byte) ([]byte, error) {
+ b := dnsmessage.NewBuilder(buf, dnsmessage.Header{ID: id, RecursionDesired: true})
+ if err := b.StartQuestions(); err != nil {
+ return nil, fmt.Errorf("start questions failed: %w", err)
+ }
+ if err := b.Question(q); err != nil {
+ return nil, fmt.Errorf("add question failed: %w", err)
+ }
+ if err := b.StartAdditionals(); err != nil {
+ return nil, fmt.Errorf("start additionals failed: %w", err)
+ }
+
+ var rh dnsmessage.ResourceHeader
+ // Set the maximum payload size we support, as per https://datatracker.ietf.org/doc/html/rfc6891#section-4.3
+ if err := rh.SetEDNS0(maxUDPMessageSize, dnsmessage.RCodeSuccess, false); err != nil {
+ return nil, fmt.Errorf("set EDNS(0) failed: %w", err)
+ }
+ if err := b.OPTResource(rh, dnsmessage.OPTResource{}); err != nil {
+ return nil, fmt.Errorf("add OPT RR failed: %w", err)
+ }
+
+ buf, err := b.Finish()
+ if err != nil {
+ return nil, fmt.Errorf("message serialization failed: %w", err)
+ }
+ return buf, nil
+}
+
+// Fold case as clarified in https://datatracker.ietf.org/doc/html/rfc4343#section-3.
+func foldCase(char byte) byte {
+ if 'a' <= char && char <= 'z' {
+ return char - 'a' + 'A'
+ }
+ return char
+}
+
+// equalASCIIName compares DNS name as specified in https://datatracker.ietf.org/doc/html/rfc1035#section-3.1 and
+// https://datatracker.ietf.org/doc/html/rfc4343#section-3.
+func equalASCIIName(x, y dnsmessage.Name) bool {
+ if x.Length != y.Length {
+ return false
+ }
+ for i := 0; i < int(x.Length); i++ {
+ if foldCase(x.Data[i]) != foldCase(y.Data[i]) {
+ return false
+ }
+ }
+ return true
+}
+
+func checkResponse(reqID uint16, reqQues dnsmessage.Question, respHdr dnsmessage.Header, respQs []dnsmessage.Question) error {
+ if !respHdr.Response {
+ return errors.New("response bit not set")
+ }
+
+ // https://datatracker.ietf.org/doc/html/rfc5452#section-4.3
+ if reqID != respHdr.ID {
+ return fmt.Errorf("message id does not match. Expected %v, got %v", reqID, respHdr.ID)
+ }
+
+ // https://datatracker.ietf.org/doc/html/rfc5452#section-4.2
+ if len(respQs) == 0 {
+ return errors.New("response had no questions")
+ }
+ respQ := respQs[0]
+ if reqQues.Type != respQ.Type || reqQues.Class != respQ.Class || !equalASCIIName(reqQues.Name, respQ.Name) {
+ return errors.New("response question doesn't match request")
+ }
+
+ return nil
+}
+
+// queryDatagram implements a DNS query over a datagram protocol.
+func queryDatagram(conn io.ReadWriter, q dnsmessage.Question) (*dnsmessage.Message, error) {
+ // Reference: https://cs.opensource.google/go/go/+/master:src/net/dnsclient_unix.go?q=func:dnsPacketRoundTrip&ss=go%2Fgo
+ id := uint16(rand.Uint32())
+ buf, err := appendRequest(id, q, make([]byte, 0, maxUDPMessageSize))
+ if err != nil {
+ return nil, &nestedError{ErrBadRequest, fmt.Errorf("append request failed: %w", err)}
+ }
+ if _, err := conn.Write(buf); err != nil {
+ return nil, &nestedError{ErrSend, err}
+ }
+ buf = buf[:cap(buf)]
+ var returnErr error
+ for {
+ n, err := conn.Read(buf)
+ // Handle bad io.Reader.
+ if err == io.EOF && n > 0 {
+ err = nil
+ }
+ if err != nil {
+ return nil, &nestedError{ErrReceive, errors.Join(returnErr, fmt.Errorf("read message failed: %w", err))}
+ }
+ var msg dnsmessage.Message
+ if err := msg.Unpack(buf[:n]); err != nil {
+ returnErr = errors.Join(returnErr, err)
+ // Ignore invalid packets that fail to parse. It could be injected.
+ continue
+ }
+ if err := checkResponse(id, q, msg.Header, msg.Questions); err != nil {
+ returnErr = errors.Join(returnErr, err)
+ continue
+ }
+ return &msg, nil
+ }
+}
+
+// queryStream implements a DNS query over a stream protocol. It frames the messages by prepending them with a 2-byte length prefix.
+func queryStream(conn io.ReadWriter, q dnsmessage.Question) (*dnsmessage.Message, error) {
+ // Reference: https://cs.opensource.google/go/go/+/master:src/net/dnsclient_unix.go?q=func:dnsStreamRoundTrip&ss=go%2Fgo
+ id := uint16(rand.Uint32())
+ buf, err := appendRequest(id, q, make([]byte, 2, 514))
+ if err != nil {
+ return nil, &nestedError{ErrBadRequest, fmt.Errorf("append request failed: %w", err)}
+ }
+ // Buffer length must fit in a uint16.
+ if len(buf) > 1<<16-1 {
+ return nil, &nestedError{ErrBadRequest, fmt.Errorf("message too large: %v bytes", len(buf))}
+ }
+ binary.BigEndian.PutUint16(buf[:2], uint16(len(buf)-2))
+
+ // TODO: Consider writer.ReadFrom(net.Buffers) in case the writer is a TCPConn.
+ if _, err := conn.Write(buf); err != nil {
+ return nil, &nestedError{ErrSend, err}
+ }
+
+ var msgLen uint16
+ if err := binary.Read(conn, binary.BigEndian, &msgLen); err != nil {
+ return nil, &nestedError{ErrReceive, fmt.Errorf("read message length failed: %w", err)}
+ }
+ if int(msgLen) <= cap(buf) {
+ buf = buf[:msgLen]
+ } else {
+ buf = make([]byte, msgLen)
+ }
+ if _, err = io.ReadFull(conn, buf); err != nil {
+ return nil, &nestedError{ErrReceive, fmt.Errorf("read message failed: %w", err)}
+ }
+
+ var msg dnsmessage.Message
+ if err = msg.Unpack(buf); err != nil {
+ return nil, &nestedError{ErrBadResponse, fmt.Errorf("response failed to unpack: %w", err)}
+ }
+ if err := checkResponse(id, q, msg.Header, msg.Questions); err != nil {
+ return nil, &nestedError{ErrBadResponse, err}
+ }
+ return &msg, nil
+}
+
+func ensurePort(address string, defaultPort string) string {
+ host, port, err := net.SplitHostPort(address)
+ if err != nil {
+ // Failed to parse as host:port. Assume address is a host.
+ return net.JoinHostPort(address, defaultPort)
+ }
+ if port == "" {
+ return net.JoinHostPort(host, defaultPort)
+ }
+ return address
+}
+
+// NewUDPResolver creates a [Resolver] that implements the DNS-over-UDP protocol, using a [transport.PacketDialer] for transport.
+// It uses a different port for every request.
+//
+// [DNS-over-UDP]: https://datatracker.ietf.org/doc/html/rfc1035#section-4.2.1
+func NewUDPResolver(pd transport.PacketDialer, resolverAddr string) Resolver {
+ resolverAddr = ensurePort(resolverAddr, "53")
+ return FuncResolver(func(ctx context.Context, q dnsmessage.Question) (*dnsmessage.Message, error) {
+ conn, err := pd.DialPacket(ctx, resolverAddr)
+ if err != nil {
+ return nil, &nestedError{ErrDial, err}
+ }
+ defer conn.Close()
+ if deadline, ok := ctx.Deadline(); ok {
+ conn.SetDeadline(deadline)
+ }
+ return queryDatagram(conn, q)
+ })
+}
+
+type streamResolver struct {
+ NewConn func(context.Context) (transport.StreamConn, error)
+}
+
+func (r *streamResolver) Query(ctx context.Context, q dnsmessage.Question) (*dnsmessage.Message, error) {
+ conn, err := r.NewConn(ctx)
+ if err != nil {
+ return nil, &nestedError{ErrDial, err}
+ }
+ // TODO: reuse connection, as per https://datatracker.ietf.org/doc/html/rfc7766#section-6.2.1.
+ defer conn.Close()
+ if deadline, ok := ctx.Deadline(); ok {
+ conn.SetDeadline(deadline)
+ }
+ return queryStream(conn, q)
+}
+
+// NewTCPResolver creates a [Resolver] that implements the [DNS-over-TCP] protocol, using a [transport.StreamDialer] for transport.
+// It creates a new connection to the resolver for every request.
+//
+// [DNS-over-TCP]: https://datatracker.ietf.org/doc/html/rfc1035#section-4.2.2
+func NewTCPResolver(sd transport.StreamDialer, resolverAddr string) Resolver {
+ // TODO: Consider handling Authenticated Data.
+ resolverAddr = ensurePort(resolverAddr, "53")
+ return &streamResolver{
+ NewConn: func(ctx context.Context) (transport.StreamConn, error) {
+ return sd.DialStream(ctx, resolverAddr)
+ },
+ }
+}
+
+// NewTLSResolver creates a [Resolver] that implements the [DNS-over-TLS] protocol, using a [transport.StreamDialer]
+// to connect to the resolverAddr, and the resolverName as the TLS server name.
+// It creates a new connection to the resolver for every request.
+//
+// [DNS-over-TLS]: https://datatracker.ietf.org/doc/html/rfc7858
+func NewTLSResolver(sd transport.StreamDialer, resolverAddr string, resolverName string) Resolver {
+ resolverAddr = ensurePort(resolverAddr, "853")
+ return &streamResolver{
+ NewConn: func(ctx context.Context) (transport.StreamConn, error) {
+ baseConn, err := sd.DialStream(ctx, resolverAddr)
+ if err != nil {
+ return nil, err
+ }
+ return tls.WrapConn(ctx, baseConn, resolverName)
+ },
+ }
+}
+
+// NewHTTPSResolver creates a [Resolver] that implements the [DNS-over-HTTPS] protocol, using a [transport.StreamDialer]
+// to connect to the resolverAddr, and the url as the DoH template URI.
+// It uses an internal HTTP client that reuses connections when possible.
+//
+// [DNS-over-HTTPS]: https://datatracker.ietf.org/doc/html/rfc8484
+func NewHTTPSResolver(sd transport.StreamDialer, resolverAddr string, url string) Resolver {
+ resolverAddr = ensurePort(resolverAddr, "443")
+ dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
+ if !strings.HasPrefix(network, "tcp") {
+ // TODO: Support UDP for QUIC.
+ return nil, fmt.Errorf("protocol not supported: %v", network)
+ }
+ conn, err := sd.DialStream(ctx, resolverAddr)
+ if err != nil {
+ return nil, &nestedError{ErrDial, err}
+ }
+ return conn, nil
+ }
+ // TODO: add mechanism to close idle connections.
+ // Copied from Intra: https://github.com/Jigsaw-Code/Intra/blob/d3554846a1146ae695e28a8ed6dd07f0cd310c5a/Android/tun2socks/intra/doh/doh.go#L213-L219
+ httpClient := http.Client{
+ Transport: &http.Transport{
+ DialContext: dialContext,
+ ForceAttemptHTTP2: true,
+ TLSHandshakeTimeout: 10 * time.Second,
+ ResponseHeaderTimeout: 20 * time.Second, // Same value as Android DNS-over-TLS
+ },
+ }
+ return FuncResolver(func(ctx context.Context, q dnsmessage.Question) (*dnsmessage.Message, error) {
+ // Prepare request.
+ buf, err := appendRequest(0, q, make([]byte, 0, 512))
+ if err != nil {
+ return nil, &nestedError{ErrBadRequest, fmt.Errorf("append request failed: %w", err)}
+ }
+ httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(buf))
+ if err != nil {
+ return nil, &nestedError{ErrBadRequest, fmt.Errorf("create HTTP request failed: %w", err)}
+ }
+ const mimetype = "application/dns-message"
+ httpReq.Header.Add("Accept", mimetype)
+ httpReq.Header.Add("Content-Type", mimetype)
+
+ // Send request and get response.
+ httpResp, err := httpClient.Do(httpReq)
+ if err != nil {
+ return nil, &nestedError{ErrReceive, fmt.Errorf("failed to get HTTP response: %w", err)}
+ }
+ defer httpResp.Body.Close()
+ if httpResp.StatusCode != http.StatusOK {
+ return nil, &nestedError{ErrReceive, fmt.Errorf("got HTTP status %v", httpResp.StatusCode)}
+ }
+ response, err := io.ReadAll(httpResp.Body)
+ if err != nil {
+ return nil, &nestedError{ErrReceive, fmt.Errorf("failed to read response: %w", err)}
+ }
+
+ // Process response.
+ var msg dnsmessage.Message
+ if err = msg.Unpack(response); err != nil {
+ return nil, &nestedError{ErrBadResponse, fmt.Errorf("failed to unpack DNS response: %w", err)}
+ }
+ if err := checkResponse(0, q, msg.Header, msg.Questions); err != nil {
+ return nil, &nestedError{ErrBadResponse, err}
+ }
+ return &msg, nil
+ })
+}
diff --git a/dns/resolver_net_test.go b/dns/resolver_net_test.go
new file mode 100644
index 00000000..bc452aef
--- /dev/null
+++ b/dns/resolver_net_test.go
@@ -0,0 +1,76 @@
+// Copyright 2023 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// go:build nettest
+
+package dns
+
+import (
+ "context"
+ "testing"
+
+ "github.com/Jigsaw-Code/outline-sdk/transport"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/net/dns/dnsmessage"
+)
+
+// TODO: Make tests not depend on the network.
+func newTestContext(t *testing.T) context.Context {
+ if deadline, ok := t.Deadline(); ok {
+ ctx, cancel := context.WithDeadline(context.Background(), deadline)
+ t.Cleanup(cancel)
+ return ctx
+ }
+ return context.Background()
+}
+
+func TestNewUDPResolver(t *testing.T) {
+ ctx := newTestContext(t)
+ resolver := NewUDPResolver(&transport.UDPDialer{}, "8.8.8.8")
+ q, err := NewQuestion("getoutline.org.", dnsmessage.TypeAAAA)
+ require.NoError(t, err)
+ resp, err := resolver.Query(ctx, *q)
+ require.NoError(t, err)
+ require.GreaterOrEqual(t, len(resp.Answers), 1)
+}
+
+func TestNewTCPResolver(t *testing.T) {
+ ctx := newTestContext(t)
+ resolver := NewTCPResolver(&transport.TCPDialer{}, "8.8.8.8")
+ q, err := NewQuestion("getoutline.org.", dnsmessage.TypeAAAA)
+ require.NoError(t, err)
+ resp, err := resolver.Query(ctx, *q)
+ require.NoError(t, err)
+ require.GreaterOrEqual(t, len(resp.Answers), 1)
+}
+
+func TestNewTLSResolver(t *testing.T) {
+ ctx := newTestContext(t)
+ resolver := NewTLSResolver(&transport.TCPDialer{}, "8.8.8.8", "8.8.8.8")
+ q, err := NewQuestion("getoutline.org.", dnsmessage.TypeAAAA)
+ require.NoError(t, err)
+ resp, err := resolver.Query(ctx, *q)
+ require.NoError(t, err)
+ require.GreaterOrEqual(t, len(resp.Answers), 1)
+}
+
+func TestNewHTTPSResolver(t *testing.T) {
+ ctx := newTestContext(t)
+ resolver := NewHTTPSResolver(&transport.TCPDialer{}, "8.8.8.8", "https://8.8.8.8/dns-query")
+ q, err := NewQuestion("getoutline.org.", dnsmessage.TypeAAAA)
+ require.NoError(t, err)
+ resp, err := resolver.Query(ctx, *q)
+ require.NoError(t, err)
+ require.GreaterOrEqual(t, len(resp.Answers), 1)
+}
diff --git a/dns/resolver_test.go b/dns/resolver_test.go
new file mode 100644
index 00000000..3943d0b5
--- /dev/null
+++ b/dns/resolver_test.go
@@ -0,0 +1,455 @@
+// Copyright 2023 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dns
+
+import (
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "math/rand"
+ "net"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "golang.org/x/net/dns/dnsmessage"
+)
+
+func TestNewQuestionTypes(t *testing.T) {
+ testDomain := "example.com."
+ qname, err := dnsmessage.NewName(testDomain)
+ require.NoError(t, err)
+ for _, qtype := range []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA, dnsmessage.TypeCNAME} {
+ t.Run(qtype.String(), func(t *testing.T) {
+ q, err := NewQuestion(testDomain, qtype)
+ require.NoError(t, err)
+ require.Equal(t, qname, q.Name)
+ require.Equal(t, qtype, q.Type)
+ require.Equal(t, dnsmessage.ClassINET, q.Class)
+ })
+ }
+}
+
+func TestNewQuestionNotFQDN(t *testing.T) {
+ testDomain := "example.com"
+ q, err := NewQuestion(testDomain, dnsmessage.TypeAAAA)
+ require.NoError(t, err)
+ require.Equal(t, dnsmessage.MustNewName("example.com."), q.Name)
+}
+
+func TestNewQuestionRoot(t *testing.T) {
+ testDomain := "."
+ qname, err := dnsmessage.NewName(testDomain)
+ require.NoError(t, err)
+ q, err := NewQuestion(testDomain, dnsmessage.TypeAAAA)
+ require.NoError(t, err)
+ require.Equal(t, qname, q.Name)
+}
+
+func TestNewQuestionEmpty(t *testing.T) {
+ testDomain := ""
+ q, err := NewQuestion(testDomain, dnsmessage.TypeAAAA)
+ require.NoError(t, err)
+ require.Equal(t, dnsmessage.MustNewName("."), q.Name)
+}
+
+func TestNewQuestionLongName(t *testing.T) {
+ testDomain := strings.Repeat("a.", 200)
+ _, err := NewQuestion(testDomain, dnsmessage.TypeAAAA)
+ require.Error(t, err)
+}
+
+func Test_appendRequest(t *testing.T) {
+ q, err := NewQuestion(".", dnsmessage.TypeAAAA)
+ require.NoError(t, err)
+
+ id := uint16(1234)
+ offset := 2
+ buf, err := appendRequest(id, *q, make([]byte, offset))
+ require.NoError(t, err)
+ require.Equal(t, make([]byte, offset), buf[:offset])
+
+ // offset + 12 bytes header + 5 question + 11 EDNS(0) OPT RR
+ require.Equal(t, offset+28, len(buf))
+
+ require.Equal(t, id, binary.BigEndian.Uint16(buf[offset:]))
+
+ var request dnsmessage.Message
+ err = request.Unpack(buf[offset:])
+ require.NoError(t, err)
+ require.Equal(t, id, request.ID)
+ require.Equal(t, 1, len(request.Questions))
+ require.Equal(t, *q, request.Questions[0])
+ require.Equal(t, 0, len(request.Answers))
+ require.Equal(t, 0, len(request.Authorities))
+ // ENDS(0) OPT resource record.
+ require.Equal(t, 1, len(request.Additionals))
+ // As per https://datatracker.ietf.org/doc/html/rfc6891#section-6.1.2
+ optRR := dnsmessage.Resource{
+ Header: dnsmessage.ResourceHeader{
+ Name: dnsmessage.MustNewName("."),
+ Type: dnsmessage.TypeOPT,
+ Class: maxUDPMessageSize,
+ TTL: 0,
+ Length: 0,
+ },
+ Body: &dnsmessage.OPTResource{},
+ }
+ require.Equal(t, optRR, request.Additionals[0])
+}
+
+func Test_foldCase(t *testing.T) {
+ require.Equal(t, byte('Y'), foldCase('Y'))
+ require.Equal(t, byte('Y'), foldCase('y'))
+ // Only fold ASCII
+ require.Equal(t, byte('ý'), foldCase('ý'))
+ require.Equal(t, byte('-'), foldCase('-'))
+}
+
+func Test_equalASCIIName(t *testing.T) {
+ require.True(t, equalASCIIName(dnsmessage.MustNewName("My-Example.Com"), dnsmessage.MustNewName("mY-eXAMPLE.cOM")))
+ require.False(t, equalASCIIName(dnsmessage.MustNewName("example.com"), dnsmessage.MustNewName("example.net")))
+ require.False(t, equalASCIIName(dnsmessage.MustNewName("example.com"), dnsmessage.MustNewName("example.com.br")))
+ require.False(t, equalASCIIName(dnsmessage.MustNewName("example.com"), dnsmessage.MustNewName("myexample.com")))
+}
+
+func Test_checkResponse(t *testing.T) {
+ reqID := uint16(rand.Uint32())
+ reqQ := dnsmessage.Question{
+ Name: dnsmessage.MustNewName("example.com."),
+ Type: dnsmessage.TypeAAAA,
+ Class: dnsmessage.ClassINET,
+ }
+ expectedHdr := dnsmessage.Header{ID: reqID, Response: true}
+ expectedQs := []dnsmessage.Question{reqQ}
+ t.Run("Match", func(t *testing.T) {
+ err := checkResponse(reqID, reqQ, expectedHdr, expectedQs)
+ require.NoError(t, err)
+ })
+ t.Run("CaseInsensitive", func(t *testing.T) {
+ mixedQ := reqQ
+ mixedQ.Name = dnsmessage.MustNewName("Example.Com.")
+ err := checkResponse(reqID, reqQ, expectedHdr, []dnsmessage.Question{mixedQ})
+ require.NoError(t, err)
+ })
+ t.Run("NotResponse", func(t *testing.T) {
+ badHdr := expectedHdr
+ badHdr.Response = false
+ err := checkResponse(reqID, reqQ, badHdr, expectedQs)
+ require.Error(t, err)
+ })
+ t.Run("BadID", func(t *testing.T) {
+ badHdr := expectedHdr
+ badHdr.ID = reqID + 1
+ err := checkResponse(reqID, reqQ, badHdr, expectedQs)
+ require.Error(t, err)
+ })
+ t.Run("NoQuestions", func(t *testing.T) {
+ err := checkResponse(reqID, reqQ, expectedHdr, []dnsmessage.Question{})
+ require.Error(t, err)
+ })
+ t.Run("BadQuestionType", func(t *testing.T) {
+ badQ := reqQ
+ badQ.Type = dnsmessage.TypeA
+ err := checkResponse(reqID, reqQ, expectedHdr, []dnsmessage.Question{badQ})
+ require.Error(t, err)
+ })
+ t.Run("BadQuestionClass", func(t *testing.T) {
+ badQ := reqQ
+ badQ.Class = dnsmessage.ClassCHAOS
+ err := checkResponse(reqID, reqQ, expectedHdr, []dnsmessage.Question{badQ})
+ require.Error(t, err)
+ })
+ t.Run("BadQuestionName", func(t *testing.T) {
+ badQ := reqQ
+ badQ.Name = dnsmessage.MustNewName("notexample.invalid.")
+ err := checkResponse(reqID, reqQ, expectedHdr, []dnsmessage.Question{badQ})
+ require.Error(t, err)
+ })
+}
+
+func newMessageResponse(req dnsmessage.Message, answer dnsmessage.ResourceBody, ttl uint32) (dnsmessage.Message, error) {
+ var resp dnsmessage.Message
+ if len(req.Questions) != 1 {
+ return resp, fmt.Errorf("Invalid number of questions %v", len(req.Questions))
+ }
+ q := req.Questions[0]
+ resp.ID = req.ID
+ resp.Header.Response = true
+ resp.Questions = []dnsmessage.Question{q}
+ resp.Answers = []dnsmessage.Resource{{
+ Header: dnsmessage.ResourceHeader{Name: q.Name, Type: q.Type, Class: q.Class, TTL: ttl},
+ Body: answer,
+ }}
+ resp.Authorities = []dnsmessage.Resource{}
+ resp.Additionals = []dnsmessage.Resource{}
+ return resp, nil
+}
+
+type queryResult struct {
+ msg *dnsmessage.Message
+ err error
+}
+
+func testDatagramExchange(t *testing.T, server func(request dnsmessage.Message, conn net.Conn)) (*dnsmessage.Message, error) {
+ front, back := net.Pipe()
+ q, err := NewQuestion("example.com.", dnsmessage.TypeAAAA)
+ require.NoError(t, err)
+ clientDone := make(chan queryResult)
+ go func() {
+ msg, err := queryDatagram(front, *q)
+ clientDone <- queryResult{msg, err}
+ }()
+ // Read request.
+ buf := make([]byte, 512)
+ n, err := back.Read(buf)
+ require.NoError(t, err)
+ buf = buf[:n]
+ // Verify request.
+ var reqMsg dnsmessage.Message
+ reqMsg.Unpack(buf)
+ reqID := reqMsg.ID
+ expectedBuf, err := appendRequest(reqID, *q, make([]byte, 0, 512))
+ require.NoError(t, err)
+ require.Equal(t, expectedBuf, buf)
+
+ server(reqMsg, back)
+
+ result := <-clientDone
+ return result.msg, result.err
+}
+
+func Test_queryDatagram(t *testing.T) {
+ t.Run("Success", func(t *testing.T) {
+ var respSent dnsmessage.Message
+ respRcvd, err := testDatagramExchange(t, func(req dnsmessage.Message, conn net.Conn) {
+ // Send bogus response.
+ _, err := conn.Write([]byte{0, 0})
+ require.NoError(t, err)
+
+ // Prepare response message.
+ respSent, err = newMessageResponse(req, &dnsmessage.AAAAResource{AAAA: [16]byte(net.IPv6loopback)}, 100)
+ require.NoError(t, err)
+
+ // Send message with invalid ID first.
+ badMsg := respSent
+ badMsg.ID = req.ID + 1
+ buf, err := (&badMsg).Pack()
+ require.NoError(t, err)
+ _, err = conn.Write(buf)
+ require.NoError(t, err)
+
+ // Send valid response.
+ buf, err = (&respSent).Pack()
+ require.NoError(t, err)
+ _, err = conn.Write(buf)
+ require.NoError(t, err)
+ })
+ require.NoError(t, err)
+ require.NotNil(t, respRcvd)
+ require.Equal(t, respSent, *respRcvd)
+ })
+ t.Run("BadResponse", func(t *testing.T) {
+ _, err := testDatagramExchange(t, func(req dnsmessage.Message, conn net.Conn) {
+ // Send bad response.
+ _, err := conn.Write([]byte{0})
+ require.NoError(t, err)
+ // Close writer.
+ conn.Close()
+ })
+ require.ErrorIs(t, err, ErrReceive)
+ require.Equal(t, 2, len(errors.Unwrap(err).(interface{ Unwrap() []error }).Unwrap()))
+ require.ErrorIs(t, err, io.EOF)
+ })
+ t.Run("FailedClientWrite", func(t *testing.T) {
+ front, back := net.Pipe()
+ back.Close()
+ q, err := NewQuestion("example.com.", dnsmessage.TypeAAAA)
+ require.NoError(t, err)
+ clientDone := make(chan queryResult)
+ go func() {
+ msg, err := queryDatagram(front, *q)
+ clientDone <- queryResult{msg, err}
+ }()
+ // Wait for queryDatagram.
+ result := <-clientDone
+ require.ErrorIs(t, result.err, ErrSend)
+ require.ErrorIs(t, result.err, io.ErrClosedPipe)
+ })
+ t.Run("FailedClientRead", func(t *testing.T) {
+ front, back := net.Pipe()
+ q, err := NewQuestion("example.com.", dnsmessage.TypeAAAA)
+ require.NoError(t, err)
+ clientDone := make(chan queryResult)
+ go func() {
+ msg, err := queryDatagram(front, *q)
+ clientDone <- queryResult{msg, err}
+ }()
+ back.Read(make([]byte, 521))
+ back.Close()
+ // Wait for queryDatagram.
+ result := <-clientDone
+ require.ErrorIs(t, result.err, ErrReceive)
+ require.ErrorIs(t, result.err, io.EOF)
+ })
+}
+
+func testStreamExchange(t *testing.T, server func(request dnsmessage.Message, conn net.Conn)) (*dnsmessage.Message, error) {
+ front, back := net.Pipe()
+ q, err := NewQuestion("example.com.", dnsmessage.TypeAAAA)
+ require.NoError(t, err)
+ clientDone := make(chan queryResult)
+ go func() {
+ msg, err := queryStream(front, *q)
+ clientDone <- queryResult{msg, err}
+ }()
+ // Read request.
+ var msgLen uint16
+ require.NoError(t, binary.Read(back, binary.BigEndian, &msgLen))
+ buf := make([]byte, msgLen)
+ n, err := back.Read(buf)
+ require.NoError(t, err)
+ buf = buf[:n]
+ // Verify request.
+ var reqMsg dnsmessage.Message
+ reqMsg.Unpack(buf)
+ reqID := reqMsg.ID
+ expectedBuf, err := appendRequest(reqID, *q, make([]byte, 0, 512))
+ require.NoError(t, err)
+ require.Equal(t, expectedBuf, buf)
+
+ server(reqMsg, back)
+
+ result := <-clientDone
+ return result.msg, result.err
+}
+
+func Test_queryStream(t *testing.T) {
+ t.Run("Success", func(t *testing.T) {
+ var respSent dnsmessage.Message
+ respRcvd, err := testStreamExchange(t, func(req dnsmessage.Message, conn net.Conn) {
+ var err error
+ // Prepare response message.
+ respSent, err = newMessageResponse(req, &dnsmessage.AAAAResource{AAAA: [16]byte(net.IPv6loopback)}, 100)
+ require.NoError(t, err)
+
+ // Send response.
+ buf, err := (&respSent).Pack()
+ require.NoError(t, err)
+ require.NoError(t, binary.Write(conn, binary.BigEndian, uint16(len(buf))))
+ _, err = conn.Write(buf)
+ require.NoError(t, err)
+ })
+ require.NoError(t, err)
+ require.NotNil(t, respRcvd)
+ require.Equal(t, respSent, *respRcvd)
+ })
+ t.Run("ShortRead", func(t *testing.T) {
+ _, err := testStreamExchange(t, func(req dnsmessage.Message, conn net.Conn) {
+ // Send response.
+ _, err := conn.Write([]byte{0})
+ require.NoError(t, err)
+
+ // Close writer.
+ conn.Close()
+ })
+ require.ErrorIs(t, err, ErrReceive)
+ require.ErrorIs(t, err, io.ErrUnexpectedEOF)
+ })
+ t.Run("ShortMessage", func(t *testing.T) {
+ _, err := testStreamExchange(t, func(req dnsmessage.Message, conn net.Conn) {
+ // Send response.
+ _, err := conn.Write([]byte{0, 100, 0})
+ require.NoError(t, err)
+ // Close writer.
+ conn.Close()
+ })
+ require.ErrorIs(t, err, ErrReceive)
+ require.ErrorIs(t, err, io.ErrUnexpectedEOF)
+ })
+ t.Run("BadMessageFormat", func(t *testing.T) {
+ _, err := testStreamExchange(t, func(req dnsmessage.Message, conn net.Conn) {
+ // Send response.
+ _, err := conn.Write([]byte{0, 2, 0, 0})
+ require.NoError(t, err)
+
+ // Close writer.
+ conn.Close()
+ })
+ require.ErrorIs(t, err, ErrBadResponse)
+ })
+ t.Run("BadMessageContent", func(t *testing.T) {
+ _, err := testStreamExchange(t, func(req dnsmessage.Message, conn net.Conn) {
+ // Make response with no answer and invalid ID.
+ resp := req
+ resp.ID = req.ID + 1
+ resp.Response = true
+ buf, err := resp.AppendPack(make([]byte, 2, 514))
+ require.NoError(t, err)
+ binary.BigEndian.PutUint16(buf, uint16(len(buf)-2))
+ // Send response.
+ _, err = conn.Write(buf)
+ require.NoError(t, err)
+
+ // Close writer.
+ conn.Close()
+ })
+ require.ErrorIs(t, err, ErrBadResponse)
+ })
+ t.Run("FailedClientWrite", func(t *testing.T) {
+ front, back := net.Pipe()
+ back.Close()
+ q, err := NewQuestion("example.com.", dnsmessage.TypeAAAA)
+ require.NoError(t, err)
+ clientDone := make(chan queryResult)
+ go func() {
+ msg, err := queryStream(front, *q)
+ clientDone <- queryResult{msg, err}
+ }()
+ // Wait for client.
+ result := <-clientDone
+ require.ErrorIs(t, result.err, ErrSend)
+ require.ErrorIs(t, result.err, io.ErrClosedPipe)
+ })
+ t.Run("FailedClientRead", func(t *testing.T) {
+ front, back := net.Pipe()
+ q, err := NewQuestion("example.com.", dnsmessage.TypeAAAA)
+ require.NoError(t, err)
+ clientDone := make(chan queryResult)
+ go func() {
+ msg, err := queryStream(front, *q)
+ clientDone <- queryResult{msg, err}
+ }()
+ back.Read(make([]byte, 521))
+ back.Close()
+ // Wait for queryDatagram.
+ result := <-clientDone
+ require.ErrorIs(t, result.err, ErrReceive)
+ require.ErrorIs(t, result.err, io.EOF)
+ })
+}
+
+func Test_ensurePort(t *testing.T) {
+ require.Equal(t, "example.com:8080", ensurePort("example.com:8080", "80"))
+ require.Equal(t, "example.com:443", ensurePort("example.com", "443"))
+ require.Equal(t, "example.com:443", ensurePort("example.com:", "443"))
+ require.Equal(t, "8.8.8.8:8080", ensurePort("8.8.8.8:8080", "443"))
+ require.Equal(t, "8.8.8.8:443", ensurePort("8.8.8.8", "443"))
+ require.Equal(t, "8.8.8.8:443", ensurePort("8.8.8.8:", "443"))
+ require.Equal(t, "[2001:4860:4860::8888]:8080", ensurePort("[2001:4860:4860::8888]:8080", "443"))
+ require.Equal(t, "[2001:4860:4860::8888]:443", ensurePort("2001:4860:4860::8888", "443"))
+ require.Equal(t, "[2001:4860:4860::8888]:443", ensurePort("[2001:4860:4860::8888]:", "443"))
+}
diff --git a/go.mod b/go.mod
index fff8d13b..5ff0f143 100644
--- a/go.mod
+++ b/go.mod
@@ -7,15 +7,17 @@ require (
github.com/google/gopacket v1.1.19
github.com/shadowsocks/go-shadowsocks2 v0.1.5
github.com/stretchr/testify v1.8.2
- golang.org/x/crypto v0.7.0
+ golang.org/x/crypto v0.17.0
+ golang.org/x/net v0.19.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.1.0 // indirect
+ github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
- golang.org/x/sys v0.6.0 // indirect
+ golang.org/x/sys v0.15.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index db499008..f1f3f92a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,4 @@
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -8,8 +9,9 @@ github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
@@ -27,19 +29,21 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
-golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
-golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
diff --git a/network/lwip2transport/device_test.go b/network/lwip2transport/device_test.go
index 39a73fa3..155ed8f2 100644
--- a/network/lwip2transport/device_test.go
+++ b/network/lwip2transport/device_test.go
@@ -54,7 +54,7 @@ type errTcpUdpHandler struct {
err error
}
-func (h *errTcpUdpHandler) Dial(context.Context, string) (transport.StreamConn, error) {
+func (h *errTcpUdpHandler) DialStream(context.Context, string) (transport.StreamConn, error) {
return nil, h.err
}
diff --git a/network/lwip2transport/tcp.go b/network/lwip2transport/tcp.go
index 0eb30043..ee856c04 100644
--- a/network/lwip2transport/tcp.go
+++ b/network/lwip2transport/tcp.go
@@ -36,7 +36,7 @@ func newTCPHandler(client transport.StreamDialer) *tcpHandler {
}
func (h *tcpHandler) Handle(conn net.Conn, target *net.TCPAddr) error {
- proxyConn, err := h.dialer.Dial(context.Background(), target.String())
+ proxyConn, err := h.dialer.DialStream(context.Background(), target.String())
if err != nil {
return err
}
diff --git a/network/packet_listener_proxy_test.go b/network/packet_listener_proxy_test.go
index cd7e0d78..b27e8f5e 100644
--- a/network/packet_listener_proxy_test.go
+++ b/network/packet_listener_proxy_test.go
@@ -23,7 +23,7 @@ import (
)
func TestWithWriteTimeoutOptionWorks(t *testing.T) {
- pl := &transport.UDPPacketListener{}
+ pl := &transport.UDPListener{}
defProxy, err := NewPacketProxyFromPacketListener(pl)
require.NoError(t, err)
diff --git a/transport/doc.go b/transport/doc.go
new file mode 100644
index 00000000..f6dc74a8
--- /dev/null
+++ b/transport/doc.go
@@ -0,0 +1,47 @@
+// Copyright 2024 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/*
+Package transport has the core types to work with transport layer connections.
+
+# Connections
+
+Connections enable communication between two endpoints over an abstract transport. There are two types of connections:
+
+ - Stream connections, like TCP and the SOCK_STREAM Posix socket type. They are represented by [StreamConn] objects.
+ - Datagram connections, like UDP and the SOCK_DGRAM Posix socket type. They are represented by [net.Conn] objects.
+
+We use "Packet" instead of "Datagram" in the method and type names related to datagrams because that is the convention in the Go standard library.
+
+Each write and read on datagram connections represent a single datagram, while reads and writes on stream connections operate on byte sequences
+that may be independent of how those bytes are packaged.
+
+Stream connections offer CloseRead and CloseWrite methods, which allows for a half-closed state (like TCP).
+In general, you communicate end of data ("EOF") to the other side of the connection by calling CloseWrite (TCP will send a FIN).
+CloseRead doesn't generate packets, but it allows for releasing resources (e.g. a read loop) and to signal errors to the peer
+if more data does arrive (TCP will usually send a RST).
+
+Connections can be wrapped to create nested connections over a new transport. For example, a StreamConn could be over TCP,
+over TLS over TCP, over HTTP over TLS over TCP, over QUIC, among other options.
+
+# Dialers
+
+Dialers enable the creation of connections given a host:port address while encapsulating the underlying transport or proxy protocol.
+The [StreamDialer] and [PacketDialer] types create stream ([StreamConn]) and datagram ([net.Conn]) connections, respectively, given an address.
+
+Dialers can also be nested. For example, a TLS Stream Dialer can use a TCP dialer to create a StreamConn backed by a TCP connection,
+then create a TLS StreamConn backed by the TCP StreamConn. A SOCKS5-over-TLS Dialer could use the TLS Dialer to create the TLS StreamConn
+to the proxy before doing the SOCKS5 connection to the target address.
+*/
+package transport
diff --git a/transport/packet.go b/transport/packet.go
index cbdc4dca..bac37b3d 100644
--- a/transport/packet.go
+++ b/transport/packet.go
@@ -22,8 +22,8 @@ import (
// PacketEndpoint represents an endpoint that can be used to establish packet connections (like UDP) to a fixed destination.
type PacketEndpoint interface {
- // Connect creates a connection bound to an endpoint, returning the connection.
- Connect(ctx context.Context) (net.Conn, error)
+ // ConnectPacket creates a connection bound to an endpoint, returning the connection.
+ ConnectPacket(ctx context.Context) (net.Conn, error)
}
// UDPEndpoint is a [PacketEndpoint] that connects to the specified address using UDP.
@@ -37,11 +37,21 @@ type UDPEndpoint struct {
var _ PacketEndpoint = (*UDPEndpoint)(nil)
-// Connect implements [PacketEndpoint].Connect.
-func (e UDPEndpoint) Connect(ctx context.Context) (net.Conn, error) {
+// ConnectPacket implements [PacketEndpoint].ConnectPacket.
+func (e UDPEndpoint) ConnectPacket(ctx context.Context) (net.Conn, error) {
return e.Dialer.DialContext(ctx, "udp", e.Address)
}
+// FuncPacketEndpoint is a [PacketEndpoint] that uses the given function to connect.
+type FuncPacketEndpoint func(ctx context.Context) (net.Conn, error)
+
+var _ PacketEndpoint = (*FuncPacketEndpoint)(nil)
+
+// ConnectPacket implements the [PacketEndpoint] interface.
+func (f FuncPacketEndpoint) ConnectPacket(ctx context.Context) (net.Conn, error) {
+ return f(ctx)
+}
+
// PacketDialerEndpoint is a [PacketEndpoint] that connects to the given address using the specified [PacketDialer].
type PacketDialerEndpoint struct {
Dialer PacketDialer
@@ -50,28 +60,28 @@ type PacketDialerEndpoint struct {
var _ PacketEndpoint = (*PacketDialerEndpoint)(nil)
-// Connect implements [PacketEndpoint].Connect.
-func (e *PacketDialerEndpoint) Connect(ctx context.Context) (net.Conn, error) {
- return e.Dialer.Dial(ctx, e.Address)
+// ConnectPacket implements [PacketEndpoint].ConnectPacket.
+func (e *PacketDialerEndpoint) ConnectPacket(ctx context.Context) (net.Conn, error) {
+ return e.Dialer.DialPacket(ctx, e.Address)
}
// PacketDialer provides a way to dial a destination and establish datagram connections.
type PacketDialer interface {
- // Dial connects to `addr`.
+ // DialPacket connects to `addr`.
// `addr` has the form "host:port", where "host" can be a domain name or IP address.
- Dial(ctx context.Context, addr string) (net.Conn, error)
+ DialPacket(ctx context.Context, addr string) (net.Conn, error)
}
-// UDPPacketDialer is a [PacketDialer] that uses the standard [net.Dialer] to dial.
+// UDPDialer is a [PacketDialer] that uses the standard [net.Dialer] to dial.
// It provides a convenient way to use a [net.Dialer] when you need a [PacketDialer].
-type UDPPacketDialer struct {
+type UDPDialer struct {
Dialer net.Dialer
}
-var _ PacketDialer = (*UDPPacketDialer)(nil)
+var _ PacketDialer = (*UDPDialer)(nil)
-// Dial implements [PacketDialer].Dial.
-func (d *UDPPacketDialer) Dial(ctx context.Context, addr string) (net.Conn, error) {
+// DialPacket implements [PacketDialer].DialPacket.
+func (d *UDPDialer) DialPacket(ctx context.Context, addr string) (net.Conn, error) {
return d.Dialer.DialContext(ctx, "udp", addr)
}
@@ -90,12 +100,12 @@ type boundPacketConn struct {
var _ net.Conn = (*boundPacketConn)(nil)
-// Dial implements [PacketDialer].Dial.
+// DialPacket implements [PacketDialer].DialPacket.
// The address is in "host:port" format and the host must be either a full IP address (not "[::]") or a domain.
// The address must be supported by the WriteTo call of the [net.PacketConn] returned by the [PacketListener].
// For example, a [net.UDPConn] only supports IP addresses, not domain names.
// If the host is a domain name, consider pre-resolving it to avoid resolution calls.
-func (e PacketListenerDialer) Dial(ctx context.Context, address string) (net.Conn, error) {
+func (e PacketListenerDialer) DialPacket(ctx context.Context, address string) (net.Conn, error) {
packetConn, err := e.Listener.ListenPacket(ctx)
if err != nil {
return nil, fmt.Errorf("could not create PacketConn: %w", err)
@@ -142,16 +152,26 @@ type PacketListener interface {
ListenPacket(ctx context.Context) (net.PacketConn, error)
}
-// UDPPacketListener is a [PacketListener] that uses the standard [net.ListenConfig].ListenPacket to listen.
-type UDPPacketListener struct {
+// UDPListener is a [PacketListener] that uses the standard [net.ListenConfig].ListenPacket to listen.
+type UDPListener struct {
net.ListenConfig
// The local address to bind to, as specified in [net.ListenPacket].
Address string
}
-var _ PacketListener = (*UDPPacketListener)(nil)
+var _ PacketListener = (*UDPListener)(nil)
// ListenPacket implements [PacketListener].ListenPacket
-func (l UDPPacketListener) ListenPacket(ctx context.Context) (net.PacketConn, error) {
+func (l UDPListener) ListenPacket(ctx context.Context) (net.PacketConn, error) {
return l.ListenConfig.ListenPacket(ctx, "udp", l.Address)
}
+
+// FuncPacketDialer is a [PacketDialer] that uses the given function to dial.
+type FuncPacketDialer func(ctx context.Context, addr string) (net.Conn, error)
+
+var _ PacketDialer = (*FuncPacketDialer)(nil)
+
+// DialPacket implements the [PacketDialer] interface.
+func (f FuncPacketDialer) DialPacket(ctx context.Context, addr string) (net.Conn, error) {
+ return f(ctx, addr)
+}
diff --git a/transport/packet_test.go b/transport/packet_test.go
index 17fe4709..aeb65a4e 100644
--- a/transport/packet_test.go
+++ b/transport/packet_test.go
@@ -16,6 +16,7 @@ package transport
import (
"context"
+ "errors"
"net"
"sync"
"syscall"
@@ -35,7 +36,7 @@ func TestUDPEndpointIPv4(t *testing.T) {
require.Equal(t, serverAddr, address)
return nil
}
- conn, err := ep.Connect(context.Background())
+ conn, err := ep.ConnectPacket(context.Background())
require.NoError(t, err)
assert.Equal(t, "udp", conn.RemoteAddr().Network())
assert.Equal(t, serverAddr, conn.RemoteAddr().String())
@@ -49,7 +50,7 @@ func TestUDPEndpointIPv6(t *testing.T) {
require.Equal(t, serverAddr, address)
return nil
}
- conn, err := ep.Connect(context.Background())
+ conn, err := ep.ConnectPacket(context.Background())
require.NoError(t, err)
assert.Equal(t, "udp", conn.RemoteAddr().Network())
assert.Equal(t, serverAddr, conn.RemoteAddr().String())
@@ -63,16 +64,39 @@ func TestUDPEndpointDomain(t *testing.T) {
resolvedAddr = address
return nil
}
- conn, err := ep.Connect(context.Background())
+ conn, err := ep.ConnectPacket(context.Background())
require.NoError(t, err)
assert.Equal(t, "udp", conn.RemoteAddr().Network())
assert.Equal(t, resolvedAddr, conn.RemoteAddr().String())
}
+func TestFuncPacketEndpoint(t *testing.T) {
+ expectedConn := &fakeConn{}
+ expectedErr := errors.New("fake error")
+ endpoint := FuncPacketEndpoint(func(ctx context.Context) (net.Conn, error) {
+ return expectedConn, expectedErr
+ })
+ conn, err := endpoint.ConnectPacket(context.Background())
+ require.Equal(t, expectedConn, conn)
+ require.Equal(t, expectedErr, err)
+}
+
+func TestFuncPacketDialer(t *testing.T) {
+ expectedConn := &fakeConn{}
+ expectedErr := errors.New("fake error")
+ dialer := FuncPacketDialer(func(ctx context.Context, addr string) (net.Conn, error) {
+ require.Equal(t, "unused", addr)
+ return expectedConn, expectedErr
+ })
+ conn, err := dialer.DialPacket(context.Background(), "unused")
+ require.Equal(t, expectedConn, conn)
+ require.Equal(t, expectedErr, err)
+}
+
// UDPPacketListener
func TestUDPPacketListenerLocalIPv4Addr(t *testing.T) {
- listener := &UDPPacketListener{Address: "127.0.0.1:0"}
+ listener := &UDPListener{Address: "127.0.0.1:0"}
pc, err := listener.ListenPacket(context.Background())
require.NoError(t, err)
require.Equal(t, "udp", pc.LocalAddr().Network())
@@ -82,7 +106,7 @@ func TestUDPPacketListenerLocalIPv4Addr(t *testing.T) {
}
func TestUDPPacketListenerLocalIPv6Addr(t *testing.T) {
- listener := &UDPPacketListener{Address: "[::1]:0"}
+ listener := &UDPListener{Address: "[::1]:0"}
pc, err := listener.ListenPacket(context.Background())
require.NoError(t, err)
require.Equal(t, "udp", pc.LocalAddr().Network())
@@ -92,7 +116,7 @@ func TestUDPPacketListenerLocalIPv6Addr(t *testing.T) {
}
func TestUDPPacketListenerLocalhost(t *testing.T) {
- listener := &UDPPacketListener{Address: "localhost:0"}
+ listener := &UDPListener{Address: "localhost:0"}
pc, err := listener.ListenPacket(context.Background())
require.NoError(t, err)
require.Equal(t, "udp", pc.LocalAddr().Network())
@@ -102,7 +126,7 @@ func TestUDPPacketListenerLocalhost(t *testing.T) {
}
func TestUDPPacketListenerDefaulAddr(t *testing.T) {
- listener := &UDPPacketListener{}
+ listener := &UDPListener{}
pc, err := listener.ListenPacket(context.Background())
require.Equal(t, "udp", pc.LocalAddr().Network())
require.NoError(t, err)
@@ -118,8 +142,8 @@ func TestUDPPacketDialer(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "udp", server.LocalAddr().Network())
- dialer := &UDPPacketDialer{}
- conn, err := dialer.Dial(context.Background(), server.LocalAddr().String())
+ dialer := &UDPDialer{}
+ conn, err := dialer.DialPacket(context.Background(), server.LocalAddr().String())
require.NoError(t, err)
request := []byte("PING")
@@ -145,7 +169,7 @@ func TestPacketListenerDialer(t *testing.T) {
request := []byte("Request")
response := []byte("Response")
- serverListener := UDPPacketListener{Address: "127.0.0.1:0"}
+ serverListener := UDPListener{Address: "127.0.0.1:0"}
serverPacketConn, err := serverListener.ListenPacket(context.Background())
require.NoError(t, err, "Failed to create UDP listener: %v", err)
t.Logf("Listening on %v", serverPacketConn.LocalAddr())
@@ -178,9 +202,9 @@ func TestPacketListenerDialer(t *testing.T) {
}()
serverEndpoint := &PacketListenerDialer{
- Listener: UDPPacketListener{Address: "127.0.0.1:0"},
+ Listener: UDPListener{Address: "127.0.0.1:0"},
}
- conn, err := serverEndpoint.Dial(context.Background(), serverPacketConn.LocalAddr().String())
+ conn, err := serverEndpoint.DialPacket(context.Background(), serverPacketConn.LocalAddr().String())
require.NoError(t, err)
t.Logf("Connected to %v from %v", conn.RemoteAddr(), conn.LocalAddr())
defer func() {
diff --git a/transport/shadowsocks/packet_listener.go b/transport/shadowsocks/packet_listener.go
index aa0c9efd..548138dc 100644
--- a/transport/shadowsocks/packet_listener.go
+++ b/transport/shadowsocks/packet_listener.go
@@ -50,7 +50,7 @@ func NewPacketListener(endpoint transport.PacketEndpoint, key *EncryptionKey) (t
}
func (c *packetListener) ListenPacket(ctx context.Context) (net.PacketConn, error) {
- proxyConn, err := c.endpoint.Connect(ctx)
+ proxyConn, err := c.endpoint.ConnectPacket(ctx)
if err != nil {
return nil, fmt.Errorf("could not connect to endpoint: %w", err)
}
diff --git a/transport/shadowsocks/stream_dialer.go b/transport/shadowsocks/stream_dialer.go
index 8749e73f..e1fc8bb3 100644
--- a/transport/shadowsocks/stream_dialer.go
+++ b/transport/shadowsocks/stream_dialer.go
@@ -63,7 +63,7 @@ type StreamDialer struct {
var _ transport.StreamDialer = (*StreamDialer)(nil)
-// Dial implements StreamDialer.Dial using a Shadowsocks server.
+// DialStream implements StreamDialer.DialStream using a Shadowsocks server.
//
// The Shadowsocks StreamDialer returns a connection after the connection to the proxy is established,
// but before the connection to the target is established. That means we cannot signal "connection refused"
@@ -78,12 +78,12 @@ var _ transport.StreamDialer = (*StreamDialer)(nil)
// initial data from the application in order to send the Shadowsocks salt, SOCKS address and initial data
// all in one packet. This makes the size of the initial packet hard to predict, avoiding packet size
// fingerprinting. We can only get the application initial data if we return a connection first.
-func (c *StreamDialer) Dial(ctx context.Context, remoteAddr string) (transport.StreamConn, error) {
+func (c *StreamDialer) DialStream(ctx context.Context, remoteAddr string) (transport.StreamConn, error) {
socksTargetAddr := socks.ParseAddr(remoteAddr)
if socksTargetAddr == nil {
return nil, errors.New("failed to parse target address")
}
- proxyConn, err := c.endpoint.Connect(ctx)
+ proxyConn, err := c.endpoint.ConnectStream(ctx)
if err != nil {
return nil, err
}
diff --git a/transport/shadowsocks/stream_dialer_test.go b/transport/shadowsocks/stream_dialer_test.go
index 8c223138..e361fcaa 100644
--- a/transport/shadowsocks/stream_dialer_test.go
+++ b/transport/shadowsocks/stream_dialer_test.go
@@ -34,7 +34,7 @@ func TestStreamDialer_Dial(t *testing.T) {
if err != nil {
t.Fatalf("Failed to create StreamDialer: %v", err)
}
- conn, err := d.Dial(context.Background(), testTargetAddr)
+ conn, err := d.DialStream(context.Background(), testTargetAddr)
if err != nil {
t.Fatalf("StreamDialer.Dial failed: %v", err)
}
@@ -56,7 +56,7 @@ func TestStreamDialer_DialNoPayload(t *testing.T) {
// Extend the wait to be safer.
d.ClientDataWait = 0 * time.Millisecond
- conn, err := d.Dial(context.Background(), testTargetAddr)
+ conn, err := d.DialStream(context.Background(), testTargetAddr)
if err != nil {
t.Fatalf("StreamDialer.Dial failed: %v", err)
}
@@ -102,7 +102,7 @@ func TestStreamDialer_DialFastClose(t *testing.T) {
// Extend the wait to be safer.
d.ClientDataWait = 100 * time.Millisecond
- conn, err := d.Dial(context.Background(), testTargetAddr)
+ conn, err := d.DialStream(context.Background(), testTargetAddr)
require.NoError(t, err, "StreamDialer.Dial failed: %v", err)
// Wait for less than 100 milliseconds to ensure that the target
@@ -151,7 +151,7 @@ func TestStreamDialer_TCPPrefix(t *testing.T) {
t.Fatalf("Failed to create StreamDialer: %v", err)
}
d.SaltGenerator = NewPrefixSaltGenerator(prefix)
- conn, err := d.Dial(context.Background(), testTargetAddr)
+ conn, err := d.DialStream(context.Background(), testTargetAddr)
if err != nil {
t.Fatalf("StreamDialer.Dial failed: %v", err)
}
@@ -170,7 +170,7 @@ func BenchmarkStreamDialer_Dial(b *testing.B) {
if err != nil {
b.Fatalf("Failed to create StreamDialer: %v", err)
}
- conn, err := d.Dial(context.Background(), testTargetAddr)
+ conn, err := d.DialStream(context.Background(), testTargetAddr)
if err != nil {
b.Fatalf("StreamDialer.Dial failed: %v", err)
}
diff --git a/transport/socks5/stream_dialer.go b/transport/socks5/stream_dialer.go
index 14743d34..9d4febef 100644
--- a/transport/socks5/stream_dialer.go
+++ b/transport/socks5/stream_dialer.go
@@ -38,12 +38,12 @@ type streamDialer struct {
var _ transport.StreamDialer = (*streamDialer)(nil)
-// Dial implements [transport.StreamDialer].Dial using SOCKS5.
+// DialStream implements [transport.StreamDialer].DialStream using SOCKS5.
// It will send the method and the connect requests in one packet, to avoid an unnecessary roundtrip.
// The returned [error] will be of type [ReplyCode] if the server sends a SOCKS error reply code, which
// you can check against the error constants in this package using [errors.Is].
-func (c *streamDialer) Dial(ctx context.Context, remoteAddr string) (transport.StreamConn, error) {
- proxyConn, err := c.proxyEndpoint.Connect(ctx)
+func (c *streamDialer) DialStream(ctx context.Context, remoteAddr string) (transport.StreamConn, error) {
+ proxyConn, err := c.proxyEndpoint.ConnectStream(ctx)
if err != nil {
return nil, fmt.Errorf("could not connect to SOCKS5 proxy: %w", err)
}
diff --git a/transport/socks5/stream_dialer_test.go b/transport/socks5/stream_dialer_test.go
index cdf997e0..e79fdcac 100644
--- a/transport/socks5/stream_dialer_test.go
+++ b/transport/socks5/stream_dialer_test.go
@@ -39,7 +39,7 @@ func TestSOCKS5Dialer_BadConnection(t *testing.T) {
dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: "127.0.0.0:0"})
require.NotNil(t, dialer)
require.NoError(t, err)
- _, err = dialer.Dial(context.Background(), "example.com:443")
+ _, err = dialer.DialStream(context.Background(), "example.com:443")
require.Error(t, err)
}
@@ -52,7 +52,7 @@ func TestSOCKS5Dialer_BadAddress(t *testing.T) {
require.NotNil(t, dialer)
require.NoError(t, err)
- _, err = dialer.Dial(context.Background(), "noport")
+ _, err = dialer.DialStream(context.Background(), "noport")
require.Error(t, err)
}
@@ -97,7 +97,7 @@ func testExchange(tb testing.TB, listener *net.TCPListener, destAddr string, req
defer running.Done()
dialer, err := NewStreamDialer(&transport.TCPEndpoint{Address: listener.Addr().String()})
require.NoError(tb, err)
- serverConn, err := dialer.Dial(context.Background(), destAddr)
+ serverConn, err := dialer.DialStream(context.Background(), destAddr)
if replyCode != 0 {
require.ErrorIs(tb, err, replyCode)
var extractedReplyCode ReplyCode
diff --git a/transport/split/stream_dialer.go b/transport/split/stream_dialer.go
index 4a1275a6..fac0a6d9 100644
--- a/transport/split/stream_dialer.go
+++ b/transport/split/stream_dialer.go
@@ -37,9 +37,9 @@ func NewStreamDialer(dialer transport.StreamDialer, prefixBytes int64) (transpor
return &splitDialer{dialer: dialer, splitPoint: prefixBytes}, nil
}
-// Dial implements [transport.StreamDialer].Dial.
-func (d *splitDialer) Dial(ctx context.Context, remoteAddr string) (transport.StreamConn, error) {
- innerConn, err := d.dialer.Dial(ctx, remoteAddr)
+// DialStream implements [transport.StreamDialer].DialStream.
+func (d *splitDialer) DialStream(ctx context.Context, remoteAddr string) (transport.StreamConn, error) {
+ innerConn, err := d.dialer.DialStream(ctx, remoteAddr)
if err != nil {
return nil, err
}
diff --git a/transport/stream.go b/transport/stream.go
index 5d279308..60241354 100644
--- a/transport/stream.go
+++ b/transport/stream.go
@@ -72,8 +72,8 @@ func WrapConn(c StreamConn, r io.Reader, w io.Writer) StreamConn {
// StreamEndpoint represents an endpoint that can be used to establish stream connections (like TCP) to a fixed
// destination.
type StreamEndpoint interface {
- // Connect establishes a connection with the endpoint, returning the connection.
- Connect(ctx context.Context) (StreamConn, error)
+ // ConnectStream establishes a connection with the endpoint, returning the connection.
+ ConnectStream(ctx context.Context) (StreamConn, error)
}
// TCPEndpoint is a [StreamEndpoint] that connects to the specified address using the specified [StreamDialer].
@@ -87,8 +87,8 @@ type TCPEndpoint struct {
var _ StreamEndpoint = (*TCPEndpoint)(nil)
-// Connect implements [StreamEndpoint].Connect.
-func (e *TCPEndpoint) Connect(ctx context.Context) (StreamConn, error) {
+// ConnectStream implements [StreamEndpoint].ConnectStream.
+func (e *TCPEndpoint) ConnectStream(ctx context.Context) (StreamConn, error) {
conn, err := e.Dialer.DialContext(ctx, "tcp", e.Address)
if err != nil {
return nil, err
@@ -96,6 +96,16 @@ func (e *TCPEndpoint) Connect(ctx context.Context) (StreamConn, error) {
return conn.(*net.TCPConn), nil
}
+// FuncStreamEndpoint is a [StreamEndpoint] that uses the given function to connect.
+type FuncStreamEndpoint func(ctx context.Context) (StreamConn, error)
+
+var _ StreamEndpoint = (*FuncStreamEndpoint)(nil)
+
+// ConnectStream implements the [StreamEndpoint] interface.
+func (f FuncStreamEndpoint) ConnectStream(ctx context.Context) (StreamConn, error) {
+ return f(ctx)
+}
+
// StreamDialerEndpoint is a [StreamEndpoint] that connects to the specified address using the specified
// [StreamDialer].
type StreamDialerEndpoint struct {
@@ -105,30 +115,40 @@ type StreamDialerEndpoint struct {
var _ StreamEndpoint = (*StreamDialerEndpoint)(nil)
-// Connect implements [StreamEndpoint].Connect.
-func (e *StreamDialerEndpoint) Connect(ctx context.Context) (StreamConn, error) {
- return e.Dialer.Dial(ctx, e.Address)
+// ConnectStream implements [StreamEndpoint].ConnectStream.
+func (e *StreamDialerEndpoint) ConnectStream(ctx context.Context) (StreamConn, error) {
+ return e.Dialer.DialStream(ctx, e.Address)
}
// StreamDialer provides a way to dial a destination and establish stream connections.
type StreamDialer interface {
- // Dial connects to `raddr`.
+ // DialStream connects to `raddr`.
// `raddr` has the form "host:port", where "host" can be a domain name or IP address.
- Dial(ctx context.Context, raddr string) (StreamConn, error)
+ DialStream(ctx context.Context, raddr string) (StreamConn, error)
}
-// TCPStreamDialer is a [StreamDialer] that uses the standard [net.Dialer] to dial.
+// TCPDialer is a [StreamDialer] that uses the standard [net.Dialer] to dial.
// It provides a convenient way to use a [net.Dialer] when you need a [StreamDialer].
-type TCPStreamDialer struct {
+type TCPDialer struct {
Dialer net.Dialer
}
-var _ StreamDialer = (*TCPStreamDialer)(nil)
+var _ StreamDialer = (*TCPDialer)(nil)
-func (d *TCPStreamDialer) Dial(ctx context.Context, addr string) (StreamConn, error) {
+func (d *TCPDialer) DialStream(ctx context.Context, addr string) (StreamConn, error) {
conn, err := d.Dialer.DialContext(ctx, "tcp", addr)
if err != nil {
return nil, err
}
return conn.(*net.TCPConn), nil
}
+
+// FuncStreamDialer is a [StreamDialer] that uses the given function to dial.
+type FuncStreamDialer func(ctx context.Context, addr string) (StreamConn, error)
+
+var _ StreamDialer = (*FuncStreamDialer)(nil)
+
+// DialStream implements the [StreamDialer] interface.
+func (f FuncStreamDialer) DialStream(ctx context.Context, addr string) (StreamConn, error) {
+ return f(ctx, addr)
+}
diff --git a/transport/stream_test.go b/transport/stream_test.go
index 9e83cd36..27f51c7e 100644
--- a/transport/stream_test.go
+++ b/transport/stream_test.go
@@ -27,6 +27,33 @@ import (
"github.com/stretchr/testify/require"
)
+type fakeConn struct {
+ StreamConn
+}
+
+func TestFuncStreamEndpoint(t *testing.T) {
+ expectedConn := &fakeConn{}
+ expectedErr := errors.New("fake error")
+ endpoint := FuncStreamEndpoint(func(ctx context.Context) (StreamConn, error) {
+ return expectedConn, expectedErr
+ })
+ conn, err := endpoint.ConnectStream(context.Background())
+ require.Equal(t, expectedConn, conn)
+ require.Equal(t, expectedErr, err)
+}
+
+func TestFuncStreamDialer(t *testing.T) {
+ expectedConn := &fakeConn{}
+ expectedErr := errors.New("fake error")
+ dialer := FuncStreamDialer(func(ctx context.Context, addr string) (StreamConn, error) {
+ require.Equal(t, "unused", addr)
+ return expectedConn, expectedErr
+ })
+ conn, err := dialer.DialStream(context.Background(), "unused")
+ require.Equal(t, expectedConn, conn)
+ require.Equal(t, expectedErr, err)
+}
+
func TestNewTCPStreamDialerIPv4(t *testing.T) {
requestText := []byte("Request")
responseText := []byte("Response")
@@ -65,13 +92,13 @@ func TestNewTCPStreamDialerIPv4(t *testing.T) {
// Client
go func() {
defer running.Done()
- dialer := &TCPStreamDialer{}
+ dialer := &TCPDialer{}
dialer.Dialer.Control = func(network, address string, c syscall.RawConn) error {
require.Equal(t, "tcp4", network)
require.Equal(t, listener.Addr().String(), address)
return nil
}
- serverConn, err := dialer.Dial(context.Background(), listener.Addr().String())
+ serverConn, err := dialer.DialStream(context.Background(), listener.Addr().String())
require.NoError(t, err, "Dial failed")
require.Equal(t, listener.Addr().String(), serverConn.RemoteAddr().String())
defer serverConn.Close()
@@ -93,14 +120,14 @@ func TestNewTCPStreamDialerIPv4(t *testing.T) {
func TestNewTCPStreamDialerAddress(t *testing.T) {
errCancel := errors.New("cancelled")
- dialer := &TCPStreamDialer{}
+ dialer := &TCPDialer{}
dialer.Dialer.Control = func(network, address string, c syscall.RawConn) error {
require.Equal(t, "tcp4", network)
require.Equal(t, "8.8.8.8:53", address)
return errCancel
}
- _, err := dialer.Dial(context.Background(), "8.8.8.8:53")
+ _, err := dialer.DialStream(context.Background(), "8.8.8.8:53")
require.ErrorIs(t, err, errCancel)
dialer.Dialer.Control = func(network, address string, c syscall.RawConn) error {
@@ -108,7 +135,7 @@ func TestNewTCPStreamDialerAddress(t *testing.T) {
require.Equal(t, "[2001:4860:4860::8888]:53", address)
return errCancel
}
- _, err = dialer.Dial(context.Background(), "[2001:4860:4860::8888]:53")
+ _, err = dialer.DialStream(context.Background(), "[2001:4860:4860::8888]:53")
require.ErrorIs(t, err, errCancel)
}
@@ -123,7 +150,7 @@ func TestDialStreamEndpointAddr(t *testing.T) {
require.Equal(t, listener.Addr().String(), address)
return nil
}
- conn, err := endpoint.Connect(context.Background())
+ conn, err := endpoint.ConnectStream(context.Background())
require.NoError(t, err)
require.Equal(t, listener.Addr().String(), conn.RemoteAddr().String())
require.Nil(t, conn.Close())
diff --git a/x/tls/doc.go b/transport/tls/doc.go
similarity index 100%
rename from x/tls/doc.go
rename to transport/tls/doc.go
diff --git a/x/tls/stream_dialer.go b/transport/tls/stream_dialer.go
similarity index 76%
rename from x/tls/stream_dialer.go
rename to transport/tls/stream_dialer.go
index 883b3798..7e6bc125 100644
--- a/x/tls/stream_dialer.go
+++ b/transport/tls/stream_dialer.go
@@ -62,13 +62,17 @@ func (c streamConn) CloseRead() error {
return c.innerConn.CloseRead()
}
-// Dial implements [transport.StreamDialer].Dial.
-func (d *StreamDialer) Dial(ctx context.Context, remoteAddr string) (transport.StreamConn, error) {
- innerConn, err := d.dialer.Dial(ctx, remoteAddr)
+// DialStream implements [transport.StreamDialer].DialStream.
+func (d *StreamDialer) DialStream(ctx context.Context, remoteAddr string) (transport.StreamConn, error) {
+ innerConn, err := d.dialer.DialStream(ctx, remoteAddr)
if err != nil {
return nil, err
}
- conn, err := WrapConn(ctx, innerConn, remoteAddr, d.options...)
+ host, _, err := net.SplitHostPort(remoteAddr)
+ if err != nil {
+ return nil, fmt.Errorf("invalid address: %w", err)
+ }
+ conn, err := WrapConn(ctx, innerConn, host, d.options...)
if err != nil {
innerConn.Close()
return nil, err
@@ -92,8 +96,8 @@ type ClientConfig struct {
SessionCache tls.ClientSessionCache
}
-// ToStdConfig creates a [tls.Config] based on the configured parameters.
-func (cfg *ClientConfig) ToStdConfig() *tls.Config {
+// toStdConfig creates a [tls.Config] based on the configured parameters.
+func (cfg *ClientConfig) toStdConfig() *tls.Config {
return &tls.Config{
ServerName: cfg.ServerName,
NextProtos: cfg.NextProtos,
@@ -120,25 +124,17 @@ func (cfg *ClientConfig) ToStdConfig() *tls.Config {
}
// ClientOption allows configuring the parameters to be used for a client TLS connection.
-type ClientOption func(host string, port int, config *ClientConfig)
+type ClientOption func(serverName string, config *ClientConfig)
// WrapConn wraps a [transport.StreamConn] in a TLS connection.
-func WrapConn(ctx context.Context, conn transport.StreamConn, remoteAdr string, options ...ClientOption) (transport.StreamConn, error) {
- host, portStr, err := net.SplitHostPort(remoteAdr)
- if err != nil {
- return nil, fmt.Errorf("could not parse remote address: %w", err)
- }
- host = normalizeHost(host)
- port, err := net.DefaultResolver.LookupPort(ctx, "tcp", portStr)
- if err != nil {
- return nil, fmt.Errorf("could not resolve port: %w", err)
- }
- cfg := ClientConfig{ServerName: host, CertificateName: host}
+func WrapConn(ctx context.Context, conn transport.StreamConn, serverName string, options ...ClientOption) (transport.StreamConn, error) {
+ cfg := ClientConfig{ServerName: serverName, CertificateName: serverName}
+ normName := normalizeHost(serverName)
for _, option := range options {
- option(host, port, &cfg)
+ option(normName, &cfg)
}
- tlsConn := tls.Client(conn, cfg.ToStdConfig())
- err = tlsConn.HandshakeContext(ctx)
+ tlsConn := tls.Client(conn, cfg.toStdConfig())
+ err := tlsConn.HandshakeContext(ctx)
if err != nil {
return nil, err
}
@@ -151,22 +147,19 @@ func WrapConn(ctx context.Context, conn transport.StreamConn, remoteAdr string,
//
// [Server Name Indication]: https://datatracker.ietf.org/doc/html/rfc6066#section-3
func WithSNI(hostName string) ClientOption {
- return func(_ string, _ int, config *ClientConfig) {
+ return func(_ string, config *ClientConfig) {
config.ServerName = hostName
}
}
-// IfHostPort applies the given option if the host and port matches the dialed one.
-func IfHostPort(matchHost string, matchPort int, option ClientOption) ClientOption {
+// IfHost applies the given option if the host matches the dialed one.
+func IfHost(matchHost string, option ClientOption) ClientOption {
matchHost = normalizeHost(matchHost)
- return func(host string, port int, config *ClientConfig) {
+ return func(host string, config *ClientConfig) {
if matchHost != "" && matchHost != host {
return
}
- if matchPort != 0 && matchPort != port {
- return
- }
- option(host, port, config)
+ option(host, config)
}
}
@@ -176,14 +169,14 @@ func IfHostPort(matchHost string, matchPort int, option ClientOption) ClientOpti
// [Application-Layer Protocol Negotiation]: https://datatracker.ietf.org/doc/html/rfc7301
// [IANA's registry]: https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
func WithALPN(protocolNameList []string) ClientOption {
- return func(_ string, _ int, config *ClientConfig) {
+ return func(_ string, config *ClientConfig) {
config.NextProtos = protocolNameList
}
}
// WithSessionCache sets the [tls.ClientSessionCache] to enable session resumption of TLS connections.
func WithSessionCache(sessionCache tls.ClientSessionCache) ClientOption {
- return func(_ string, _ int, config *ClientConfig) {
+ return func(_ string, config *ClientConfig) {
config.SessionCache = sessionCache
}
}
@@ -191,7 +184,7 @@ func WithSessionCache(sessionCache tls.ClientSessionCache) ClientOption {
// WithCertificateName sets the hostname to be used for the certificate cerification.
// If absent, defaults to the dialed hostname.
func WithCertificateName(hostname string) ClientOption {
- return func(_ string, _ int, config *ClientConfig) {
+ return func(_ string, config *ClientConfig) {
config.CertificateName = hostname
}
}
diff --git a/x/tls/stream_dialer_test.go b/transport/tls/stream_dialer_test.go
similarity index 52%
rename from x/tls/stream_dialer_test.go
rename to transport/tls/stream_dialer_test.go
index d32611ce..7927c099 100644
--- a/x/tls/stream_dialer_test.go
+++ b/transport/tls/stream_dialer_test.go
@@ -24,9 +24,9 @@ import (
)
func TestDomain(t *testing.T) {
- sd, err := NewStreamDialer(&transport.TCPStreamDialer{})
+ sd, err := NewStreamDialer(&transport.TCPDialer{})
require.NoError(t, err)
- conn, err := sd.Dial(context.Background(), "dns.google:443")
+ conn, err := sd.DialStream(context.Background(), "dns.google:443")
require.NoError(t, err)
tlsConn, ok := conn.(streamConn)
require.True(t, ok)
@@ -37,111 +37,90 @@ func TestDomain(t *testing.T) {
}
func TestUntrustedRoot(t *testing.T) {
- sd, err := NewStreamDialer(&transport.TCPStreamDialer{})
+ sd, err := NewStreamDialer(&transport.TCPDialer{})
require.NoError(t, err)
- _, err = sd.Dial(context.Background(), "untrusted-root.badssl.com:443")
+ _, err = sd.DialStream(context.Background(), "untrusted-root.badssl.com:443")
var certErr x509.UnknownAuthorityError
require.ErrorAs(t, err, &certErr)
}
func TestRevoked(t *testing.T) {
- sd, err := NewStreamDialer(&transport.TCPStreamDialer{})
+ sd, err := NewStreamDialer(&transport.TCPDialer{})
require.NoError(t, err)
- _, err = sd.Dial(context.Background(), "revoked.badssl.com:443")
+ _, err = sd.DialStream(context.Background(), "revoked.badssl.com:443")
var certErr x509.CertificateInvalidError
require.ErrorAs(t, err, &certErr)
require.Equal(t, x509.Expired, certErr.Reason)
}
func TestIP(t *testing.T) {
- sd, err := NewStreamDialer(&transport.TCPStreamDialer{})
+ sd, err := NewStreamDialer(&transport.TCPDialer{})
require.NoError(t, err)
- conn, err := sd.Dial(context.Background(), "8.8.8.8:443")
+ conn, err := sd.DialStream(context.Background(), "8.8.8.8:443")
require.NoError(t, err)
conn.Close()
}
func TestIPOverride(t *testing.T) {
- sd, err := NewStreamDialer(&transport.TCPStreamDialer{}, WithCertificateName("8.8.8.8"))
+ sd, err := NewStreamDialer(&transport.TCPDialer{}, WithCertificateName("8.8.8.8"))
require.NoError(t, err)
- conn, err := sd.Dial(context.Background(), "dns.google:443")
+ conn, err := sd.DialStream(context.Background(), "dns.google:443")
require.NoError(t, err)
conn.Close()
}
func TestFakeSNI(t *testing.T) {
- sd, err := NewStreamDialer(&transport.TCPStreamDialer{}, WithSNI("decoy.example.com"))
+ sd, err := NewStreamDialer(&transport.TCPDialer{}, WithSNI("decoy.example.com"))
require.NoError(t, err)
- conn, err := sd.Dial(context.Background(), "www.youtube.com:443")
+ conn, err := sd.DialStream(context.Background(), "www.youtube.com:443")
require.NoError(t, err)
conn.Close()
}
func TestNoSNI(t *testing.T) {
- sd, err := NewStreamDialer(&transport.TCPStreamDialer{}, WithSNI(""))
+ sd, err := NewStreamDialer(&transport.TCPDialer{}, WithSNI(""))
require.NoError(t, err)
- conn, err := sd.Dial(context.Background(), "dns.google:443")
+ conn, err := sd.DialStream(context.Background(), "dns.google:443")
require.NoError(t, err)
conn.Close()
}
func TestAllCustom(t *testing.T) {
- sd, err := NewStreamDialer(&transport.TCPStreamDialer{}, WithSNI("decoy.android.com"), WithCertificateName("www.youtube.com"))
+ sd, err := NewStreamDialer(&transport.TCPDialer{}, WithSNI("decoy.android.com"), WithCertificateName("www.youtube.com"))
require.NoError(t, err)
- conn, err := sd.Dial(context.Background(), "www.google.com:443")
+ conn, err := sd.DialStream(context.Background(), "www.google.com:443")
require.NoError(t, err)
conn.Close()
}
func TestHostSelector(t *testing.T) {
- sd, err := NewStreamDialer(&transport.TCPStreamDialer{},
- IfHostPort("dns.google", 0, WithSNI("decoy.example.com")),
- IfHostPort("www.youtube.com", 0, WithSNI("notyoutube.com")),
+ sd, err := NewStreamDialer(&transport.TCPDialer{},
+ IfHost("dns.google", WithSNI("decoy.example.com")),
+ IfHost("www.youtube.com", WithSNI("notyoutube.com")),
)
require.NoError(t, err)
- conn, err := sd.Dial(context.Background(), "dns.google:443")
+ conn, err := sd.DialStream(context.Background(), "dns.google:443")
require.NoError(t, err)
tlsConn := conn.(streamConn)
require.Equal(t, "decoy.example.com", tlsConn.ConnectionState().ServerName)
conn.Close()
- conn, err = sd.Dial(context.Background(), "www.youtube.com:443")
+ conn, err = sd.DialStream(context.Background(), "www.youtube.com:443")
require.NoError(t, err)
tlsConn = conn.(streamConn)
require.Equal(t, "notyoutube.com", tlsConn.ConnectionState().ServerName)
conn.Close()
}
-func TestPortSelector(t *testing.T) {
- sd, err := NewStreamDialer(&transport.TCPStreamDialer{},
- IfHostPort("", 443, WithALPN([]string{"http/1.1"})),
- IfHostPort("www.google.com", 443, WithALPN([]string{"h2"})),
- IfHostPort("", 853, WithALPN([]string{"dot"})),
- )
- require.NoError(t, err)
-
- conn, err := sd.Dial(context.Background(), "dns.google:443")
- require.NoError(t, err)
- tlsConn := conn.(streamConn)
- require.Equal(t, "http/1.1", tlsConn.ConnectionState().NegotiatedProtocol)
- conn.Close()
-
- conn, err = sd.Dial(context.Background(), "www.google.com:443")
- require.NoError(t, err)
- tlsConn = conn.(streamConn)
- require.Equal(t, "h2", tlsConn.ConnectionState().NegotiatedProtocol)
- conn.Close()
-}
-
func TestWithSNI(t *testing.T) {
var cfg ClientConfig
- WithSNI("example.com")("", 0, &cfg)
+ WithSNI("example.com")("", &cfg)
require.Equal(t, "example.com", cfg.ServerName)
}
func TestWithALPN(t *testing.T) {
var cfg ClientConfig
- WithALPN([]string{"h2", "http/1.1"})("", 0, &cfg)
+ WithALPN([]string{"h2", "http/1.1"})("", &cfg)
require.Equal(t, []string{"h2", "http/1.1"}, cfg.NextProtos)
}
diff --git a/transport/tlsfrag/stream_dialer.go b/transport/tlsfrag/stream_dialer.go
index 550ae131..1cf38bec 100644
--- a/transport/tlsfrag/stream_dialer.go
+++ b/transport/tlsfrag/stream_dialer.go
@@ -57,10 +57,10 @@ func NewStreamDialerFunc(base transport.StreamDialer, frag FragFunc) (transport.
return &tlsFragDialer{base, frag}, nil
}
-// Dial implements [transport.StreamConn].Dial. It establishes a connection to raddr in the format "host-or-ip:port".
+// DialStream implements [transport.StreamConn].DialStream. It establishes a connection to raddr in the format "host-or-ip:port".
// The initial TLS Client Hello record sent through the connection will be fragmented.
-func (d *tlsFragDialer) Dial(ctx context.Context, raddr string) (conn transport.StreamConn, err error) {
- conn, err = d.dialer.Dial(ctx, raddr)
+func (d *tlsFragDialer) DialStream(ctx context.Context, raddr string) (conn transport.StreamConn, err error) {
+ conn, err = d.dialer.DialStream(ctx, raddr)
if err != nil {
return
}
diff --git a/transport/tlsfrag/stream_dialer_test.go b/transport/tlsfrag/stream_dialer_test.go
index c88754a8..3d127395 100644
--- a/transport/tlsfrag/stream_dialer_test.go
+++ b/transport/tlsfrag/stream_dialer_test.go
@@ -182,7 +182,7 @@ func assertCanDialFragFunc(t *testing.T, inner transport.StreamDialer, raddr str
d, err := NewStreamDialerFunc(inner, frag)
require.NoError(t, err)
require.NotNil(t, d)
- conn, err := d.Dial(context.Background(), raddr)
+ conn, err := d.DialStream(context.Background(), raddr)
require.NoError(t, err)
require.NotNil(t, conn)
return conn
@@ -192,7 +192,7 @@ func assertCanDialFixedLenFrag(t *testing.T, inner transport.StreamDialer, raddr
d, err := NewFixedLenStreamDialer(inner, splitLen)
require.NoError(t, err)
require.NotNil(t, d)
- conn, err := d.Dial(context.Background(), raddr)
+ conn, err := d.DialStream(context.Background(), raddr)
require.NoError(t, err)
require.NotNil(t, conn)
return conn
@@ -231,7 +231,7 @@ type collectStreamDialer struct {
bufs net.Buffers
}
-func (d *collectStreamDialer) Dial(ctx context.Context, raddr string) (transport.StreamConn, error) {
+func (d *collectStreamDialer) DialStream(ctx context.Context, raddr string) (transport.StreamConn, error) {
return d, nil
}
diff --git a/x/config/config.go b/x/config/config.go
index 046b2893..57f04186 100644
--- a/x/config/config.go
+++ b/x/config/config.go
@@ -24,6 +24,7 @@ import (
"github.com/Jigsaw-Code/outline-sdk/transport"
"github.com/Jigsaw-Code/outline-sdk/transport/socks5"
"github.com/Jigsaw-Code/outline-sdk/transport/split"
+ "github.com/Jigsaw-Code/outline-sdk/transport/tlsfrag"
)
func parseConfigPart(oneDialerConfig string) (*url.URL, error) {
@@ -31,7 +32,7 @@ func parseConfigPart(oneDialerConfig string) (*url.URL, error) {
if oneDialerConfig == "" {
return nil, errors.New("empty config part")
}
- // Make it ":" it it's only "" to parse as a URL.
+ // Make it ":" if it's only "" to parse as a URL.
if !strings.Contains(oneDialerConfig, ":") {
oneDialerConfig += ":"
}
@@ -44,7 +45,7 @@ func parseConfigPart(oneDialerConfig string) (*url.URL, error) {
// NewStreamDialer creates a new [transport.StreamDialer] according to the given config.
func NewStreamDialer(transportConfig string) (transport.StreamDialer, error) {
- return WrapStreamDialer(&transport.TCPStreamDialer{}, transportConfig)
+ return WrapStreamDialer(&transport.TCPDialer{}, transportConfig)
}
// WrapStreamDialer created a [transport.StreamDialer] according to transportConfig, using dialer as the
@@ -75,6 +76,9 @@ func newStreamDialerFromPart(innerDialer transport.StreamDialer, oneDialerConfig
// Please keep scheme list sorted.
switch strings.ToLower(url.Scheme) {
+ case "override":
+ return newOverrideStreamDialerFromURL(innerDialer, url)
+
case "socks5":
endpoint := transport.StreamDialerEndpoint{Dialer: innerDialer, Address: url.Host}
return socks5.NewStreamDialer(&endpoint)
@@ -93,6 +97,14 @@ func newStreamDialerFromPart(innerDialer transport.StreamDialer, oneDialerConfig
case "tls":
return newTlsStreamDialerFromURL(innerDialer, url)
+ case "tlsfrag":
+ lenStr := url.Opaque
+ fixedLen, err := strconv.Atoi(lenStr)
+ if err != nil {
+ return nil, fmt.Errorf("invalid tlsfrag option: %v. It should be in tlsfrag: format", lenStr)
+ }
+ return tlsfrag.NewFixedLenStreamDialer(innerDialer, fixedLen)
+
default:
return nil, fmt.Errorf("config scheme '%v' is not supported", url.Scheme)
}
@@ -100,7 +112,7 @@ func newStreamDialerFromPart(innerDialer transport.StreamDialer, oneDialerConfig
// NewPacketDialer creates a new [transport.PacketDialer] according to the given config.
func NewPacketDialer(transportConfig string) (dialer transport.PacketDialer, err error) {
- dialer = &transport.UDPPacketDialer{}
+ dialer = &transport.UDPDialer{}
transportConfig = strings.TrimSpace(transportConfig)
if transportConfig == "" {
return dialer, nil
@@ -122,6 +134,9 @@ func newPacketDialerFromPart(innerDialer transport.PacketDialer, oneDialerConfig
// Please keep scheme list sorted.
switch strings.ToLower(url.Scheme) {
+ case "override":
+ return newOverridePacketDialerFromURL(innerDialer, url)
+
case "socks5":
return nil, errors.New("socks5 is not supported for PacketDialers")
@@ -162,3 +177,43 @@ func NewPacketListener(transportConfig string) (transport.PacketListener, error)
return nil, fmt.Errorf("config scheme '%v' is not supported", url.Scheme)
}
}
+
+func SanitizeConfig(transportConfig string) (string, error) {
+ // Do nothing if the config is empty
+ if transportConfig == "" {
+ return "", nil
+ }
+ // Split the string into parts
+ parts := strings.Split(transportConfig, "|")
+
+ // Iterate through each part
+ for i, part := range parts {
+ u, err := parseConfigPart(part)
+ if err != nil {
+ return "", fmt.Errorf("failed to parse config part: %w", err)
+ }
+ scheme := strings.ToLower(u.Scheme)
+ switch scheme {
+ case "ss":
+ parts[i], _ = sanitizeShadowsocksURL(u)
+ case "socks5":
+ parts[i], _ = sanitizeSocks5URL(u)
+ case "override", "split", "tls", "tlsfrag":
+ // No sanitization needed
+ parts[i] = u.String()
+ default:
+ parts[i] = scheme + "://UNKNOWN"
+ }
+ }
+ // Join the parts back into a string
+ return strings.Join(parts, "|"), nil
+}
+
+func sanitizeSocks5URL(u *url.URL) (string, error) {
+ const redactedPlaceholder = "REDACTED"
+ if u.User != nil {
+ u.User = url.User(redactedPlaceholder)
+ return u.String(), nil
+ }
+ return u.String(), nil
+}
diff --git a/x/config/config_test.go b/x/config/config_test.go
new file mode 100644
index 00000000..01abfb17
--- /dev/null
+++ b/x/config/config_test.go
@@ -0,0 +1,104 @@
+package config
+
+import (
+ "encoding/base64"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestSanitizeConfig(t *testing.T) {
+ // Test that empty config is accepted.
+ _, err := SanitizeConfig("")
+ require.NoError(t, err)
+
+ // Test that a invalid cypher is rejected.
+ sanitizedConfig, err := SanitizeConfig("split:5|ss://jhvdsjkfhvkhsadvf@example.com:1234?prefix=HTTP%2F1.1%20")
+ require.NoError(t, err)
+ require.Equal(t, "split:5|ss://ERROR", sanitizedConfig)
+
+ // Test that a valid config is accepted and user info is redacted.
+ sanitizedConfig, err = SanitizeConfig("split:5|ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpLeTUyN2duU3FEVFB3R0JpQ1RxUnlT@example.com:1234?prefix=HTTP%2F1.1%20")
+ require.NoError(t, err)
+ require.Equal(t, "split:5|ss://REDACTED@example.com:1234?prefix=HTTP%2F1.1%20", sanitizedConfig)
+
+ // Test sanitizer with unknown transport.
+ sanitizedConfig, err = SanitizeConfig("split:5|vless://ac08785d-203d-4db4-915c-eb4e23435fd62@example.com:443?path=%2Fvless&security=tls&encryption=none&alpn=h2&host=sub.hello.com&fp=chrome&type=ws&sni=sub.hello.com#vless-ws-tls-cdn")
+ require.NoError(t, err)
+ require.Equal(t, "split:5|vless://UNKNOWN", sanitizedConfig)
+
+ // Test sanitizer with transport that don't have user info.
+ sanitizedConfig, err = SanitizeConfig("split:5|tlsfrag:5")
+ require.NoError(t, err)
+ require.Equal(t, "split:5|tlsfrag:5", sanitizedConfig)
+
+ // Test sanitization on an unknown transport.
+ sanitizedConfig, err = SanitizeConfig("transport://hjdbfjhbqfjheqrf")
+ require.NoError(t, err)
+ require.Equal(t, "transport://UNKNOWN", sanitizedConfig)
+
+ // Test that an invalid config is rejected.
+ _, err = SanitizeConfig("::hghg")
+ require.Error(t, err)
+}
+
+func TestShowsocksLagacyBase64URL(t *testing.T) {
+ encoded := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte("aes-256-gcm:1234567@example.com:1234?prefix=HTTP%2F1.1%20"))
+ u, err := parseConfigPart("ss://" + string(encoded) + "#outline-123")
+ require.NoError(t, err)
+ config, err := parseShadowsocksLegacyBase64URL(u)
+ require.Equal(t, "example.com:1234", config.serverAddress)
+ require.Equal(t, "HTTP/1.1 ", string(config.prefix))
+ require.NoError(t, err)
+}
+
+func TestParseShadowsocksURL(t *testing.T) {
+ encoded := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte("aes-256-gcm:1234567@example.com:1234?prefix=HTTP%2F1.1%20"))
+ u, err := parseConfigPart("ss://" + string(encoded) + "#outline-123")
+ require.NoError(t, err)
+ config, err := parseShadowsocksURL(u)
+ require.Equal(t, "example.com:1234", config.serverAddress)
+ require.Equal(t, "HTTP/1.1 ", string(config.prefix))
+ require.NoError(t, err)
+
+ encoded = base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte("aes-256-gcm:1234567"))
+ u, err = parseConfigPart("ss://" + string(encoded) + "@example.com:1234?prefix=HTTP%2F1.1%20" + "#outline-123")
+ require.NoError(t, err)
+ config, err = parseShadowsocksURL(u)
+ require.Equal(t, "example.com:1234", config.serverAddress)
+ require.Equal(t, "HTTP/1.1 ", string(config.prefix))
+ require.NoError(t, err)
+}
+
+func TestSocks5URLSanitization(t *testing.T) {
+ configString := "socks5://myuser:mypassword@192.168.1.100:1080"
+ sanitizedConfig, err := SanitizeConfig(configString)
+ require.NoError(t, err)
+ require.Equal(t, "socks5://REDACTED@192.168.1.100:1080", sanitizedConfig)
+}
+
+func TestParseShadowsocksSIP002URLUnsuccessful(t *testing.T) {
+ encoded := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString([]byte("aes-256-gcm:1234567@example.com:1234?prefix=HTTP%2F1.1%20"))
+ u, err := parseConfigPart("ss://" + string(encoded) + "#outline-123")
+ require.NoError(t, err)
+ _, err = parseShadowsocksSIP002URL(u)
+ require.Error(t, err)
+}
+
+func TestParseShadowsocksSIP002URLUnsupportedCypher(t *testing.T) {
+ configString := "ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwnTpLeTUyN2duU3FEVFB3R0JpQ1RxUnlT@example.com:1234?prefix=HTTP%2F1.1%20"
+ u, err := parseConfigPart(configString)
+ require.NoError(t, err)
+ _, err = parseShadowsocksSIP002URL(u)
+ require.Error(t, err)
+}
+
+func TestParseShadowsocksSIP002URLSuccessful(t *testing.T) {
+ configString := "ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpLeTUyN2duU3FEVFB3R0JpQ1RxUnlT@example.com:1234?prefix=HTTP%2F1.1%20"
+ u, err := parseConfigPart(configString)
+ require.NoError(t, err)
+ config, err := parseShadowsocksSIP002URL(u)
+ require.NoError(t, err)
+ require.Equal(t, "example.com:1234", config.serverAddress)
+ require.Equal(t, "HTTP/1.1 ", string(config.prefix))
+}
diff --git a/x/config/doc.go b/x/config/doc.go
index ae3eb279..50214bc3 100644
--- a/x/config/doc.go
+++ b/x/config/doc.go
@@ -48,19 +48,41 @@ It takes the length of the prefix. The stream will be split when PREFIX_LENGTH b
split:[PREFIX_LENGTH]
-TLS transport (currently streams only, package [github.com/Jigsaw-Code/outline-sdk/x/tls])
+TLS transport (currently streams only, package [github.com/Jigsaw-Code/outline-sdk/transport/tls])
The sni parameter defines the name to be sent in the TLS SNI. It can be empty.
The certname parameter defines what name to validate against the server certificate.
tls:sni=[SNI]&certname=[CERT_NAME]
+TLS fragmentation (streams only, package [github.com/Jigsaw-Code/outline-sdk/transport/tlsfrag]).
+
+The Client Hello record payload will be split into two fragments of size LENGTH and len(payload)-LENGTH if LENGTH>0.
+If LENGTH<0, the two fragments will be of size len(payload)-LENGTH and LENGTH respectively.
+For more details, refer to [github.com/Jigsaw-Code/outline-sdk/transport/tlsfrag].
+
+ tlsfrag:[LENGTH]
+
+Address override.
+
+This dialer configuration is helpful for testing and development or if you need to fix the domain
+resolution.
+The host parameter, if not empty, specifies the host to dial instead of the original host.
+The port parameter, if not empty, specifies the port to dial instead of the original port.
+
+ override:host=[HOST]&port=[PORT]
+
# Examples
Packet splitting - To split outgoing streams on bytes 2 and 123, you can use:
split:2|split:123
+Evading DNS and SNI blocking - A blocked site hosted on Cloudflare can potentially be accessed by resolving cloudflare.net instead of the original
+domain and using stream split:
+
+ override:host=cloudflare.net.|split:2
+
SOCKS5-over-TLS, with domain-fronting - To tunnel SOCKS5 over TLS, and set the SNI to decoy.example.com, while still validating against your host name, use:
tls:sni=decoy.example.com&certname=[HOST]|socks5:[HOST]:[PORT]
diff --git a/x/config/override.go b/x/config/override.go
new file mode 100644
index 00000000..885bc87e
--- /dev/null
+++ b/x/config/override.go
@@ -0,0 +1,95 @@
+// Copyright 2024 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "net/url"
+ "strings"
+
+ "github.com/Jigsaw-Code/outline-sdk/transport"
+)
+
+func newOverrideFromURL(configURL *url.URL) (func(string) (string, error), error) {
+ query := configURL.Opaque
+ values, err := url.ParseQuery(query)
+ if err != nil {
+ return nil, err
+ }
+ hostOverride, portOverride := "", ""
+ for key, values := range values {
+ switch strings.ToLower(key) {
+ case "host":
+ if len(values) != 1 {
+ return nil, fmt.Errorf("host option must has one value, found %v", len(values))
+ }
+ hostOverride = values[0]
+ case "port":
+ if len(values) != 1 {
+ return nil, fmt.Errorf("port option must has one value, found %v", len(values))
+ }
+ portOverride = values[0]
+ default:
+ return nil, fmt.Errorf("unsupported option %v", key)
+ }
+ }
+ return func(address string) (string, error) {
+ // Optimization when we fully override the address.
+ if hostOverride != "" && portOverride != "" {
+ return net.JoinHostPort(hostOverride, portOverride), nil
+ }
+ host, port, err := net.SplitHostPort(address)
+ if err != nil {
+ return "", fmt.Errorf("address is not valid host:port: %w", err)
+ }
+ if hostOverride != "" {
+ host = hostOverride
+ }
+ if portOverride != "" {
+ port = portOverride
+ }
+ return net.JoinHostPort(host, port), nil
+ }, nil
+}
+
+func newOverrideStreamDialerFromURL(innerDialer transport.StreamDialer, configURL *url.URL) (transport.StreamDialer, error) {
+ override, err := newOverrideFromURL(configURL)
+ if err != nil {
+ return nil, err
+ }
+ return transport.FuncStreamDialer(func(ctx context.Context, addr string) (transport.StreamConn, error) {
+ addr, err := override(addr)
+ if err != nil {
+ return nil, err
+ }
+ return innerDialer.DialStream(ctx, addr)
+ }), nil
+}
+
+func newOverridePacketDialerFromURL(innerDialer transport.PacketDialer, configURL *url.URL) (transport.PacketDialer, error) {
+ override, err := newOverrideFromURL(configURL)
+ if err != nil {
+ return nil, err
+ }
+ return transport.FuncPacketDialer(func(ctx context.Context, addr string) (net.Conn, error) {
+ addr, err := override(addr)
+ if err != nil {
+ return nil, err
+ }
+ return innerDialer.DialPacket(ctx, addr)
+ }), nil
+}
diff --git a/x/config/override_test.go b/x/config/override_test.go
new file mode 100644
index 00000000..6b2024a3
--- /dev/null
+++ b/x/config/override_test.go
@@ -0,0 +1,71 @@
+// Copyright 2024 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package config
+
+import (
+ "net/url"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func Test_newOverrideFromURL(t *testing.T) {
+ t.Run("Host Override", func(t *testing.T) {
+ cfgUrl, err := url.Parse("override:host=www.google.com")
+ require.NoError(t, err)
+ override, err := newOverrideFromURL(cfgUrl)
+ require.NoError(t, err)
+ addr, err := override("www.youtube.com:443")
+ require.NoError(t, err)
+ require.Equal(t, "www.google.com:443", addr)
+ })
+ t.Run("Port Override", func(t *testing.T) {
+ cfgUrl, err := url.Parse("override:port=853")
+ require.NoError(t, err)
+ override, err := newOverrideFromURL(cfgUrl)
+ require.NoError(t, err)
+ addr, err := override("8.8.8.8:53")
+ require.NoError(t, err)
+ require.Equal(t, "8.8.8.8:853", addr)
+ })
+ t.Run("Full Override", func(t *testing.T) {
+ cfgUrl, err := url.Parse("override:host=8.8.8.8&port=853")
+ require.NoError(t, err)
+ override, err := newOverrideFromURL(cfgUrl)
+ require.NoError(t, err)
+ addr, err := override("dns.google:53")
+ require.NoError(t, err)
+ require.Equal(t, "8.8.8.8:853", addr)
+ })
+ t.Run("Invalid address", func(t *testing.T) {
+ t.Run("Host Override", func(t *testing.T) {
+ cfgUrl, err := url.Parse("override:host=www.google.com")
+ require.NoError(t, err)
+ override, err := newOverrideFromURL(cfgUrl)
+ require.NoError(t, err)
+ _, err = override("foo bar")
+ require.Error(t, err)
+ })
+ t.Run("Full Override", func(t *testing.T) {
+ cfgUrl, err := url.Parse("override:host=8.8.8.8&port=853")
+ require.NoError(t, err)
+ override, err := newOverrideFromURL(cfgUrl)
+ require.NoError(t, err)
+ addr, err := override("foo bar")
+ require.NoError(t, err)
+ require.Equal(t, "8.8.8.8:853", addr)
+ })
+ })
+}
diff --git a/x/config/shadowsocks.go b/x/config/shadowsocks.go
index effdc546..e8620a23 100644
--- a/x/config/shadowsocks.go
+++ b/x/config/shadowsocks.go
@@ -72,6 +72,65 @@ type shadowsocksConfig struct {
}
func parseShadowsocksURL(url *url.URL) (*shadowsocksConfig, error) {
+ // attempt to decode as SIP002 URI format and
+ // fall back to legacy base64 format if decoding fails
+ config, err := parseShadowsocksSIP002URL(url)
+ if err == nil {
+ return config, nil
+ }
+ return parseShadowsocksLegacyBase64URL(url)
+}
+
+// parseShadowsocksLegacyBase64URL parses URL based on legacy base64 format:
+// https://shadowsocks.org/doc/configs.html#uri-and-qr-code
+func parseShadowsocksLegacyBase64URL(url *url.URL) (*shadowsocksConfig, error) {
+ config := &shadowsocksConfig{}
+ if url.Host == "" {
+ return nil, errors.New("host not specified")
+ }
+ decoded, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(url.Host)
+ if err != nil {
+ // If decoding fails, return the original url with error
+ return nil, fmt.Errorf("failed to decode host string [%v]: %w", url.String(), err)
+ }
+ var fragment string
+ if url.Fragment != "" {
+ fragment = "#" + url.Fragment
+ } else {
+ fragment = ""
+ }
+ newURL, err := url.Parse(strings.ToLower(url.Scheme) + "://" + string(decoded) + fragment)
+ if err != nil {
+ // if parsing fails, return the original url with error
+ return nil, fmt.Errorf("failed to parse config part: %w", err)
+ }
+ // extend this check to see if decoded string contains contains other valid fields
+ if newURL.User == nil {
+ return nil, fmt.Errorf("invalid user info: %w", err)
+ }
+ cipherInfoBytes := newURL.User.String()
+ cipherName, secret, found := strings.Cut(string(cipherInfoBytes), ":")
+ if !found {
+ return nil, errors.New("invalid cipher info: no ':' separator")
+ }
+ config.serverAddress = newURL.Host
+ config.cryptoKey, err = shadowsocks.NewEncryptionKey(cipherName, secret)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create cipher: %w", err)
+ }
+ prefixStr := newURL.Query().Get("prefix")
+ if len(prefixStr) > 0 {
+ config.prefix, err = parseStringPrefix(prefixStr)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse prefix: %w", err)
+ }
+ }
+ return config, nil
+}
+
+// parseShadowsocksSIP002URL parses URL based on SIP002 format:
+// https://shadowsocks.org/doc/sip002.html
+func parseShadowsocksSIP002URL(url *url.URL) (*shadowsocksConfig, error) {
config := &shadowsocksConfig{}
if url.Host == "" {
return nil, errors.New("host not specified")
@@ -110,3 +169,12 @@ func parseStringPrefix(utf8Str string) ([]byte, error) {
}
return rawBytes, nil
}
+
+func sanitizeShadowsocksURL(u *url.URL) (string, error) {
+ const redactedPlaceholder = "REDACTED"
+ config, err := parseShadowsocksURL(u)
+ if err != nil {
+ return "ss://ERROR", err
+ }
+ return "ss://" + redactedPlaceholder + "@" + config.serverAddress + "?prefix=" + url.PathEscape(string(config.prefix)), nil
+}
diff --git a/x/config/tls.go b/x/config/tls.go
index 7a100c03..0789696e 100644
--- a/x/config/tls.go
+++ b/x/config/tls.go
@@ -20,7 +20,7 @@ import (
"strings"
"github.com/Jigsaw-Code/outline-sdk/transport"
- "github.com/Jigsaw-Code/outline-sdk/x/tls"
+ "github.com/Jigsaw-Code/outline-sdk/transport/tls"
)
func parseOptions(configURL *url.URL) ([]tls.ClientOption, error) {
diff --git a/x/config/tls_test.go b/x/config/tls_test.go
index 2f0ff437..47e20ddc 100644
--- a/x/config/tls_test.go
+++ b/x/config/tls_test.go
@@ -19,14 +19,14 @@ import (
"testing"
"github.com/Jigsaw-Code/outline-sdk/transport"
- "github.com/Jigsaw-Code/outline-sdk/x/tls"
+ "github.com/Jigsaw-Code/outline-sdk/transport/tls"
"github.com/stretchr/testify/require"
)
func TestTLS(t *testing.T) {
tlsURL, err := url.Parse("tls")
require.NoError(t, err)
- _, err = newTlsStreamDialerFromURL(&transport.TCPStreamDialer{}, tlsURL)
+ _, err = newTlsStreamDialerFromURL(&transport.TCPDialer{}, tlsURL)
require.NoError(t, err)
}
@@ -37,7 +37,7 @@ func TestTLS_SNI(t *testing.T) {
require.NoError(t, err)
cfg := tls.ClientConfig{ServerName: "host", CertificateName: "host"}
for _, option := range options {
- option("host", 443, &cfg)
+ option("host", &cfg)
}
require.Equal(t, "www.google.com", cfg.ServerName)
require.Equal(t, "host", cfg.CertificateName)
@@ -50,7 +50,7 @@ func TestTLS_NoSNI(t *testing.T) {
require.NoError(t, err)
cfg := tls.ClientConfig{ServerName: "host", CertificateName: "host"}
for _, option := range options {
- option("host", 443, &cfg)
+ option("host", &cfg)
}
require.Equal(t, "", cfg.ServerName)
require.Equal(t, "host", cfg.CertificateName)
@@ -70,7 +70,7 @@ func TestTLS_CertName(t *testing.T) {
require.NoError(t, err)
cfg := tls.ClientConfig{ServerName: "host", CertificateName: "host"}
for _, option := range options {
- option("host", 443, &cfg)
+ option("host", &cfg)
}
require.Equal(t, "host", cfg.ServerName)
require.Equal(t, "www.google.com", cfg.CertificateName)
@@ -83,7 +83,7 @@ func TestTLS_Combined(t *testing.T) {
require.NoError(t, err)
cfg := tls.ClientConfig{ServerName: "host", CertificateName: "host"}
for _, option := range options {
- option("host", 443, &cfg)
+ option("host", &cfg)
}
require.Equal(t, "sni.example.com", cfg.ServerName)
require.Equal(t, "certname.example.com", cfg.CertificateName)
diff --git a/x/connectivity/connectivity.go b/x/connectivity/connectivity.go
index 77f2dba0..c6d26124 100644
--- a/x/connectivity/connectivity.go
+++ b/x/connectivity/connectivity.go
@@ -18,17 +18,16 @@ import (
"context"
"errors"
"fmt"
- "net"
"syscall"
"time"
- "github.com/Jigsaw-Code/outline-sdk/transport"
- "github.com/miekg/dns"
+ "github.com/Jigsaw-Code/outline-sdk/dns"
+ "golang.org/x/net/dns/dnsmessage"
)
-// TestError captures the observed error of the connectivity test.
-type TestError struct {
- // Which operation in the test that failed: "dial", "write" or "read"
+// ConnectivityError captures the observed error of the connectivity test.
+type ConnectivityError struct {
+ // Which operation in the test that failed: "connect", "send" or "receive"
Op string
// The POSIX error, when available
PosixError string
@@ -36,34 +35,25 @@ type TestError struct {
Err error
}
-var _ error = (*TestError)(nil)
+var _ error = (*ConnectivityError)(nil)
-func (err *TestError) Error() string {
+func (err *ConnectivityError) Error() string {
return fmt.Sprintf("%v: %v", err.Op, err.Err)
}
-func (err *TestError) Unwrap() error {
+func (err *ConnectivityError) Unwrap() error {
return err.Err
}
-// TestResolverStreamConnectivity uses the given [transport.StreamEndpoint] to connect to a DNS resolver and resolve the test domain.
-// The context can be used to set a timeout or deadline, or to pass values to the dialer.
-func TestResolverStreamConnectivity(ctx context.Context, resolver transport.StreamEndpoint, testDomain string) (time.Duration, error) {
- return testResolver(ctx, resolver.Connect, testDomain)
-}
-
-// TestResolverPacketConnectivity uses the given [transport.PacketEndpoint] to connect to a DNS resolver and resolve the test domain.
-// The context can be used to set a timeout or deadline, or to pass values to the listener.
-func TestResolverPacketConnectivity(ctx context.Context, resolver transport.PacketEndpoint, testDomain string) (time.Duration, error) {
- return testResolver(ctx, resolver.Connect, testDomain)
-}
-
func isTimeout(err error) bool {
var timeErr interface{ Timeout() bool }
return errors.As(err, &timeErr) && timeErr.Timeout()
}
-func makeTestError(op string, err error) error {
+func makeConnectivityError(op string, err error) *ConnectivityError {
+ // An early close on the connection may cause an "unexpected EOF" error. That's an application-layer error,
+ // not triggered by a syscall error so we don't capture an error code.
+ // TODO: figure out how to standardize on those errors.
var code string
var errno syscall.Errno
if errors.As(err, &errno) {
@@ -71,45 +61,39 @@ func makeTestError(op string, err error) error {
} else if isTimeout(err) {
code = "ETIMEDOUT"
}
- return &TestError{Op: op, PosixError: code, Err: err}
+ return &ConnectivityError{Op: op, PosixError: code, Err: err}
}
-func testResolver[C net.Conn](ctx context.Context, connect func(context.Context) (C, error), testDomain string) (time.Duration, error) {
- deadline, ok := ctx.Deadline()
- if !ok {
+// TestConnectivityWithResolver tests weather we can get a response from the given [Resolver]. It can be used
+// to test connectivity of its underlying [transport.StreamDialer] or [transport.PacketDialer].
+// Invalid tests that cannot assert connectivity will return (nil, error).
+// Valid tests will return (*ConnectivityError, nil), where *ConnectivityError will be nil if there's connectivity or
+// a structure with details of the error found.
+func TestConnectivityWithResolver(ctx context.Context, resolver dns.Resolver, testDomain string) (*ConnectivityError, error) {
+ if _, ok := ctx.Deadline(); !ok {
// Default deadline is 5 seconds.
- deadline = time.Now().Add(5 * time.Second)
+ deadline := time.Now().Add(5 * time.Second)
var cancel context.CancelFunc
ctx, cancel = context.WithDeadline(ctx, deadline)
// Releases the timer.
defer cancel()
}
- testTime := time.Now()
- testErr := func() error {
- conn, dialErr := connect(ctx)
- if dialErr != nil {
- return makeTestError("dial", dialErr)
- }
- defer conn.Close()
- conn.SetDeadline(deadline)
- dnsConn := dns.Conn{Conn: conn}
+ q, err := dns.NewQuestion(testDomain, dnsmessage.TypeA)
+ if err != nil {
+ return nil, fmt.Errorf("question creation failed: %w", err)
+ }
- var dnsRequest dns.Msg
- dnsRequest.SetQuestion(dns.Fqdn(testDomain), dns.TypeA)
- writeErr := dnsConn.WriteMsg(&dnsRequest)
- if writeErr != nil {
- return makeTestError("write", writeErr)
- }
+ _, err = resolver.Query(ctx, *q)
- _, readErr := dnsConn.ReadMsg()
- if readErr != nil {
- // An early close on the connection may cause a "unexpected EOF" error. That's an application-layer error,
- // not triggered by a syscall error so we don't capture an error code.
- // TODO: figure out how to standardize on those errors.
- return makeTestError("read", readErr)
- }
- return nil
- }()
- duration := time.Since(testTime)
- return duration, testErr
+ if errors.Is(err, dns.ErrBadRequest) {
+ return nil, err
+ }
+ if errors.Is(err, dns.ErrDial) {
+ return makeConnectivityError("connect", err), nil
+ } else if errors.Is(err, dns.ErrSend) {
+ return makeConnectivityError("send", err), nil
+ } else if errors.Is(err, dns.ErrReceive) {
+ return makeConnectivityError("receive", err), nil
+ }
+ return nil, nil
}
diff --git a/x/connectivity/connectivity_test.go b/x/connectivity/connectivity_test.go
index 6fca8883..ee607633 100644
--- a/x/connectivity/connectivity_test.go
+++ b/x/connectivity/connectivity_test.go
@@ -26,18 +26,20 @@ import (
"testing"
"time"
+ "github.com/Jigsaw-Code/outline-sdk/dns"
"github.com/Jigsaw-Code/outline-sdk/transport"
- "github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "golang.org/x/net/dns/dnsmessage"
)
// StreamDialer Tests
func TestTestResolverStreamConnectivityOk(t *testing.T) {
// TODO(fortuna): Run a local resolver and make test not depend on an external server.
- resolver := &transport.TCPEndpoint{Address: "8.8.8.8:53"}
- _, err := TestResolverStreamConnectivity(context.Background(), resolver, "example.com")
+ resolver := dns.NewTCPResolver(&transport.TCPDialer{}, "8.8.8.8:53")
+ result, err := TestConnectivityWithResolver(context.Background(), resolver, "example.com")
require.NoError(t, err)
+ require.Nil(t, result)
}
// TODO: Move this to the SDK.
@@ -69,15 +71,16 @@ func TestTestResolverStreamConnectivityRefused(t *testing.T) {
// Close right away to ensure the port is closed. The OS will likely not reuse it soon enough.
require.Nil(t, listener.Close())
- resolver := &transport.TCPEndpoint{Address: listener.Addr().String()}
- _, err = TestResolverStreamConnectivity(context.Background(), resolver, "anything")
- var testErr *TestError
- require.ErrorAs(t, err, &testErr)
- require.Equal(t, "dial", testErr.Op)
- require.Equal(t, "ECONNREFUSED", testErr.PosixError)
+ resolver := dns.NewTCPResolver(&transport.TCPDialer{}, listener.Addr().String())
+ result, err := TestConnectivityWithResolver(context.Background(), resolver, "anything")
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ require.ErrorIs(t, result.Err, dns.ErrDial)
+ require.Equal(t, "connect", result.Op)
+ require.Equal(t, "ECONNREFUSED", result.PosixError)
var sysErr *os.SyscallError
- require.ErrorAs(t, err, &sysErr)
+ require.ErrorAs(t, result.Err, &sysErr)
expectedSyscall := "connect"
if runtime.GOOS == "windows" {
expectedSyscall = "connectex"
@@ -85,7 +88,7 @@ func TestTestResolverStreamConnectivityRefused(t *testing.T) {
require.Equal(t, expectedSyscall, sysErr.Syscall)
var errno syscall.Errno
- require.ErrorAs(t, sysErr.Err, &errno)
+ require.ErrorAs(t, result.Err, &errno)
require.Equal(t, "ECONNREFUSED", errnoName(errno))
}
@@ -104,16 +107,16 @@ func TestTestResolverStreamConnectivityReset(t *testing.T) {
}, &running)
defer listener.Close()
- resolver := &transport.TCPEndpoint{Address: listener.Addr().String()}
- _, err := TestResolverStreamConnectivity(context.Background(), resolver, "anything")
-
- var testErr *TestError
- require.ErrorAs(t, err, &testErr)
- require.Equalf(t, "read", testErr.Op, "Wrong test operation. Error: %v", testErr.Err)
- require.Equal(t, "ECONNRESET", testErr.PosixError)
+ resolver := dns.NewTCPResolver(&transport.TCPDialer{}, listener.Addr().String())
+ result, err := TestConnectivityWithResolver(context.Background(), resolver, "anything")
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ require.Equalf(t, "receive", result.Op, "Wrong test operation. Error: %v", result.Err)
+ require.ErrorIs(t, result.Err, dns.ErrReceive)
+ require.Equal(t, "ECONNRESET", result.PosixError)
var sysErr *os.SyscallError
- require.ErrorAs(t, err, &sysErr)
+ require.ErrorAs(t, result.Err, &sysErr)
expectedSyscall := "read"
if runtime.GOOS == "windows" {
expectedSyscall = "wsarecv"
@@ -121,7 +124,7 @@ func TestTestResolverStreamConnectivityReset(t *testing.T) {
require.Equalf(t, expectedSyscall, sysErr.Syscall, "Wrong system call. Error: %v", sysErr)
var errno syscall.Errno
- require.ErrorAs(t, err, &errno)
+ require.ErrorAs(t, result.Err, &errno)
require.Equal(t, "ECONNRESET", errnoName(errno))
}
@@ -136,17 +139,17 @@ func TestTestStreamDialerEarlyClose(t *testing.T) {
}, &running)
defer listener.Close()
- resolver := &transport.TCPEndpoint{Address: listener.Addr().String()}
- _, err := TestResolverStreamConnectivity(context.Background(), resolver, "anything")
-
- var testErr *TestError
- require.ErrorAs(t, err, &testErr)
- require.Equalf(t, "read", testErr.Op, "Wrong test operation. Error: %v", testErr.Err)
- require.Equal(t, "", testErr.PosixError)
- require.Error(t, err, "unexpected EOF")
+ resolver := dns.NewTCPResolver(&transport.TCPDialer{}, listener.Addr().String())
+ result, err := TestConnectivityWithResolver(context.Background(), resolver, "anything")
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ require.Equalf(t, "receive", result.Op, "Wrong test operation. Error: %v", result.Err)
+ require.Equal(t, "", result.PosixError)
+ require.ErrorIs(t, result.Err, dns.ErrReceive)
+ require.ErrorIs(t, result.Err, io.EOF)
var sysErr *os.SyscallError
- require.False(t, errors.As(err, &sysErr))
+ require.False(t, errors.As(result.Err, &sysErr))
}
func TestTestResolverStreamConnectivityTimeout(t *testing.T) {
@@ -161,16 +164,17 @@ func TestTestResolverStreamConnectivityTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
- resolver := &transport.TCPEndpoint{Address: listener.Addr().String()}
- _, err := TestResolverStreamConnectivity(ctx, resolver, "anything")
+ resolver := dns.NewTCPResolver(&transport.TCPDialer{}, listener.Addr().String())
+ result, err := TestConnectivityWithResolver(ctx, resolver, "anything")
+ require.NoError(t, err)
+ require.NotNil(t, result)
- var testErr *TestError
- require.ErrorAs(t, err, &testErr)
- assert.Equalf(t, "read", testErr.Op, "Wrong test operation. Error: %v", testErr.Err)
+ assert.Equalf(t, "receive", result.Op, "Wrong test operation. Error: %v", result.Err)
- assert.ErrorContains(t, err, "i/o timeout")
- assert.True(t, isTimeout(err))
- assert.Equalf(t, "ETIMEDOUT", testErr.PosixError, "Wrong posix error code. Error: %#v, %v", testErr.Err, testErr.Err.Error())
+ require.ErrorIs(t, result.Err, dns.ErrReceive)
+ assert.ErrorContains(t, result.Err, "i/o timeout")
+ assert.True(t, isTimeout(result.Err))
+ assert.Equalf(t, "ETIMEDOUT", result.PosixError, "Wrong posix error code. Error: %#v, %v", result.Err, result.Err.Error())
timeout.Done()
listener.Close()
@@ -188,21 +192,22 @@ func TestTestPacketPacketConnectivityOk(t *testing.T) {
buf := make([]byte, 512)
n, clientAddr, err := server.ReadFrom(buf)
require.NoError(t, err)
- var request dns.Msg
+ var request dnsmessage.Message
err = request.Unpack(buf[:n])
require.NoError(t, err)
- var response dns.Msg
- response.SetReply(&request)
- responseBytes, err := response.Pack()
+ request.Response = true
+ request.RecursionAvailable = true
+ responseBytes, err := request.AppendPack(buf[0:0])
require.NoError(t, err)
_, err = server.WriteTo(responseBytes, clientAddr)
require.NoError(t, err)
}()
- resolver := &transport.UDPEndpoint{Address: server.LocalAddr().String()}
- _, err = TestResolverPacketConnectivity(context.Background(), resolver, "example.com")
+ resolver := dns.NewUDPResolver(&transport.UDPDialer{}, server.LocalAddr().String())
+ result, err := TestConnectivityWithResolver(context.Background(), resolver, "anything")
require.NoError(t, err)
+ require.Nil(t, result)
}
// TODO: Add more tests
diff --git a/x/examples/fetch/main.go b/x/examples/fetch/main.go
index b3c7dd3c..7c1e08e6 100644
--- a/x/examples/fetch/main.go
+++ b/x/examples/fetch/main.go
@@ -42,35 +42,61 @@ func init() {
func main() {
verboseFlag := flag.Bool("v", false, "Enable debug output")
transportFlag := flag.String("transport", "", "Transport config")
+ addressFlag := flag.String("address", "", "Address to connect to. If empty, use the URL authority")
+ methodFlag := flag.String("method", "GET", "The HTTP method to use")
flag.Parse()
if *verboseFlag {
debugLog = *log.New(os.Stderr, "[DEBUG] ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
}
+ var overrideHost, overridePort string
+ if *addressFlag != "" {
+ var err error
+ overrideHost, overridePort, err = net.SplitHostPort(*addressFlag)
+ if err != nil {
+ // Fail to parse. Assume the flag is host only.
+ overrideHost = *addressFlag
+ overridePort = ""
+ }
+ }
url := flag.Arg(0)
if url == "" {
- log.Print("Need to pass the URL to fetch in the command-line")
+ log.Println("Need to pass the URL to fetch in the command-line")
flag.Usage()
os.Exit(1)
}
dialer, err := config.NewStreamDialer(*transportFlag)
if err != nil {
- log.Fatalf("Could not create dialer: %v", err)
+ log.Fatalf("Could not create dialer: %v\n", err)
}
dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
+ host, port, err := net.SplitHostPort(addr)
+ if err != nil {
+ return nil, fmt.Errorf("invalid address: %w", err)
+ }
+ if overrideHost != "" {
+ host = overrideHost
+ }
+ if overridePort != "" {
+ port = overridePort
+ }
if !strings.HasPrefix(network, "tcp") {
return nil, fmt.Errorf("protocol not supported: %v", network)
}
- return dialer.Dial(ctx, addr)
+ return dialer.DialStream(ctx, net.JoinHostPort(host, port))
}
httpClient := &http.Client{Transport: &http.Transport{DialContext: dialContext}, Timeout: 5 * time.Second}
- resp, err := httpClient.Get(url)
+ req, err := http.NewRequest(*methodFlag, url, nil)
+ if err != nil {
+ log.Fatalln("Failed to create request:", err)
+ }
+ resp, err := httpClient.Do(req)
if err != nil {
- log.Fatalf("URL GET failed: %v", err)
+ log.Fatalf("HTTP request failed: %v\n", err)
}
defer resp.Body.Close()
@@ -81,7 +107,8 @@ func main() {
}
_, err = io.Copy(os.Stdout, resp.Body)
+ fmt.Println()
if err != nil {
- log.Fatalf("Read of page body failed: %v", err)
+ log.Fatalf("Read of page body failed: %v\n", err)
}
}
diff --git a/x/examples/fyne-proxy/FyneApp.toml b/x/examples/fyne-proxy/FyneApp.toml
new file mode 100644
index 00000000..e8a5364d
--- /dev/null
+++ b/x/examples/fyne-proxy/FyneApp.toml
@@ -0,0 +1,5 @@
+[Details]
+ Icon = "Icon.png"
+ Name = "Local Proxy"
+ ID = "org.getoutline.sdk.examples.fyne_proxy"
+ Build = 1
diff --git a/x/examples/fyne-proxy/Icon.png b/x/examples/fyne-proxy/Icon.png
new file mode 100644
index 00000000..9fe32c03
Binary files /dev/null and b/x/examples/fyne-proxy/Icon.png differ
diff --git a/x/examples/fyne-proxy/README.md b/x/examples/fyne-proxy/README.md
new file mode 100644
index 00000000..a901e1a5
--- /dev/null
+++ b/x/examples/fyne-proxy/README.md
@@ -0,0 +1,83 @@
+# Local Proxy with Fyne
+
+This folder has a graphical application that runs a local proxy given a address and configuration.
+It uses [Fyne](https://fyne.io/) for the UI.
+
+
+
+
+
+You can configure your system to use the proxy, as per the instructions below:
+
+- [Windows](https://support.microsoft.com/en-us/windows/use-a-proxy-server-in-windows-03096c53-0554-4ffe-b6ab-8b1deee8dae1)
+- [macOS](https://support.apple.com/guide/mac-help/change-proxy-settings-on-mac-mchlp2591/mac)
+- [Ubuntu](https://help.ubuntu.com/stable/ubuntu-help/net-proxy.html.en)
+- [Other systems and browsers](https://www.avast.com/c-how-to-set-up-a-proxy) (disregard the Avast ads)
+
+## Network Mode
+By default the proxy runs on `localhost`, meaning only your host can access it. You can change the address to your local network IP address, and
+that will make the proxy available to all devices on the network. Consider the fact that anyone can find and access your server before running
+it in local network mode in a public network or a network you don't trust.
+
+## Global Mode
+We don't recommend using 0.0.0.0, since that may open up your machine to the outside, and currently there's no encryption or authentication to protect the access.
+
+## Desktop
+
+You can run the app without explicitly cloning the repository with:
+
+```sh
+go run github.com/Jigsaw-Code/outline-sdk/x/examples/fyne-proxy@latest
+```
+
+To run the local version while developing, from the `fyne-proxy` directory:
+
+```sh
+go run .
+```
+
+To package, from the app folder:
+
+```sh
+go run fyne.io/fyne/v2/cmd/fyne package
+```
+
+
+## Android
+
+To run the app, start the emulator, call fyne install to build and install the app. See https://developer.android.com/studio/run/emulator-commandline
+
+```sh
+# Point ANDROID_NDK_HOME to the right location
+export ANDROID_NDK_HOME="$HOME/Library/Android/sdk/ndk/26.1.10909125"
+# Start the emulator.
+$ANDROID_HOME/emulator/emulator -no-boot-anim -avd Pixel_3a_API_33_arm64-v8a
+go run fyne.io/fyne/v2/cmd/fyne install -os android
+```
+
+If you need, you can build the APK, then install like this:
+
+```sh
+go run fyne.io/fyne/v2/cmd/fyne package -os android -appID com.example.myapp
+$ANDROID_HOME/platform-tools/adb install ./Local_Proxy.apk
+```
+
+## iOS
+
+Install on a running simulator:
+
+```sh
+go run fyne.io/fyne/v2/cmd/fyne install -os iossimulator
+```
+
+If you package it first, you can install the .app with:
+
+```sh
+xcrun simctl install booted ./Local_Proxy.app
+```
+
+To install on a real device, you need `ios-deploy` (`brew install ios-deploy`). After you connect your phone, run:
+
+```sh
+go run fyne.io/fyne/v2/cmd/fyne install -os ios
+```
diff --git a/x/examples/fyne-proxy/go.mod b/x/examples/fyne-proxy/go.mod
new file mode 100644
index 00000000..72cf65f1
--- /dev/null
+++ b/x/examples/fyne-proxy/go.mod
@@ -0,0 +1,59 @@
+module github.com/Jigsaw-Code/outline-sdk/x/examples/fyne-proxy
+
+go 1.20
+
+require (
+ fyne.io/fyne/v2 v2.4.3
+ github.com/Jigsaw-Code/outline-sdk v0.0.12-0.20240117212231-233d1898e1db
+ github.com/Jigsaw-Code/outline-sdk/x v0.0.0-20240117212231-233d1898e1db
+)
+
+require (
+ fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e // indirect
+ github.com/BurntSushi/toml v1.3.2 // indirect
+ github.com/akavel/rsrc v0.10.2 // indirect
+ github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/fogleman/gg v1.3.0 // indirect
+ github.com/fredbi/uri v1.0.0 // indirect
+ github.com/fsnotify/fsnotify v1.6.0 // indirect
+ github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect
+ github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504 // indirect
+ github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect
+ github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect
+ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect
+ github.com/go-ole/go-ole v1.2.6 // indirect
+ github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8 // indirect
+ github.com/go-text/typesetting v0.0.0-20230616162802-9c17dd34aa4a // indirect
+ github.com/godbus/dbus/v5 v5.1.0 // indirect
+ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
+ github.com/gopherjs/gopherjs v1.17.2 // indirect
+ github.com/jackmordaunt/icns/v2 v2.2.6 // indirect
+ github.com/josephspurrier/goversioninfo v1.4.0 // indirect
+ github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
+ github.com/lucor/goinfo v0.9.0 // indirect
+ github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 // indirect
+ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
+ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
+ github.com/shadowsocks/go-shadowsocks2 v0.1.5 // indirect
+ github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
+ github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
+ github.com/stretchr/testify v1.8.4 // indirect
+ github.com/tevino/abool v1.2.0 // indirect
+ github.com/urfave/cli/v2 v2.11.1 // indirect
+ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
+ github.com/yuin/goldmark v1.5.5 // indirect
+ golang.org/x/crypto v0.17.0 // indirect
+ golang.org/x/image v0.14.0 // indirect
+ golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
+ golang.org/x/mod v0.14.0 // indirect
+ golang.org/x/net v0.19.0 // indirect
+ golang.org/x/sys v0.15.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ golang.org/x/tools v0.16.0 // indirect
+ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect
+)
diff --git a/x/examples/fyne-proxy/go.sum b/x/examples/fyne-proxy/go.sum
new file mode 100644
index 00000000..7074a9e3
--- /dev/null
+++ b/x/examples/fyne-proxy/go.sum
@@ -0,0 +1,698 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
+cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
+cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
+cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
+cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+fyne.io/fyne/v2 v2.4.3 h1:v2wncjEAcwXZ8UNmTCWTGL9+sGyPc5RuzBvM96GcC78=
+fyne.io/fyne/v2 v2.4.3/go.mod h1:1h3BKxmQYRJlr2g+RGVxedzr6vLVQ/AJmFWcF9CJnoQ=
+fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e h1:Hvs+kW2VwCzNToF3FmnIAzmivNgrclwPgoUdVSrjkP8=
+fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e/go.mod h1:oM2AQqGJ1AMo4nNqZFYU8xYygSBZkW2hmdJ7n4yjedE=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
+github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/Jigsaw-Code/outline-sdk v0.0.12-0.20240117212231-233d1898e1db h1:I1guGrgFXY/w+YWt7QNcb3nrTNts5opuPO44XlRF0xI=
+github.com/Jigsaw-Code/outline-sdk v0.0.12-0.20240117212231-233d1898e1db/go.mod h1:FtzQwsbvAT55lpc4kmOaHyvfX8MFW8y7yOHL81wHOVQ=
+github.com/Jigsaw-Code/outline-sdk/x v0.0.0-20240117212231-233d1898e1db h1:pVN7fcEihOgzIUVBn92y84HsXrZlaHRkUgnm7Rn7lUo=
+github.com/Jigsaw-Code/outline-sdk/x v0.0.0-20240117212231-233d1898e1db/go.mod h1:VVVqAev7l5HwkQVZfs79UrXWtmU3rq76+PLAhkSvFRs=
+github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw=
+github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
+github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
+github.com/fredbi/uri v1.0.0 h1:s4QwUAZ8fz+mbTsukND+4V5f+mJ/wjaTokwstGUAemg=
+github.com/fredbi/uri v1.0.0/go.mod h1:1xC40RnIOGCaQzswaOvrzvG/3M3F0hyDVb3aO/1iGy0=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
+github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
+github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe h1:A/wiwvQ0CAjPkuJytaD+SsXkPU0asQ+guQEIg1BJGX4=
+github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe/go.mod h1:d4clgH0/GrRwWjRzJJQXxT/h1TyuNSfF/X64zb/3Ggg=
+github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504 h1:+31CdF/okdokeFNoy9L/2PccG3JFidQT3ev64/r4pYU=
+github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504/go.mod h1:gLRWYfYnMA9TONeppRSikMdXlHQ97xVsPojddUv3b/E=
+github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 h1:hnLq+55b7Zh7/2IRzWCpiTcAvjv/P8ERF+N7+xXbZhk=
+github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2/go.mod h1:eO7W361vmlPOrykIg+Rsh1SZ3tQBaOsfzZhsIOb/Lm0=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk=
+github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8 h1:VkKnvzbvHqgEfm351rfr8Uclu5fnwq8HP2ximUzJsBM=
+github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8/go.mod h1:h29xCucjNsDcYb7+0rJokxVwYAq+9kQ19WiFuBKkYtc=
+github.com/go-text/typesetting v0.0.0-20230616162802-9c17dd34aa4a h1:VjN8ttdfklC0dnAdKbZqGNESdERUxtE3l8a/4Grgarc=
+github.com/go-text/typesetting v0.0.0-20230616162802-9c17dd34aa4a/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
+github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gopherjs/gopherjs v0.0.0-20211219123610-ec9572f70e60/go.mod h1:cz9oNYuRUWGdHmLF2IodMLkAhcPtXeULvcBNagUrxTI=
+github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
+github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
+github.com/goxjs/gl v0.0.0-20210104184919-e3fafc6f8f2a/go.mod h1:dy/f2gjY09hwVfIyATps4G2ai7/hLwLkc5TrPqONuXY=
+github.com/goxjs/glfw v0.0.0-20191126052801-d2efb5f20838/go.mod h1:oS8P8gVOT4ywTcjV6wZlOU4GuVFQ8F5328KY3MJ79CY=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
+github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
+github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
+github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
+github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
+github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jackmordaunt/icns/v2 v2.2.6 h1:M7kg6pWRmB+SyCvM058cV2BlAz3MedOHy4e3j2i7FQg=
+github.com/jackmordaunt/icns/v2 v2.2.6/go.mod h1:DqlVnR5iafSphrId7aSD06r3jg0KRC9V6lEBBp504ZQ=
+github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8=
+github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e h1:LvL4XsI70QxOGHed6yhQtAU34Kx3Qq2wwBzGFKY8zKk=
+github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/lucor/goinfo v0.9.0 h1:EdsMzmY5TZujA4xb9xMLIdlp2+zvF7miNYkVXvqqgOQ=
+github.com/lucor/goinfo v0.9.0/go.mod h1:L6m6tN5Rlova5Z83h1ZaKsMP1iiaoZ9vGTNzu5QKOD4=
+github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 h1:YocNLcTBdEdvY3iDK6jfWXvEaM5OCKkjxPKoJRdB3Gg=
+github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo=
+github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
+github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
+github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
+github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
+github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28=
+github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM=
+github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
+github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
+github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
+github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
+github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
+github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
+github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
+github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
+github.com/urfave/cli/v2 v2.11.1 h1:UKK6SP7fV3eKOefbS87iT9YHefv7iB/53ih6e+GNAsE=
+github.com/urfave/cli/v2 v2.11.1/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo=
+github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
+github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.5.5 h1:IJznPe8wOzfIKETmMkd06F8nXkmlhaHqFRM9l1hAGsU=
+github.com/yuin/goldmark v1.5.5/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
+go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
+go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
+golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
+golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
+golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg=
+golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
+golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
+golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM=
+golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
+google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
+google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
+google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
+google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
+google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 h1:oomkgU6VaQDsV6qZby2uz1Lap0eXmku8+2em3A/l700=
+honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2/go.mod h1:sUMDUKNB2ZcVjt92UnLy3cdGs+wDAcrPdV3JP6sVgA4=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/x/examples/fyne-proxy/main.go b/x/examples/fyne-proxy/main.go
new file mode 100644
index 00000000..9de6c345
--- /dev/null
+++ b/x/examples/fyne-proxy/main.go
@@ -0,0 +1,205 @@
+// Copyright 2024 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "fmt"
+ "image/color"
+ "log"
+ "net"
+ "net/http"
+ "syscall"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/app"
+ "fyne.io/fyne/v2/canvas"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/layout"
+ "fyne.io/fyne/v2/theme"
+ "fyne.io/fyne/v2/widget"
+ "github.com/Jigsaw-Code/outline-sdk/transport"
+ "github.com/Jigsaw-Code/outline-sdk/x/config"
+ "github.com/Jigsaw-Code/outline-sdk/x/httpproxy"
+)
+
+type runningProxy struct {
+ server *http.Server
+ Address string
+}
+
+func (p *runningProxy) Close() {
+ p.server.Close()
+}
+
+// newFilteredStreamDialer creates a direct [transport.StreamDialer] that blocks
+// non public IPs to prevent access to localhost or the local network.
+func newFilteredStreamDialer() transport.StreamDialer {
+ var dialer net.Dialer
+ dialer.Control = func(network, address string, c syscall.RawConn) error {
+ host, _, err := net.SplitHostPort(address)
+ if err != nil {
+ return fmt.Errorf("failed to parse address: %w", err)
+ }
+ if ip := net.ParseIP(host); ip != nil {
+ if !ip.IsGlobalUnicast() {
+ return fmt.Errorf("addresses that are not global unicast are fobidden")
+ }
+ if ip.IsPrivate() {
+ return fmt.Errorf("private addresses are forbidden")
+ }
+ }
+ return nil
+ }
+ return &transport.TCPDialer{Dialer: dialer}
+}
+
+func runServer(address, transport string) (*runningProxy, error) {
+ // TODO: block localhost, maybe local net.
+ dialer, err := config.WrapStreamDialer(newFilteredStreamDialer(), transport)
+ if err != nil {
+ return nil, fmt.Errorf("could not create dialer: %w", err)
+ }
+
+ listener, err := net.Listen("tcp", address)
+ if err != nil {
+ return nil, fmt.Errorf("could not listen on address %v: %w", address, err)
+ }
+
+ server := http.Server{Handler: httpproxy.NewProxyHandler(dialer)}
+ go func() {
+ if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
+ log.Printf("Serve failed: %v\n", err)
+ }
+ }()
+ return &runningProxy{server: &server, Address: listener.Addr().String()}, nil
+}
+
+type appTheme struct {
+ fyne.Theme
+}
+
+const ColorNameOnPrimary = "OnPrimary"
+
+func (t *appTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
+ switch name {
+ case theme.ColorNameHeaderBackground:
+ return t.Color(theme.ColorNamePrimary, variant)
+ case theme.ColorNamePrimary:
+ if variant == theme.VariantLight {
+ return color.RGBA{R: 0x00, G: 0x67, B: 0x7F, A: 255}
+ } else {
+ return color.RGBA{R: 0x7C, G: 0xD2, B: 0xF0, A: 255}
+ }
+ case ColorNameOnPrimary:
+ if variant == theme.VariantLight {
+ return color.White
+ } else {
+ return color.RGBA{R: 0x00, G: 0x35, B: 0x43, A: 255}
+ }
+ default:
+ return t.Theme.Color(name, variant)
+ }
+}
+
+func makeAppHeader(title string) *fyne.Container {
+ titleLabel := &widget.RichText{Scroll: container.ScrollNone, Segments: []widget.RichTextSegment{
+ &widget.TextSegment{Text: title, Style: widget.RichTextStyle{
+ Alignment: fyne.TextAlignCenter,
+ ColorName: ColorNameOnPrimary,
+ SizeName: theme.SizeNameHeadingText,
+ TextStyle: fyne.TextStyle{Bold: true},
+ }},
+ }}
+ return container.NewStack(canvas.NewRectangle(theme.HeaderBackgroundColor()), titleLabel)
+}
+
+func main() {
+ fyneApp := app.New()
+ if meta := fyneApp.Metadata(); meta.Name == "" {
+ // App not packaged, probably from `go run`.
+ meta.Name = "Local Proxy"
+ app.SetMetadata(meta)
+ }
+ fyneApp.Settings().SetTheme(&appTheme{theme.DefaultTheme()})
+
+ mainWin := fyneApp.NewWindow(fyneApp.Metadata().Name)
+ mainWin.Resize(fyne.Size{Width: 350})
+
+ addressEntry := widget.NewEntry()
+ addressEntry.SetPlaceHolder("Enter proxy local address")
+ addressEntry.Text = "localhost:8080"
+
+ configEntry := widget.NewMultiLineEntry()
+ configEntry.Wrapping = fyne.TextWrapBreak
+ configEntry.SetPlaceHolder("Enter transport config")
+
+ statusBox := widget.NewLabel("")
+ statusBox.Wrapping = fyne.TextWrapWord
+
+ startStopButton := widget.NewButton("", func() {})
+ startStopButton.Importance = widget.HighImportance
+ setProxyUI := func(proxy *runningProxy, err error) {
+ if proxy != nil {
+ statusBox.SetText("Proxy listening on " + proxy.Address)
+ addressEntry.Disable()
+ configEntry.Disable()
+ startStopButton.SetText("Stop")
+ startStopButton.SetIcon(theme.MediaStopIcon())
+ return
+ }
+ if err != nil {
+ statusBox.SetText("❌ ERROR: " + err.Error())
+ } else {
+ statusBox.SetText("Proxy not running")
+ }
+ addressEntry.Enable()
+ configEntry.Enable()
+ startStopButton.SetText("Start")
+ startStopButton.SetIcon(theme.MediaPlayIcon())
+ }
+ var proxy *runningProxy
+ startStopButton.OnTapped = func() {
+ log.Println(startStopButton.Text)
+ var err error
+ if proxy == nil {
+ // Start proxy.
+ proxy, err = runServer(addressEntry.Text, configEntry.Text)
+ } else {
+ // Stop proxy
+ proxy.Close()
+ proxy = nil
+ }
+ setProxyUI(proxy, err)
+ }
+ setProxyUI(proxy, nil)
+
+ content := container.NewVBox(
+ makeAppHeader(fyneApp.Metadata().Name),
+ container.NewPadded(
+ container.NewVBox(
+ widget.NewLabelWithStyle("Local address", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
+ addressEntry,
+ widget.NewRichTextFromMarkdown("**Transport config** ([format](https://pkg.go.dev/github.com/Jigsaw-Code/outline-sdk/x/config#hdr-Config_Format))"),
+ configEntry,
+ container.NewHBox(layout.NewSpacer(), startStopButton),
+ widget.NewLabelWithStyle("Status", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
+ statusBox,
+ ),
+ ),
+ )
+ mainWin.SetContent(content)
+ mainWin.Show()
+ fyneApp.Run()
+}
diff --git a/x/examples/fyne-proxy/tools.go b/x/examples/fyne-proxy/tools.go
new file mode 100644
index 00000000..02519041
--- /dev/null
+++ b/x/examples/fyne-proxy/tools.go
@@ -0,0 +1,25 @@
+// Copyright 2024 Jigsaw Operations LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build tools
+// +build tools
+
+// See https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module
+// and https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md
+
+package tools
+
+import (
+ _ "fyne.io/fyne/v2/cmd/fyne"
+)
diff --git a/x/examples/http2transport/README.md b/x/examples/http2transport/README.md
index c7fe1858..0e02c8fb 100644
--- a/x/examples/http2transport/README.md
+++ b/x/examples/http2transport/README.md
@@ -9,5 +9,5 @@ Flags:
Example:
```
KEY=ss://ENCRYPTION_KEY@HOST:PORT/
-go run github.com/Jigsaw-Code/outline-sdk/x/examples/http2transport@latest -transport "$KEY" -addr localhost:54321
+go run github.com/Jigsaw-Code/outline-sdk/x/examples/http2transport@latest -transport "$KEY" -localAddr localhost:54321
```
diff --git a/x/examples/outline-cli/outline_packet_proxy.go b/x/examples/outline-cli/outline_packet_proxy.go
index 3fdfe672..2c58362d 100644
--- a/x/examples/outline-cli/outline_packet_proxy.go
+++ b/x/examples/outline-cli/outline_packet_proxy.go
@@ -18,6 +18,7 @@ import (
"context"
"fmt"
+ "github.com/Jigsaw-Code/outline-sdk/dns"
"github.com/Jigsaw-Code/outline-sdk/network"
"github.com/Jigsaw-Code/outline-sdk/network/dnstruncate"
"github.com/Jigsaw-Code/outline-sdk/transport"
@@ -51,13 +52,17 @@ func newOutlinePacketProxy(transportConfig string) (opp *outlinePacketProxy, err
return
}
-func (proxy *outlinePacketProxy) testConnectivityAndRefresh(resolver, domain string) error {
+func (proxy *outlinePacketProxy) testConnectivityAndRefresh(resolverAddr, domain string) error {
dialer := transport.PacketListenerDialer{Listener: proxy.remotePl}
- dnsResolver := &transport.PacketDialerEndpoint{Dialer: dialer, Address: resolver}
- _, err := connectivity.TestResolverPacketConnectivity(context.Background(), dnsResolver, domain)
+ dnsResolver := dns.NewUDPResolver(dialer, resolverAddr)
+ result, err := connectivity.TestConnectivityWithResolver(context.Background(), dnsResolver, domain)
if err != nil {
- logging.Info.Println("remote server cannot handle UDP traffic, switch to DNS truncate mode")
+ logging.Info.Printf("connectivity test failed. Refresh skipped. Error: %v\n", err)
+ return err
+ }
+ if result != nil {
+ logging.Info.Println("remote server cannot handle UDP traffic, switch to DNS truncate mode.")
return proxy.SetProxy(proxy.fallback)
} else {
logging.Info.Println("remote server supports UDP, we will delegate all UDP packets to it")
diff --git a/x/examples/outline-connectivity-app/README.md b/x/examples/outline-connectivity-app/README.md
index 560f50a0..b0f4e335 100644
--- a/x/examples/outline-connectivity-app/README.md
+++ b/x/examples/outline-connectivity-app/README.md
@@ -126,7 +126,7 @@ If you just want to develop ios or android, you can run `yarn watch:ios` or `yar
1. **\[P1\]** read browser language on load, centralize language list, and only localize once
1. **\[P1\]** documentation on how to generate mobile app build credentials
1. **\[P1\]** add individual test result errors to the test result output UI
-1. **\[P2\]** use x/config to parse the access key and showcase the different transports (see: https://github.com/Jigsaw-Code/outline-sdk/blob/main/x/examples/outline-connectivity/main.go)
+1. **\[P2\]** use x/config to parse the access key and showcase the different transports (see: https://github.com/Jigsaw-Code/outline-sdk/blob/main/x/examples/test-connectivity/main.go)
1. **\[P2\]** generalize request handler via generics/reflection
1. **\[P2\]** create a logo for the app
1. **\[P2\]** android-specific toggle CSS
diff --git a/x/examples/outline-connectivity-app/go.mod b/x/examples/outline-connectivity-app/go.mod
index 15438e01..de548a2c 100644
--- a/x/examples/outline-connectivity-app/go.mod
+++ b/x/examples/outline-connectivity-app/go.mod
@@ -3,38 +3,39 @@ module github.com/Jigsaw-Code/outline-sdk/x/examples/outline-connectivity-app
go 1.20
require (
- github.com/Jigsaw-Code/outline-sdk v0.0.6
- github.com/Jigsaw-Code/outline-sdk/x v0.0.0-20230907224418-a5564f78bcef
- github.com/wailsapp/wails/v2 v2.5.1
- golang.org/x/mobile v0.0.0-20230905140555-fbe1c053b6a9
+ github.com/Jigsaw-Code/outline-sdk v0.0.12
+ github.com/Jigsaw-Code/outline-sdk/x v0.0.0-20240119140742-794f7d63eae6
+ github.com/wailsapp/wails/v2 v2.6.0
+ golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a
)
require (
github.com/bep/debounce v1.2.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
- github.com/google/uuid v1.1.2 // indirect
+ github.com/google/uuid v1.3.0 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
- github.com/labstack/echo/v4 v4.9.0 // indirect
- github.com/labstack/gommon v0.3.1 // indirect
- github.com/leaanthony/go-ansi-parser v1.0.1 // indirect
+ github.com/labstack/echo/v4 v4.10.2 // indirect
+ github.com/labstack/gommon v0.4.0 // indirect
+ github.com/leaanthony/go-ansi-parser v1.6.0 // indirect
github.com/leaanthony/gosod v1.0.3 // indirect
- github.com/leaanthony/slicer v1.5.0 // indirect
- github.com/mattn/go-colorable v0.1.11 // indirect
- github.com/mattn/go-isatty v0.0.14 // indirect
- github.com/miekg/dns v1.1.54 // indirect
- github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 // indirect
+ github.com/leaanthony/slicer v1.6.0 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.19 // indirect
+ github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
- github.com/samber/lo v1.27.1 // indirect
+ github.com/rivo/uniseg v0.4.4 // indirect
+ github.com/samber/lo v1.38.1 // indirect
github.com/shadowsocks/go-shadowsocks2 v0.1.5 // indirect
- github.com/tkrajina/go-reflector v0.5.5 // indirect
+ github.com/tkrajina/go-reflector v0.5.6 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
- github.com/valyala/fasttemplate v1.2.1 // indirect
+ github.com/valyala/fasttemplate v1.2.2 // indirect
+ github.com/wailsapp/go-webview2 v1.0.1 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
- golang.org/x/crypto v0.14.0 // indirect
- golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
- golang.org/x/mod v0.12.0 // indirect
- golang.org/x/net v0.17.0 // indirect
- golang.org/x/sys v0.13.0 // indirect
- golang.org/x/text v0.13.0 // indirect
- golang.org/x/tools v0.12.1-0.20230818130535-1517d1a3ba60 // indirect
+ golang.org/x/crypto v0.17.0 // indirect
+ golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
+ golang.org/x/mod v0.14.0 // indirect
+ golang.org/x/net v0.19.0 // indirect
+ golang.org/x/sys v0.15.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ golang.org/x/tools v0.16.0 // indirect
)
diff --git a/x/examples/outline-connectivity-app/go.sum b/x/examples/outline-connectivity-app/go.sum
index 4f250292..28b2451c 100644
--- a/x/examples/outline-connectivity-app/go.sum
+++ b/x/examples/outline-connectivity-app/go.sum
@@ -1,7 +1,7 @@
-github.com/Jigsaw-Code/outline-sdk v0.0.6 h1:8QqbfgMrdqU7LuUIrkoBev5O5I4u3tORe3RGJcCGOJ4=
-github.com/Jigsaw-Code/outline-sdk v0.0.6/go.mod h1:hhlKz0+r9wSDFT8usvN8Zv/BFToCIFAUn1P2Qk8G2CM=
-github.com/Jigsaw-Code/outline-sdk/x v0.0.0-20230907224418-a5564f78bcef h1:tqXEkM8UyjB869ypgaYcYXZ/OfUO/vaifqHyW34Vz+M=
-github.com/Jigsaw-Code/outline-sdk/x v0.0.0-20230907224418-a5564f78bcef/go.mod h1:uQAl7pYVmEmJmeTG/4OOgsYeClpMxixJdCxGfjGT6ZQ=
+github.com/Jigsaw-Code/outline-sdk v0.0.12 h1:6s4OK26M0M70KHGGm++eapP/fNauJRQ8KNFbS+MuMTA=
+github.com/Jigsaw-Code/outline-sdk v0.0.12/go.mod h1:9cEaF6sWWMzY8orcUI9pV5D0oFp2FZArTSyJiYtMQQs=
+github.com/Jigsaw-Code/outline-sdk/x v0.0.0-20240119140742-794f7d63eae6 h1:Y3WaI2Tvwc+KwQIwh/ny1Ou3HlzX8Yp6CqY20NIHVvc=
+github.com/Jigsaw-Code/outline-sdk/x v0.0.0-20240119140742-794f7d63eae6/go.mod h1:vsyeUIHG39Moab2p9/0E95ygjGKJ/5/6cg94/oU8Ilo=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -9,71 +9,79 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
-github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
-github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
-github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY=
-github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
-github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
-github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
+github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
+github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
+github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
+github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
-github.com/leaanthony/go-ansi-parser v1.0.1 h1:97v6c5kYppVsbScf4r/VZdXyQ21KQIfeQOk2DgKxGG4=
-github.com/leaanthony/go-ansi-parser v1.0.1/go.mod h1:7arTzgVI47srICYhvgUV4CGd063sGEeoSlych5yeSPM=
+github.com/leaanthony/go-ansi-parser v1.6.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg=
+github.com/leaanthony/go-ansi-parser v1.6.0/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ=
github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4=
-github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY=
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
+github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
+github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
-github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
-github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/miekg/dns v1.1.54 h1:5jon9mWcb0sFJGpnI99tOMhCPyJ+RPVz5b63MQG0VWI=
-github.com/miekg/dns v1.1.54/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
-github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 h1:acNfDZXmm28D2Yg/c3ALnZStzNaZMSagpbr96vY6Zjc=
-github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
-github.com/samber/lo v1.27.1 h1:sTXwkRiIFIQG+G0HeAvOEnGjqWeWtI9cg5/n51KrxPg=
-github.com/samber/lo v1.27.1/go.mod h1:it33p9UtPMS7z72fP4gw/EIfQB2eI8ke7GR2wc6+Rhg=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
+github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
+github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28=
github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
-github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
-github.com/tkrajina/go-reflector v0.5.5 h1:gwoQFNye30Kk7NrExj8zm3zFtrGPqOkzFMLuQZg1DtQ=
-github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
+github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
+github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
+github.com/wailsapp/go-webview2 v1.0.1 h1:dEJIeEApW/MhO2tTMISZBFZPuW7kwrFA1NtgFB1z1II=
+github.com/wailsapp/go-webview2 v1.0.1/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
-github.com/wailsapp/wails/v2 v2.5.1 h1:mfG+2kWqQXYOwdgI43HEILjOZDXbk5woPYI3jP2b+js=
-github.com/wailsapp/wails/v2 v2.5.1/go.mod h1:jbOZbcr/zm79PxXxAjP8UoVlDd9wLW3uDs+isIthDfs=
+github.com/wailsapp/wails/v2 v2.6.0 h1:EyH0zR/EO6dDiqNy8qU5spaXDfkluiq77xrkabPYD4c=
+github.com/wailsapp/wails/v2 v2.6.0/go.mod h1:WBG9KKWuw0FKfoepBrr/vRlyTmHaMibWesK3yz6nNiM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
-golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
-golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
-golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
-golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
-golang.org/x/mobile v0.0.0-20230905140555-fbe1c053b6a9 h1:LaLfQUz4L1tfuOlrtEouZLZ0qHDwKn87E1NKoiudP/o=
-golang.org/x/mobile v0.0.0-20230905140555-fbe1c053b6a9/go.mod h1:2jxcxt/JNJik+N+QcB8q308+SyrE3bu43+sGZDmJ02M=
-golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
-golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
+golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
+golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a h1:sYbmY3FwUWCBTodZL1S3JUuOvaW6kM2o+clDzzDNBWg=
+golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a/go.mod h1:Ede7gF0KGoHlj822RtphAHK1jLdrcuRBZg0sF1Q+SPc=
+golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
+golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
-golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
-golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -84,17 +92,19 @@ golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
-golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
-golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.12.1-0.20230818130535-1517d1a3ba60 h1:o4bs4seAAlSiZQAZbO6/RP5XBCZCooQS3Pgc0AUjWts=
-golang.org/x/tools v0.12.1-0.20230818130535-1517d1a3ba60/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
+golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM=
+golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/x/examples/outline-connectivity-app/shared_backend/main.go b/x/examples/outline-connectivity-app/shared_backend/main.go
index 77bca73f..2dbde42d 100644
--- a/x/examples/outline-connectivity-app/shared_backend/main.go
+++ b/x/examples/outline-connectivity-app/shared_backend/main.go
@@ -16,21 +16,20 @@ package shared_backend
import (
"context"
- "encoding/base64"
"errors"
"fmt"
"log"
"net"
+ "net/http"
"net/url"
"runtime"
- "strconv"
"strings"
"time"
- "github.com/Jigsaw-Code/outline-sdk/transport"
- "github.com/Jigsaw-Code/outline-sdk/transport/shadowsocks"
+ "github.com/Jigsaw-Code/outline-sdk/dns"
"github.com/Jigsaw-Code/outline-sdk/x/config"
"github.com/Jigsaw-Code/outline-sdk/x/connectivity"
+ "github.com/Jigsaw-Code/outline-sdk/x/report"
_ "golang.org/x/mobile/bind"
)
@@ -42,105 +41,141 @@ type ConnectivityTestProtocolConfig struct {
type ConnectivityTestResult struct {
// Inputs
- Proxy string `json:"proxy"`
- Resolver string `json:"resolver"`
- Proto string `json:"proto"`
- Prefix string `json:"prefix"`
+ Transport string `json:"transport"`
+ Resolver string `json:"resolver"`
+ Proto string `json:"proto"`
// Observations
Time time.Time `json:"time"`
DurationMs int64 `json:"durationMs"`
Error *ConnectivityTestError `json:"error"`
}
+func (r ConnectivityTestResult) IsSuccess() bool {
+ return r.Error == nil
+}
+
type ConnectivityTestError struct {
// TODO: add Shadowsocks/Transport error
- Op string `json:"operation"`
+ Op string `json:"op,omitempty"`
// Posix error, when available
- PosixError string `json:"posixError"`
+ PosixError string `json:"posixError,omitempty"`
// TODO: remove IP addresses
- Msg string `json:"message"`
+ Msg string `json:"message,omitempty"`
}
type ConnectivityTestRequest struct {
- AccessKey string `json:"accessKey"`
+ Transport string `json:"transport"`
Domain string `json:"domain"`
Resolvers []string `json:"resolvers"`
Protocols ConnectivityTestProtocolConfig `json:"protocols"`
+ ReportTo string `json:"reportTo"`
}
-type sessionConfig struct {
- Hostname string
- Port int
- CryptoKey *shadowsocks.EncryptionKey
- Prefix Prefix
-}
-
-type Prefix []byte
-
func ConnectivityTest(request ConnectivityTestRequest) ([]ConnectivityTestResult, error) {
- accessKeyParameters, err := parseAccessKey(request.AccessKey)
- if err != nil {
- return nil, err
- }
+ var result ConnectivityTestResult
+ var results []ConnectivityTestResult
- proxyIPs, err := net.DefaultResolver.LookupIP(context.Background(), "ip", accessKeyParameters.Hostname)
+ sanitizedConfig, err := config.SanitizeConfig(request.Transport)
if err != nil {
- return nil, err
+ log.Fatalf("Failed to sanitize config: %v", err)
}
- // TODO: limit number of IPs. Or force an input IP?
- var results []ConnectivityTestResult
- for _, hostIP := range proxyIPs {
- proxyAddress := net.JoinHostPort(hostIP.String(), fmt.Sprint(accessKeyParameters.Port))
-
- for _, resolverHost := range request.Resolvers {
- resolverHost := strings.TrimSpace(resolverHost)
- resolverAddress := net.JoinHostPort(resolverHost, "53")
-
- if request.Protocols.TCP {
- testTime := time.Now()
- var testErr error
- var testDuration time.Duration
-
- streamDialer, err := config.NewStreamDialer("")
- if err != nil {
- log.Fatalf("Failed to create StreamDialer: %v", err)
- }
- resolver := &transport.StreamDialerEndpoint{Dialer: streamDialer, Address: resolverAddress}
- testDuration, testErr = connectivity.TestResolverStreamConnectivity(context.Background(), resolver, resolverAddress)
-
- results = append(results, ConnectivityTestResult{
- Proxy: proxyAddress,
- Resolver: resolverAddress,
- Proto: "tcp",
- Prefix: accessKeyParameters.Prefix.String(),
- Time: testTime.UTC().Truncate(time.Second),
- DurationMs: testDuration.Milliseconds(),
- Error: makeErrorRecord(testErr),
- })
+ for _, resolverHost := range request.Resolvers {
+ resolverHost := strings.TrimSpace(resolverHost)
+ resolverAddress := net.JoinHostPort(resolverHost, "53")
+ fmt.Printf("ResolverAddress: %v\n", resolverAddress)
+
+ if request.Protocols.TCP {
+ testTime := time.Now()
+ var testErr error
+ var testDuration time.Duration
+
+ streamDialer, err := config.NewStreamDialer(request.Transport)
+ if err != nil {
+ //log.Fatalf("Failed to create StreamDialer: %v", err)
+ testDuration = time.Duration(0)
+ testErr = err
+ } else {
+ resolver := dns.NewTCPResolver(streamDialer, resolverAddress)
+ startTime := time.Now()
+ // TODO: report question fail
+ testErr, _ := connectivity.TestConnectivityWithResolver(context.Background(), resolver, request.Domain)
+ testDuration := time.Since(startTime)
+ //testDuration, testErr = connectivity.TestResolverStreamConnectivity(context.Background(), resolver, resolverAddress)
+ fmt.Printf("TestDuration: %v\n", testDuration)
+ fmt.Printf("TestError: %v\n", testErr)
+ }
+ result = ConnectivityTestResult{
+ Transport: sanitizedConfig,
+ Resolver: resolverAddress,
+ Proto: "tcp",
+ Time: testTime.UTC().Truncate(time.Second),
+ DurationMs: testDuration.Milliseconds(),
+ Error: makeErrorRecord(testErr),
+ }
+ results = append(results, result)
+ }
+
+ if request.Protocols.UDP {
+ testTime := time.Now()
+ var testErr error
+ var testDuration time.Duration
+
+ packetDialer, err := config.NewPacketDialer(request.Transport)
+ if err != nil {
+ //log.Fatalf("Failed to create PacketDialer: %v", err)
+ testDuration = time.Duration(0)
+ testErr = err
+ } else {
+ resolver := dns.NewUDPResolver(packetDialer, resolverAddress)
+ startTime := time.Now()
+ // TODO: report question fail
+ testErr, _ := connectivity.TestConnectivityWithResolver(context.Background(), resolver, request.Domain)
+ testDuration := time.Since(startTime)
+ //testDuration, testErr = connectivity.TestResolverStreamConnectivity(context.Background(), resolver, resolverAddress)
+ fmt.Printf("TestDuration: %v\n", testDuration)
+ fmt.Printf("TestError: %v\n", testErr)
}
- if request.Protocols.UDP {
- testTime := time.Now()
- var testErr error
- var testDuration time.Duration
-
- packetDialer, err := config.NewPacketDialer("")
- if err != nil {
- log.Fatalf("Failed to create PacketDialer: %v", err)
- }
- resolver := &transport.PacketDialerEndpoint{Dialer: packetDialer, Address: resolverAddress}
- testDuration, testErr = connectivity.TestResolverPacketConnectivity(context.Background(), resolver, resolverAddress)
-
- results = append(results, ConnectivityTestResult{
- Proxy: proxyAddress,
- Resolver: resolverAddress,
- Proto: "udp",
- Prefix: accessKeyParameters.Prefix.String(),
- Time: testTime.UTC().Truncate(time.Second),
- DurationMs: testDuration.Milliseconds(),
- Error: makeErrorRecord(testErr),
- })
+ result = ConnectivityTestResult{
+ Transport: sanitizedConfig,
+ Resolver: resolverAddress,
+ Proto: "udp",
+ Time: testTime.UTC().Truncate(time.Second),
+ DurationMs: testDuration.Milliseconds(),
+ Error: makeErrorRecord(testErr),
+ }
+ results = append(results, result)
+ }
+ }
+ for _, result := range results {
+ fmt.Printf("Result: %v\n", result)
+ var r report.Report = result
+ u, err := url.Parse(request.ReportTo)
+ if err != nil {
+ log.Printf("Expected no error, but got: %v", err)
+ //return results, errors.New("failed to parse collector URL")
+ }
+ fmt.Println("Parsed URL: ", u.String())
+ if u.String() != "" {
+ remoteCollector := &report.RemoteCollector{
+ CollectorURL: u,
+ HttpClient: &http.Client{Timeout: 10 * time.Second},
+ }
+ retryCollector := &report.RetryCollector{
+ Collector: remoteCollector,
+ MaxRetry: 3,
+ InitialDelay: 1 * time.Second,
+ }
+ c := report.SamplingCollector{
+ Collector: retryCollector,
+ SuccessFraction: 0.1,
+ FailureFraction: 1.0,
+ }
+ err = c.Collect(context.Background(), r)
+ if err != nil {
+ log.Printf("Failed to collect report: %v\n", err)
+ //return results, errors.New("failed to collect report")
}
}
}
@@ -161,7 +196,7 @@ func makeErrorRecord(err error) *ConnectivityTestError {
return nil
}
var record = new(ConnectivityTestError)
- var testErr *connectivity.TestError
+ var testErr *connectivity.ConnectivityError
if errors.As(err, &testErr) {
record.Op = testErr.Op
record.PosixError = testErr.PosixError
@@ -181,61 +216,3 @@ func unwrapAll(err error) error {
err = unwrapped
}
}
-
-func (p Prefix) String() string {
- runes := make([]rune, len(p))
- for i, b := range p {
- runes[i] = rune(b)
- }
- return string(runes)
-}
-
-func parseAccessKey(accessKey string) (*sessionConfig, error) {
- var config sessionConfig
- accessKeyURL, err := url.Parse(accessKey)
- if err != nil {
- return nil, fmt.Errorf("failed to parse access key: %w", err)
- }
- var portString string
- // Host is a : string
- config.Hostname, portString, err = net.SplitHostPort(accessKeyURL.Host)
- if err != nil {
- return nil, fmt.Errorf("failed to parse endpoint address: %w", err)
- }
- config.Port, err = strconv.Atoi(portString)
- if err != nil {
- return nil, fmt.Errorf("failed to parse port number: %w", err)
- }
- cipherInfoBytes, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(accessKeyURL.User.String())
- if err != nil {
- return nil, fmt.Errorf("failed to decode cipher info [%v]: %v", accessKeyURL.User.String(), err)
- }
- cipherName, secret, found := strings.Cut(string(cipherInfoBytes), ":")
- if !found {
- return nil, fmt.Errorf("invalid cipher info: no ':' separator")
- }
- config.CryptoKey, err = shadowsocks.NewEncryptionKey(cipherName, secret)
- if err != nil {
- return nil, fmt.Errorf("failed to create cipher: %w", err)
- }
- prefixStr := accessKeyURL.Query().Get("prefix")
- if len(prefixStr) > 0 {
- config.Prefix, err = ParseStringPrefix(prefixStr)
- if err != nil {
- return nil, fmt.Errorf("failed to parse prefix: %w", err)
- }
- }
- return &config, nil
-}
-
-func ParseStringPrefix(utf8Str string) (Prefix, error) {
- runes := []rune(utf8Str)
- rawBytes := make([]byte, len(runes))
- for i, r := range runes {
- if (r & 0xFF) != r {
- return nil, fmt.Errorf("character out of range: %d", r)
- }
- rawBytes[i] = byte(r)
- }
- return rawBytes, nil
-}
diff --git a/x/examples/outline-connectivity-app/shared_frontend/pages/connectivity_test/index.ts b/x/examples/outline-connectivity-app/shared_frontend/pages/connectivity_test/index.ts
index 4cb6095a..49b332e3 100644
--- a/x/examples/outline-connectivity-app/shared_frontend/pages/connectivity_test/index.ts
+++ b/x/examples/outline-connectivity-app/shared_frontend/pages/connectivity_test/index.ts
@@ -14,9 +14,11 @@
import { configureLocalization, msg, localized } from "@lit/localize";
import { css, html, LitElement, nothing } from "lit";
-import { property } from "lit/decorators.js";
+import { property, customElement, query } from "lit/decorators.js";
import { sourceLocale, targetLocales } from "./generated/messages";
import { ConnectivityTestRequest, ConnectivityTestResponse, ConnectivityTestResult, OperatingSystem, PlatformMetadata } from "./types";
+import "./popover_info";
+import "./outline_logo";
export * from "./types";
@@ -63,8 +65,9 @@ export class ConnectivityTestPage extends LitElement {
const formData = new FormData(formElement);
- const accessKey = formData.get("accessKey")?.toString().trim();
+ const transport = formData.get("transport")?.toString().trim();
const domain = formData.get("domain")?.toString().trim();
+ const reportTo = formData.get("reportTo")?.toString().trim();
const resolvers =
formData
.get("resolvers")
@@ -77,16 +80,17 @@ export class ConnectivityTestPage extends LitElement {
};
const prefix = formData.get("prefix")?.toString();
- if (!accessKey || !domain || !resolvers) {
+ if (!domain || !resolvers) {
return null;
}
return {
- accessKey,
+ transport,
domain,
resolvers,
protocols,
prefix,
+ reportTo,
};
}
@@ -325,6 +329,13 @@ export class ConnectivityTestPage extends LitElement {
margin-top: var(--size-gap);
}
+ .logo {
+ display: block;
+ margin-top: 40px;
+ max-width: 300px;
+ min-width: 250px;
+ }
+
.field {
display: block;
margin-bottom: var(--size-gap);
@@ -598,22 +609,26 @@ export class ConnectivityTestPage extends LitElement {
// TODO: move language definitions to a centralized place
return html`
${this.renderResults()}
+