diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 16:16:56 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 16:16:56 +0000 |
commit | 62e212dec2415aa605363d616c481c2a56e70737 (patch) | |
tree | b5ace7061598be728afaf95a9a40054daf05ce19 | |
parent | Initial commit. (diff) | |
download | golang-github-containers-gvisor-tap-vsocks-62e212dec2415aa605363d616c481c2a56e70737.tar.xz golang-github-containers-gvisor-tap-vsocks-62e212dec2415aa605363d616c481c2a56e70737.zip |
Adding upstream version 0.7.3+ds1.upstream/0.7.3+ds1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
99 files changed, 9455 insertions, 0 deletions
diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..da67103 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +bin/ +.git/ +*.pcap +tmp/ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d1273ea --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +/pkg/types/version.go export-subst diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b3b12a8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: +- package-ecosystem: gomod + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 +- package-ecosystem: gomod + directory: "/tools" + schedule: + interval: daily + open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..c484837 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,83 @@ +name: Go + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + + build: + runs-on: ubuntu-20.04 # explicitly use 20.04, see commit 428c40018a + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + go-version: ["1.20.x", "1.21.x", "1.22.0-rc.1"] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # history/tags are needed for automatic version generation + fetch-tags: true + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + + - name: Build + run: | + make cross qemu-wrapper vm win-gvproxy win-sshproxy + mv bin/gvproxy.exe bin/gvproxy-windowsgui.exe + + - uses: actions/upload-artifact@v4 + if: matrix.go-version == '1.21.x' + with: + name: gvisor-tap-vsock-binaries + path: bin/* + + tests: + runs-on: macos-latest # Only Mac runners support nested virt + needs: build # Don't bother testing if cross arch build fails + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + + - name: Install + run: | + brew install qemu + touch continue + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.20.x + + - name: Test + run: make test + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: qcon + path: test/qcon.log + + win-sshproxy-tests: + runs-on: windows-latest # Only builds/runs on windows + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.20.x + + - name: Build + run: make win-sshproxy + + - name: Test + run: go test -v .\test-win-sshproxy + + diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..5e3495e --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,21 @@ +name: golangci-lint + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.20.x + + - uses: actions/checkout@v4 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fa14d1a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Release build + +on: + push: + tags: + - 'v*' + +jobs: + build: + runs-on: ubuntu-20.04 # explicitly use 20.04, see commit 428c40018a + timeout-minutes: 30 + strategy: + fail-fast: false + steps: + - name: Checkout source code + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: >- + WORKAROUND: Fetch tags that points to the revisions + checked-out(actions/checkout#1467) + run: |- + git fetch --tags --force + + - name: Set up Go + uses: actions/setup-go@v5 + + - name: Build + run: | + make cross qemu-wrapper vm win-gvproxy win-sshproxy + mv bin/gvproxy.exe bin/gvproxy-windowsgui.exe + + - uses: actions/upload-artifact@v4 + with: + name: gvisor-tap-vsock-binaries + path: bin/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..49bac5b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +./bin/ +capture.pcap +tmp/ +test/qcon.log diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..61f36a0 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,13 @@ +run: + timeout: 10m + +linters: + enable: + - errcheck + - goimports + - gofmt + - gosec + - gocritic + - unused + - misspell + - revive diff --git a/.packit.yaml b/.packit.yaml new file mode 100644 index 0000000..ef7b40e --- /dev/null +++ b/.packit.yaml @@ -0,0 +1,62 @@ +--- +# See the documentation for more information: +# https://packit.dev/docs/configuration/ + +# Build targets can be found at: +# On PR: https://copr.fedorainfracloud.org/coprs/rhcontainerbot/packit-builds/ +# On commit: https://copr.fedorainfracloud.org/coprs/rhcontainerbot/podman-next/ + +specfile_path: rpm/gvisor-tap-vsock.spec +upstream_tag_template: v{version} + +srpm_build_deps: + - make + +jobs: + - job: copr_build + trigger: pull_request + enable_net: true + targets: + - fedora-all-aarch64 + - fedora-all-x86_64 + - fedora-eln-aarch64 + - fedora-eln-x86_64 + - centos-stream+epel-next-8-x86_64 + - centos-stream+epel-next-8-aarch64 + - centos-stream+epel-next-9-x86_64 + - centos-stream+epel-next-9-aarch64 + additional_repos: + - "copr://rhcontainerbot/podman-next" + + # Run on commit to main branch + - job: copr_build + trigger: commit + enable_net: true + branch: main + owner: rhcontainerbot + project: podman-next + targets: + - fedora-all-aarch64 + - fedora-all-ppc64le + - fedora-all-s390x + - fedora-all-x86_64 + - fedora-eln-aarch64 + - fedora-eln-ppc64le + - fedora-eln-s390x + - fedora-eln-x86_64 + + - job: propose_downstream + trigger: release + update_release: false + dist_git_branches: + - fedora-all + + - job: koji_build + trigger: commit + dist_git_branches: + - fedora-all + + - job: bodhi_update + trigger: commit + dist_git_branches: + - fedora-branched # rawhide updates are created automatically diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md new file mode 100644 index 0000000..8ab14e8 --- /dev/null +++ b/CODE-OF-CONDUCT.md @@ -0,0 +1,3 @@ +## The gvisor-tap-vsock Project Community Code of Conduct + +The gvisor-tap-vsock project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/main/CODE-OF-CONDUCT.md). @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..00f2c08 --- /dev/null +++ b/Makefile @@ -0,0 +1,69 @@ +TAG ?= $(shell git describe --match=NeVeRmAtCh --always --abbrev=40 --dirty) +GIT_VERSION ?= $(shell git describe --always --dirty) +CONTAINER_RUNTIME ?= podman + +.PHONY: build +build: gvproxy qemu-wrapper vm + +TOOLS_DIR := tools +include tools/tools.mk + +VERSION_LDFLAGS=-X github.com/containers/gvisor-tap-vsock/pkg/types.gitVersion=$(GIT_VERSION) +LDFLAGS = -s -w $(VERSION_LDFLAGS) + +.PHONY: gvproxy +gvproxy: + go build -ldflags "$(LDFLAGS)" -o bin/gvproxy ./cmd/gvproxy + +.PHONY: qemu-wrapper +qemu-wrapper: + go build -ldflags "$(LDFLAGS)" -o bin/qemu-wrapper ./cmd/qemu-wrapper + +.PHONY: vm +vm: + GOOS=linux CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o bin/gvforwarder ./cmd/vm + +# win-sshproxy is compiled as a windows GUI to support backgrounding +.PHONY: win-sshproxy +win-sshproxy: + GOOS=windows go build -ldflags "$(LDFLAGS) -H=windowsgui" -o bin/win-sshproxy.exe ./cmd/win-sshproxy + +# gvproxy for windows is compiled as a windows GUI to support backgrounding +.PHONY: win-gvproxy +win-gvproxy: + GOOS=windows go build -ldflags "$(LDFLAGS) -H=windowsgui" -o bin/gvproxy.exe ./cmd/gvproxy + +.PHONY: clean +clean: + rm -rf ./bin + +.PHONY: vendor +vendor: + go mod tidy + go mod vendor + +.PHONY: lint +lint: $(TOOLS_BINDIR)/golangci-lint + "$(TOOLS_BINDIR)"/golangci-lint run + +.PHONY: image +image: + ${CONTAINER_RUNTIME} build -t quay.io/crcont/gvisor-tap-vsock:$(TAG) -f images/Dockerfile . + +.PHONY: cross +cross: $(TOOLS_BINDIR)/makefat + GOARCH=amd64 GOOS=freebsd go build -ldflags "$(LDFLAGS)" -o bin/gvproxy-freebsd-amd64 ./cmd/gvproxy + GOARCH=amd64 GOOS=windows go build -ldflags "$(LDFLAGS)" -o bin/gvproxy-windows.exe ./cmd/gvproxy + GOARCH=amd64 GOOS=linux go build -ldflags "$(LDFLAGS)" -o bin/gvproxy-linux-amd64 ./cmd/gvproxy + GOARCH=arm64 GOOS=linux go build -ldflags "$(LDFLAGS)" -o bin/gvproxy-linux-arm64 ./cmd/gvproxy + GOARCH=amd64 GOOS=darwin go build -ldflags "$(LDFLAGS)" -o bin/gvproxy-darwin-amd64 ./cmd/gvproxy + GOARCH=arm64 GOOS=darwin go build -ldflags "$(LDFLAGS)" -o bin/gvproxy-darwin-arm64 ./cmd/gvproxy + cd bin && $(TOOLS_BINDIR)/makefat gvproxy-darwin gvproxy-darwin-amd64 gvproxy-darwin-arm64 && rm gvproxy-darwin-amd64 gvproxy-darwin-arm64 + +.PHONY: test-companion +test-companion: + GOOS=linux go build -ldflags "$(LDFLAGS)" -o bin/test-companion ./cmd/test-companion + +.PHONY: test +test: gvproxy test-companion + go test -timeout 20m -v ./... @@ -0,0 +1,9 @@ +approvers: + - baude + - cfergeau + +reviewers: + - baude + - cfergeau + - jakecorrenti + - praveenkumar diff --git a/README.md b/README.md new file mode 100644 index 0000000..7105ee5 --- /dev/null +++ b/README.md @@ -0,0 +1,205 @@ +# gvisor-tap-vsock + + +A replacement for [libslirp](https://gitlab.com/qemu-project/libslirp) and [VPNKit](https://github.com/moby/vpnkit), written in pure Go. +It is based on the network stack of [gVisor](https://github.com/google/gvisor/tree/master/pkg/tcpip). + +Compared to libslirp, gvisor-tap-vsock brings a configurable DNS server and dynamic port forwarding. + +It can be used with QEMU, Hyperkit, Hyper-V and User Mode Linux. + +## Build + +``` +make +``` + +## Run with QEMU (Linux or macOS) + +Usually with QEMU, to not run as root, you would have to use `-netdev user,id=n0`. +With this project, this is the same but you have to run a daemon on the host. + +There 2 ways for the VM to communicate with the daemon: with a tcp port or with a unix socket. + +- With gvproxy and the VM discussing on a tcp port: +``` +(terminal 1) $ bin/gvproxy -debug -listen unix:///tmp/network.sock -listen-qemu tcp://0.0.0.0:1234 +(terminal 2) $ qemu-system-x86_64 (all your qemu options) -netdev socket,id=vlan,connect=127.0.0.1:1234 -device virtio-net-pci,netdev=vlan,mac=5a:94:ef:e4:0c:ee +``` + +- With gvproxy and the VM discussing on a unix socket: +``` +(terminal 1) $ bin/gvproxy -debug -listen unix:///tmp/network.sock -listen-qemu unix:///tmp/qemu.sock +(terminal 2) $ bin/qemu-wrapper /tmp/qemu.sock qemu-system-x86_64 (all your qemu options) -netdev socket,id=vlan,fd=3 -device virtio-net-pci,netdev=vlan,mac=5a:94:ef:e4:0c:ee +``` + +Starting from Qemu version 7.2.0 it is possible to run w/o a wrapper: +``` +(terminal 1) $ bin/gvproxy -debug -listen unix:///tmp/network.sock -listen-qemu unix:///tmp/qemu.sock +(terminal 2) $ qemu-system-x86_64 (all your qemu options) -netdev stream,id=vlan,addr.type=unix,addr.path=/tmp/qemu.sock -device virtio-net-pci,netdev=vlan,mac=5a:94:ef:e4:0c:ee +``` + +## Run with User Mode Linux + +``` +(terminal 1) $ bin/gvproxy -debug -listen unix:///tmp/network.sock -listen-bess unixpacket:///tmp/bess.sock +(terminal 2) $ linux.uml vec0:transport=bess,dst=/tmp/bess.sock,depth=128,gro=1,mac=5a:94:ef:e4:0c:ee root=/dev/root rootfstype=hostfs init=/bin/bash mem=2G +(terminal 2: UML)$ ip addr add 192.168.127.2/24 dev vec0 +(terminal 2: UML)$ ip link set vec0 up +(terminal 2: UML)$ ip route add default via 192.168.127.254 +``` + +More docs about the User Mode Linux with BESS socket transport: https://www.kernel.org/doc/html/latest/virt/uml/user_mode_linux_howto_v2.html#bess-socket-transport + +## Run with [vfkit](https://github.com/crc-org/vfkit) + +With vfkit 0.1.0 or newer, gvproxy can be used without any helper running in the VM: + +``` +(terminal 1) $ bin/gvproxy -debug -listen unix:///tmp/network.sock --listen-vfkit unixgram:///tmp/vfkit.sock +(terminal 2) $ vfkit (all your vfkit options) --device virtio-net,unixSocketPath=/tmp/vfkit.sock,mac=5a:94:ef:e4:0c:ee +``` + +## Run with vsock + +Made for Windows but also works for Linux and macOS with vfkit. + +### Host + +#### Windows prerequisites + +``` +$service = New-Item -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\GuestCommunicationServices" -Name "00000400-FACB-11E6-BD58-64006A7986D3" +$service.SetValue("ElementName", "gvisor-tap-vsock") +``` + +More docs: https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/make-integration-service + +In the VM, be sure to have `hv_sock` module loaded. + +#### Linux prerequisites + +On Fedora 32, it worked out of the box. On others distros, you might have to look at https://github.com/mdlayher/vsock#requirements. + +#### macOS prerequisites + + +`vfkit` must be started with a vsock device: `--device virtio-vsock,port=1024,socketURL=/tmp/vfkit-vsock-1024.sock` +Then you can launch `gvproxy` with the following listen argument: +`--listen unix:///tmp/vfkit-vsock-1024.sock` + +#### Run + +``` +(host) $ sudo bin/gvproxy -debug -listen vsock://:1024 -listen unix:///tmp/network.sock +``` + +### VM + +With a container: +``` +(vm) # docker run -d --name=gvisor-tap-vsock --privileged --net=host -it quay.io/crcont/gvisor-tap-vsock:latest +(vm) $ ping -c1 192.168.127.1 +(vm) $ curl http://redhat.com +``` + +With the executable: +``` +(vm) # ./gvforwarder -debug +``` + +## Services + +### API + +The executable running on the host, `gvproxy`, exposes a HTTP API. It can be used with curl. + +``` +$ curl --unix-socket /tmp/network.sock http:/unix/stats +{ + "BytesSent": 0, + "BytesReceived": 0, + "UnknownProtocolRcvdPackets": 0, + "MalformedRcvdPackets": 0, +... +``` + +### Gateway + +The executable running on the host runs a virtual gateway that can be used by the VM. +It runs a DHCP server. It allows VMs to configure the network automatically (IP, MTU, DNS, search domain, etc.). + +### DNS + +The gateway also runs a DNS server. It can be configured to serve static zones. + +Activate it by changing the `/etc/resolv.conf` file inside the VM with: +``` +nameserver 192.168.127.1 +``` + +### Port forwarding + +Dynamic port forwarding is supported. + +Expose a port: +``` +$ curl --unix-socket /tmp/network.sock http:/unix/services/forwarder/expose -X POST -d '{"local":":6443","remote":"192.168.127.2:6443"}' +``` + +Unexpose a port: +``` +$ curl --unix-socket /tmp/network.sock http:/unix/services/forwarder/unexpose -X POST -d '{"local":":6443"}' +``` + +List exposed ports: +``` +$ curl --unix-socket /tmp/network.sock http:/unix/services/forwarder/all | jq . +[ + { + "local": ":2222", + "remote": "192.168.127.2:22" + }, + { + "local": ":6443", + "remote": "192.168.127.2:6443" + } +] + +``` + +### Tunneling + +The HTTP API exposed on the host can be used to connect to a specific IP and port inside the virtual network. +A working example for SSH can be found [here](https://github.com/containers/gvisor-tap-vsock/blob/master/cmd/ssh-over-vsock). + +## Limitations + +* ICMP is not forwarded outside the network. + +## Performance + +Using iperf3, it can achieve between 1.6 and 2.3Gbits/s depending on which side the test is performed (tested with a mtu of 4000 with QEMU on macOS). + +## How it works with vsock + +### Internet access + +![schema](./doc/curl.png) + +0. A tap network interface is running in the VM. It's the default gateway. +1. User types `curl redhat.com` +2. Linux kernel sends raw Ethernet packets to the tap device. +3. Tap device sends these packets to a process on the host using [vsock](https://wiki.qemu.org/Features/VirtioVsock) +4. The process on the host maintains both internal (host to VM) and external (host to Internet endpoint) connections. It uses regular syscalls to connect to external endpoints. + +This is the same behaviour as [slirp](https://wiki.qemu.org/index.php/Documentation/Networking#User_Networking_.28SLIRP.29). + +### Expose a port + +![schema](./doc/http.png) + +1. The process on the host binds the port 80. +2. Each time, a client sends a http request, the process creates and sends the appropriate Ethernet packets to the VM. +3. The tap device receives the packets and injects them in the kernel. +4. The http server receives the request and send back the response. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4b5a0d4 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +## Security and Disclosure Information Policy for the gvisor-tap-vsock Project + +The gvisor-tap-vsock project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/main/SECURITY.md) for the Containers Projects. diff --git a/cmd/gvproxy/main.go b/cmd/gvproxy/main.go new file mode 100644 index 0000000..2181de5 --- /dev/null +++ b/cmd/gvproxy/main.go @@ -0,0 +1,567 @@ +package main + +import ( + "bufio" + "context" + "flag" + "fmt" + "net" + "net/http" + "net/http/pprof" + "net/url" + "os" + "os/signal" + "runtime" + "strconv" + "strings" + "syscall" + "time" + + "github.com/containers/gvisor-tap-vsock/pkg/net/stdio" + "github.com/containers/gvisor-tap-vsock/pkg/sshclient" + "github.com/containers/gvisor-tap-vsock/pkg/transport" + "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/containers/gvisor-tap-vsock/pkg/virtualnetwork" + "github.com/containers/winquit/pkg/winquit" + "github.com/dustin/go-humanize" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "golang.org/x/sync/errgroup" +) + +var ( + debug bool + mtu int + endpoints arrayFlags + vpnkitSocket string + qemuSocket string + bessSocket string + stdioSocket string + vfkitSocket string + forwardSocket arrayFlags + forwardDest arrayFlags + forwardUser arrayFlags + forwardIdentify arrayFlags + sshPort int + pidFile string + exitCode int + logFile string +) + +const ( + gatewayIP = "192.168.127.1" + sshHostPort = "192.168.127.2:22" + hostIP = "192.168.127.254" + host = "host" + gateway = "gateway" +) + +func main() { + version := types.NewVersion("gvproxy") + version.AddFlag() + flag.Var(&endpoints, "listen", "control endpoint") + flag.BoolVar(&debug, "debug", false, "Print debug info") + flag.IntVar(&mtu, "mtu", 1500, "Set the MTU") + flag.IntVar(&sshPort, "ssh-port", 2222, "Port to access the guest virtual machine. Must be between 1024 and 65535") + flag.StringVar(&vpnkitSocket, "listen-vpnkit", "", "VPNKit socket to be used by Hyperkit") + flag.StringVar(&qemuSocket, "listen-qemu", "", "Socket to be used by Qemu") + flag.StringVar(&bessSocket, "listen-bess", "", "unixpacket socket to be used by Bess-compatible applications") + flag.StringVar(&stdioSocket, "listen-stdio", "", "accept stdio pipe") + flag.StringVar(&vfkitSocket, "listen-vfkit", "", "unixgram socket to be used by vfkit-compatible applications") + flag.Var(&forwardSocket, "forward-sock", "Forwards a unix socket to the guest virtual machine over SSH") + flag.Var(&forwardDest, "forward-dest", "Forwards a unix socket to the guest virtual machine over SSH") + flag.Var(&forwardUser, "forward-user", "SSH user to use for unix socket forward") + flag.Var(&forwardIdentify, "forward-identity", "Path to SSH identity key for forwarding") + flag.StringVar(&pidFile, "pid-file", "", "Generate a file with the PID in it") + flag.StringVar(&logFile, "log-file", "", "Output log messages (logrus) to a given file path") + flag.Parse() + + if version.ShowVersion() { + fmt.Println(version.String()) + os.Exit(0) + } + + // If the user provides a log-file, we re-direct log messages + // from logrus to the file + if logFile != "" { + lf, err := os.Create(logFile) + if err != nil { + fmt.Printf("unable to open log file %s, exiting...\n", logFile) + os.Exit(1) + } + defer func() { + if err := lf.Close(); err != nil { + fmt.Printf("unable to close log-file: %q\n", err) + } + }() + log.SetOutput(lf) + + // If debug is set, lets seed the log file with some basic information + // about the environment and how it was called + log.Debugf("gvproxy version: %q", version.String()) + log.Debugf("os: %q arch: %q", runtime.GOOS, runtime.GOARCH) + log.Debugf("command line: %q", os.Args) + } + + log.Infof(version.String()) + ctx, cancel := context.WithCancel(context.Background()) + // Make this the last defer statement in the stack + defer os.Exit(exitCode) + + groupErrs, ctx := errgroup.WithContext(ctx) + // Setup signal channel for catching user signals + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + + if debug { + log.SetLevel(log.DebugLevel) + } + + // Intercept WM_QUIT/WM_CLOSE events if on Windows as SIGTERM (noop on other OSs) + winquit.SimulateSigTermOnQuit(sigChan) + + // Make sure the qemu socket provided is valid syntax + if len(qemuSocket) > 0 { + uri, err := url.Parse(qemuSocket) + if err != nil || uri == nil { + exitWithError(errors.Wrapf(err, "invalid value for listen-qemu")) + } + if _, err := os.Stat(uri.Path); err == nil && uri.Scheme == "unix" { + exitWithError(errors.Errorf("%q already exists", uri.Path)) + } + } + if len(bessSocket) > 0 { + uri, err := url.Parse(bessSocket) + if err != nil || uri == nil { + exitWithError(errors.Wrapf(err, "invalid value for listen-bess")) + } + if uri.Scheme != "unixpacket" { + exitWithError(errors.New("listen-bess must be unixpacket:// address")) + } + if _, err := os.Stat(uri.Path); err == nil { + exitWithError(errors.Errorf("%q already exists", uri.Path)) + } + } + if len(vfkitSocket) > 0 { + uri, err := url.Parse(vfkitSocket) + if err != nil || uri == nil { + exitWithError(errors.Wrapf(err, "invalid value for listen-vfkit")) + } + if uri.Scheme != "unixgram" { + exitWithError(errors.New("listen-vfkit must be unixgram:// address")) + } + if _, err := os.Stat(uri.Path); err == nil { + exitWithError(errors.Errorf("%q already exists", uri.Path)) + } + } + + if vpnkitSocket != "" && qemuSocket != "" { + exitWithError(errors.New("cannot use qemu and vpnkit protocol at the same time")) + } + if vpnkitSocket != "" && bessSocket != "" { + exitWithError(errors.New("cannot use bess and vpnkit protocol at the same time")) + } + if qemuSocket != "" && bessSocket != "" { + exitWithError(errors.New("cannot use qemu and bess protocol at the same time")) + } + + // If the given port is not between the privileged ports + // and the oft considered maximum port, return an error. + if sshPort < 1024 || sshPort > 65535 { + exitWithError(errors.New("ssh-port value must be between 1024 and 65535")) + } + protocol := types.HyperKitProtocol + if qemuSocket != "" { + protocol = types.QemuProtocol + } + if bessSocket != "" { + protocol = types.BessProtocol + } + if vfkitSocket != "" { + protocol = types.VfkitProtocol + } + + if c := len(forwardSocket); c != len(forwardDest) || c != len(forwardUser) || c != len(forwardIdentify) { + exitWithError(errors.New("-forward-sock, --forward-dest, --forward-user, and --forward-identity must all be specified together, " + + "the same number of times, or not at all")) + } + + for i := 0; i < len(forwardSocket); i++ { + _, err := os.Stat(forwardIdentify[i]) + if err != nil { + exitWithError(errors.Wrapf(err, "Identity file %s can't be loaded", forwardIdentify[i])) + } + } + + // Create a PID file if requested + if len(pidFile) > 0 { + f, err := os.Create(pidFile) + if err != nil { + exitWithError(err) + } + // Remove the pid-file when exiting + defer func() { + if err := os.Remove(pidFile); err != nil { + log.Error(err) + } + }() + pid := os.Getpid() + if _, err := f.WriteString(strconv.Itoa(pid)); err != nil { + exitWithError(err) + } + } + + config := types.Configuration{ + Debug: debug, + CaptureFile: captureFile(), + MTU: mtu, + Subnet: "192.168.127.0/24", + GatewayIP: gatewayIP, + GatewayMacAddress: "5a:94:ef:e4:0c:dd", + DHCPStaticLeases: map[string]string{ + "192.168.127.2": "5a:94:ef:e4:0c:ee", + }, + DNS: []types.Zone{ + { + Name: "containers.internal.", + Records: []types.Record{ + { + Name: gateway, + IP: net.ParseIP(gatewayIP), + }, + { + Name: host, + IP: net.ParseIP(hostIP), + }, + }, + }, + { + Name: "docker.internal.", + Records: []types.Record{ + { + Name: gateway, + IP: net.ParseIP(gatewayIP), + }, + { + Name: host, + IP: net.ParseIP(hostIP), + }, + }, + }, + }, + DNSSearchDomains: searchDomains(), + Forwards: map[string]string{ + fmt.Sprintf("127.0.0.1:%d", sshPort): sshHostPort, + }, + NAT: map[string]string{ + hostIP: "127.0.0.1", + }, + GatewayVirtualIPs: []string{hostIP}, + VpnKitUUIDMacAddresses: map[string]string{ + "c3d68012-0208-11ea-9fd7-f2189899ab08": "5a:94:ef:e4:0c:ee", + }, + Protocol: protocol, + } + + groupErrs.Go(func() error { + return run(ctx, groupErrs, &config, endpoints) + }) + + // Wait for something to happen + groupErrs.Go(func() error { + select { + // Catch signals so exits are graceful and defers can run + case <-sigChan: + cancel() + return errors.New("signal caught") + case <-ctx.Done(): + return nil + } + }) + // Wait for all of the go funcs to finish up + if err := groupErrs.Wait(); err != nil { + log.Errorf("gvproxy exiting: %v", err) + exitCode = 1 + } +} + +type arrayFlags []string + +func (i *arrayFlags) String() string { + return "my string representation" +} + +func (i *arrayFlags) Set(value string) error { + *i = append(*i, value) + return nil +} + +func captureFile() string { + if !debug { + return "" + } + return "capture.pcap" +} + +func run(ctx context.Context, g *errgroup.Group, configuration *types.Configuration, endpoints []string) error { + vn, err := virtualnetwork.New(configuration) + if err != nil { + return err + } + log.Info("waiting for clients...") + + for _, endpoint := range endpoints { + log.Infof("listening %s", endpoint) + ln, err := transport.Listen(endpoint) + if err != nil { + return errors.Wrap(err, "cannot listen") + } + httpServe(ctx, g, ln, withProfiler(vn)) + } + + ln, err := vn.Listen("tcp", fmt.Sprintf("%s:80", gatewayIP)) + if err != nil { + return err + } + mux := http.NewServeMux() + mux.Handle("/services/forwarder/all", vn.Mux()) + mux.Handle("/services/forwarder/expose", vn.Mux()) + mux.Handle("/services/forwarder/unexpose", vn.Mux()) + httpServe(ctx, g, ln, mux) + + if debug { + g.Go(func() error { + debugLog: + for { + select { + case <-time.After(5 * time.Second): + log.Debugf("%v sent to the VM, %v received from the VM\n", humanize.Bytes(vn.BytesSent()), humanize.Bytes(vn.BytesReceived())) + case <-ctx.Done(): + break debugLog + } + } + return nil + }) + } + + if vpnkitSocket != "" { + vpnkitListener, err := transport.Listen(vpnkitSocket) + if err != nil { + return errors.Wrap(err, "vpnkit listen error") + } + g.Go(func() error { + vpnloop: + for { + select { + case <-ctx.Done(): + break vpnloop + default: + // pass through + } + conn, err := vpnkitListener.Accept() + if err != nil { + log.Errorf("vpnkit accept error: %s", err) + continue + } + g.Go(func() error { + return vn.AcceptVpnKit(conn) + }) + } + return nil + }) + } + + if qemuSocket != "" { + qemuListener, err := transport.Listen(qemuSocket) + if err != nil { + return errors.Wrap(err, "qemu listen error") + } + + g.Go(func() error { + <-ctx.Done() + if err := qemuListener.Close(); err != nil { + log.Errorf("error closing %s: %q", qemuSocket, err) + } + return os.Remove(qemuSocket) + }) + + g.Go(func() error { + conn, err := qemuListener.Accept() + if err != nil { + return errors.Wrap(err, "qemu accept error") + } + return vn.AcceptQemu(ctx, conn) + }) + } + + if bessSocket != "" { + bessListener, err := transport.Listen(bessSocket) + if err != nil { + return errors.Wrap(err, "bess listen error") + } + + g.Go(func() error { + <-ctx.Done() + if err := bessListener.Close(); err != nil { + log.Errorf("error closing %s: %q", bessSocket, err) + } + return os.Remove(bessSocket) + }) + + g.Go(func() error { + conn, err := bessListener.Accept() + if err != nil { + return errors.Wrap(err, "bess accept error") + + } + return vn.AcceptBess(ctx, conn) + }) + } + + if vfkitSocket != "" { + conn, err := transport.ListenUnixgram(vfkitSocket) + if err != nil { + return errors.Wrap(err, "vfkit listen error") + } + + g.Go(func() error { + <-ctx.Done() + if err := conn.Close(); err != nil { + log.Errorf("error closing %s: %q", vfkitSocket, err) + } + return os.Remove(vfkitSocket) + }) + + g.Go(func() error { + vfkitConn, err := transport.AcceptVfkit(conn) + if err != nil { + return errors.Wrap(err, "vfkit accept error") + } + return vn.AcceptVfkit(ctx, vfkitConn) + }) + } + + if stdioSocket != "" { + g.Go(func() error { + conn := stdio.GetStdioConn() + return vn.AcceptStdio(ctx, conn) + }) + } + + for i := 0; i < len(forwardSocket); i++ { + var ( + src *url.URL + err error + ) + if strings.Contains(forwardSocket[i], "://") { + src, err = url.Parse(forwardSocket[i]) + if err != nil { + return err + } + } else { + src = &url.URL{ + Scheme: "unix", + Path: forwardSocket[i], + } + } + + dest := &url.URL{ + Scheme: "ssh", + User: url.User(forwardUser[i]), + Host: sshHostPort, + Path: forwardDest[i], + } + j := i + g.Go(func() error { + defer os.Remove(forwardSocket[j]) + forward, err := sshclient.CreateSSHForward(ctx, src, dest, forwardIdentify[j], vn) + if err != nil { + return err + } + go func() { + <-ctx.Done() + // Abort pending accepts + forward.Close() + }() + loop: + for { + select { + case <-ctx.Done(): + break loop + default: + // proceed + } + err := forward.AcceptAndTunnel(ctx) + if err != nil { + log.Debugf("Error occurred handling ssh forwarded connection: %q", err) + } + } + return nil + }) + } + + return nil +} + +func httpServe(ctx context.Context, g *errgroup.Group, ln net.Listener, mux http.Handler) { + g.Go(func() error { + <-ctx.Done() + return ln.Close() + }) + g.Go(func() error { + s := &http.Server{ + Handler: mux, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + } + err := s.Serve(ln) + if err != nil { + if err != http.ErrServerClosed { + return err + } + return err + } + return nil + }) +} + +func withProfiler(vn *virtualnetwork.VirtualNetwork) http.Handler { + mux := vn.Mux() + if debug { + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + } + return mux +} + +func exitWithError(err error) { + log.Error(err) + os.Exit(1) +} + +func searchDomains() []string { + if runtime.GOOS == "darwin" || runtime.GOOS == "linux" { + f, err := os.Open("/etc/resolv.conf") + if err != nil { + log.Errorf("open file error: %v", err) + return nil + } + defer f.Close() + sc := bufio.NewScanner(f) + searchPrefix := "search " + for sc.Scan() { + if strings.HasPrefix(sc.Text(), searchPrefix) { + searchDomains := strings.Split(strings.TrimPrefix(sc.Text(), searchPrefix), " ") + log.Debugf("Using search domains: %v", searchDomains) + return searchDomains + } + } + if err := sc.Err(); err != nil { + log.Errorf("scan file error: %v", err) + return nil + } + } + return nil +} diff --git a/cmd/qemu-wrapper/README.md b/cmd/qemu-wrapper/README.md new file mode 100644 index 0000000..a6e48dc --- /dev/null +++ b/cmd/qemu-wrapper/README.md @@ -0,0 +1,6 @@ +Qemu doesn't accept a unix socket as netdev, only a file descriptro. +This wrapper is filling the gap. + +``` +$ ./qemu-wrapper /tmp/qemu.sock qemu-system-x86_64 [...] -netdev socket,id=vlan,fd=3 -device virtio-net-pci,netdev=vlan +``` diff --git a/cmd/qemu-wrapper/main.go b/cmd/qemu-wrapper/main.go new file mode 100644 index 0000000..80aaf63 --- /dev/null +++ b/cmd/qemu-wrapper/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "log" + "net" + "os" + "os/exec" +) + +func main() { + conn, err := net.Dial("unix", os.Args[1]) + if err != nil { + log.Fatal(err) + } + fd, err := conn.(*net.UnixConn).File() + if err != nil { + log.Fatal(err) + } + cmd := exec.Command(os.Args[2], os.Args[3:]...) // #nosec G204 + cmd.ExtraFiles = append(cmd.ExtraFiles, fd) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + log.Fatal(err) + } +} diff --git a/cmd/ssh-over-vsock/client.go b/cmd/ssh-over-vsock/client.go new file mode 100644 index 0000000..cca9a86 --- /dev/null +++ b/cmd/ssh-over-vsock/client.go @@ -0,0 +1,66 @@ +package main + +import ( + "fmt" + "net" + "os" + "time" + + "golang.org/x/crypto/ssh" +) + +type client struct { + Conn net.Conn + Config *ssh.ClientConfig +} + +func newClient(conn net.Conn, user string, key string) (*client, error) { + config, err := newConfig(user, key) + if err != nil { + return nil, fmt.Errorf("Error getting config for native Go SSH: %s", err) + } + + return &client{ + Conn: conn, + Config: config, + }, nil +} + +func newConfig(user string, keyFile string) (*ssh.ClientConfig, error) { + key, err := os.ReadFile(keyFile) + if err != nil { + return nil, err + } + privateKey, err := ssh.ParsePrivateKey(key) + if err != nil { + return nil, err + } + return &ssh.ClientConfig{ + User: user, + Auth: []ssh.AuthMethod{ssh.PublicKeys(privateKey)}, + // #nosec G106 + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: time.Minute, + }, nil +} + +func (client *client) output(command string) (string, error) { + c, chans, reqs, err := ssh.NewClientConn(client.Conn, "", client.Config) + if err != nil { + return "", err + } + conn := ssh.NewClient(c, chans, reqs) + session, err := conn.NewSession() + if err != nil { + _ = conn.Close() + return "", err + } + defer conn.Close() + defer session.Close() + + output, err := session.CombinedOutput(command) + if err != nil { + return "", err + } + return string(output), nil +} diff --git a/cmd/ssh-over-vsock/main.go b/cmd/ssh-over-vsock/main.go new file mode 100644 index 0000000..f94ed01 --- /dev/null +++ b/cmd/ssh-over-vsock/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "flag" + "fmt" + "net" + "os" + + "github.com/containers/gvisor-tap-vsock/pkg/transport" + "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +var ( + ip string + port int + user string + key string + endpoint string +) + +func main() { + version := types.NewVersion("ssh-over-vsock") + version.AddFlag() + flag.StringVar(&ip, "ip", "192.168.127.2", "ip of the host") + flag.IntVar(&port, "port", 22, "port of the host") + flag.StringVar(&user, "user", "", "ssh user") + flag.StringVar(&key, "key", "", "ssh key") + flag.StringVar(&endpoint, "url", "/tmp/network.sock", "url of the daemon") + flag.Parse() + + if version.ShowVersion() { + fmt.Println(version.String()) + os.Exit(0) + } + + if err := run(); err != nil { + logrus.Fatal(err) + } +} + +func run() error { + conn, err := net.Dial("unix", endpoint) + if err != nil { + return errors.Wrap(err, "cannot connect to host") + } + defer conn.Close() + + if err := transport.Tunnel(conn, ip, port); err != nil { + return err + } + + client, err := newClient(conn, user, key) + if err != nil { + return err + } + out, err := client.output("ps") + if err != nil { + return err + } + fmt.Println(out) + return nil +} diff --git a/cmd/test-companion/main.go b/cmd/test-companion/main.go new file mode 100644 index 0000000..3977308 --- /dev/null +++ b/cmd/test-companion/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "net" + "net/http" + "time" + + "github.com/miekg/dns" + log "github.com/sirupsen/logrus" +) + +func main() { + go func() { + mux := dns.NewServeMux() + mux.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) { + m := new(dns.Msg) + m.SetReply(r) + m.RecursionAvailable = true + for _, q := range m.Question { + if q.Qtype == dns.TypeA { + m.Answer = append(m.Answer, &dns.A{ + Hdr: dns.RR_Header{ + Name: q.Name, + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: 0, + }, + A: net.ParseIP("1.2.3.4"), + }) + } + } + if err := w.WriteMsg(m); err != nil { + log.Error(err) + } + }) + if err := dns.ListenAndServe(":53", "udp", mux); err != nil { + log.Fatal(err) + } + }() + + mux := http.NewServeMux() + mux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { + _, _ = writer.Write([]byte(`Hello world!`)) + }) + + s := &http.Server{ + Addr: ":8080", + Handler: mux, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + } + log.Fatal(s.ListenAndServe()) +} diff --git a/cmd/vm/main_linux.go b/cmd/vm/main_linux.go new file mode 100644 index 0000000..71e2106 --- /dev/null +++ b/cmd/vm/main_linux.go @@ -0,0 +1,222 @@ +package main + +import ( + "encoding/binary" + "flag" + "fmt" + "io" + "net" + "net/http" + "os" + "os/exec" + "strings" + "time" + + "github.com/containers/gvisor-tap-vsock/pkg/transport" + "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/songgao/packets/ethernet" + "github.com/songgao/water" + "github.com/vishvananda/netlink" + "gvisor.dev/gvisor/pkg/tcpip/header" +) + +var ( + endpoint string + iface string + stopIfIfaceExist string + mac string + debug bool + mtu int + tapPreexists bool +) + +func main() { + version := types.NewVersion("gvforwarder") + version.AddFlag() + flag.StringVar(&endpoint, "url", fmt.Sprintf("vsock://2:1024%s", types.ConnectPath), "url where the tap send packets") + flag.StringVar(&iface, "iface", "tap0", "tap interface name") + flag.StringVar(&stopIfIfaceExist, "stop-if-exist", "eth0,ens3,enp0s1", "stop if one of these interfaces exists at startup") + flag.StringVar(&mac, "mac", "5a:94:ef:e4:0c:ee", "mac address") + flag.BoolVar(&debug, "debug", false, "debug") + flag.IntVar(&mtu, "mtu", 4000, "mtu") + flag.BoolVar(&tapPreexists, "preexisting", false, "use preexisting/preconfigured TAP interface") + flag.Parse() + + if version.ShowVersion() { + fmt.Println(version.String()) + os.Exit(0) + } + + expected := strings.Split(stopIfIfaceExist, ",") + links, err := netlink.LinkList() + if err != nil { + log.Fatal(err) + } + for _, link := range links { + if contains(expected, link.Attrs().Name) { + log.Infof("interface %s prevented this program to run", link.Attrs().Name) + return + } + } + for { + if err := run(); err != nil { + log.Error(err) + } + time.Sleep(time.Second) + } +} + +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + +func run() error { + conn, path, err := transport.Dial(endpoint) + if err != nil { + return errors.Wrap(err, "cannot connect to host") + } + defer conn.Close() + + if path != "" { + req, err := http.NewRequest("POST", path, nil) + if err != nil { + return err + } + if err := req.Write(conn); err != nil { + return err + } + } + + tap, err := water.New(water.Config{ + DeviceType: water.TAP, + PlatformSpecificParams: water.PlatformSpecificParams{ + Name: iface, + }, + }) + if err != nil { + return errors.Wrap(err, "cannot create tap device") + } + defer tap.Close() + + if !tapPreexists { + if err := linkUp(); err != nil { + return errors.Wrap(err, "cannot set mac address") + } + } + + errCh := make(chan error, 1) + go tx(conn, tap, errCh, mtu) + go rx(conn, tap, errCh, mtu) + if !tapPreexists { + go func() { + if err := dhcp(); err != nil { + errCh <- errors.Wrap(err, "dhcp error") + } + }() + } + return <-errCh +} + +func linkUp() error { + link, err := netlink.LinkByName(iface) + if err != nil { + return err + } + if mac == "" { + return netlink.LinkSetUp(link) + } + hw, err := net.ParseMAC(mac) + if err != nil { + return err + } + if err := netlink.LinkSetHardwareAddr(link, hw); err != nil { + return err + } + return netlink.LinkSetUp(link) +} + +func dhcp() error { + if _, err := exec.LookPath("udhcpc"); err == nil { // busybox dhcp client + cmd := exec.Command("udhcpc", "-f", "-q", "-i", iface, "-v") + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + return cmd.Run() + } + cmd := exec.Command("dhclient", "-4", "-d", "-v", iface) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + return cmd.Run() +} + +func rx(conn net.Conn, tap *water.Interface, errCh chan error, mtu int) { + log.Info("waiting for packets...") + size := make([]byte, 2) + var frame ethernet.Frame + for { + frame.Resize(mtu) + n, err := tap.Read([]byte(frame)) + if err != nil { + errCh <- errors.Wrap(err, "cannot read packet from tap") + return + } + frame = frame[:n] + + if debug { + packet := gopacket.NewPacket(frame, layers.LayerTypeEthernet, gopacket.Default) + log.Info(packet.String()) + } + + binary.LittleEndian.PutUint16(size, uint16(n)) + if _, err := conn.Write(append(size, frame...)); err != nil { + errCh <- errors.Wrap(err, "cannot write size and packet to socket") + return + } + } +} + +func tx(conn net.Conn, tap *water.Interface, errCh chan error, mtu int) { + sizeBuf := make([]byte, 2) + buf := make([]byte, mtu+header.EthernetMinimumSize) + + for { + n, err := io.ReadFull(conn, sizeBuf) + if err != nil { + errCh <- errors.Wrap(err, "cannot read size from socket") + return + } + if n != 2 { + errCh <- fmt.Errorf("unexpected size %d", n) + return + } + size := int(binary.LittleEndian.Uint16(sizeBuf[0:2])) + + n, err = io.ReadFull(conn, buf[:size]) + if err != nil { + errCh <- errors.Wrap(err, "cannot read payload from socket") + return + } + if n == 0 || n != size { + errCh <- fmt.Errorf("unexpected size %d != %d", n, size) + return + } + + if debug { + packet := gopacket.NewPacket(buf[:size], layers.LayerTypeEthernet, gopacket.Default) + log.Info(packet.String()) + } + + if _, err := tap.Write(buf[:size]); err != nil { + errCh <- errors.Wrap(err, "cannot write packet to tap") + return + } + } +} diff --git a/cmd/win-sshproxy/event-hook.go b/cmd/win-sshproxy/event-hook.go new file mode 100644 index 0000000..a5c23e4 --- /dev/null +++ b/cmd/win-sshproxy/event-hook.go @@ -0,0 +1,73 @@ +//go:build windows +// +build windows + +package main + +import ( + "bytes" + "fmt" + + "github.com/sirupsen/logrus" + "golang.org/x/sys/windows/svc/eventlog" +) + +// Logrus hook that delegates to windows event log +type EventLogHook struct { + events *eventlog.Log +} + +type LogFormat struct { + name string +} + +func (f *LogFormat) Format(entry *logrus.Entry) ([]byte, error) { + var b *bytes.Buffer + + if entry.Buffer != nil { + b = entry.Buffer + } else { + b = &bytes.Buffer{} + } + + fmt.Fprintf(b, "[%-5s] %s: %s", entry.Level.String(), f.name, entry.Message) + + for key, value := range entry.Data { + fmt.Fprintf(b, " {%s = %s}", key, value) + } + + b.WriteByte('\n') + return b.Bytes(), nil +} + +func NewEventHook(events *eventlog.Log, name string) *EventLogHook { + logrus.SetFormatter(&LogFormat{name}) + return &EventLogHook{events} +} + +func (hook *EventLogHook) Fire(entry *logrus.Entry) error { + line, err := entry.String() + if err != nil { + return err + } + + switch entry.Level { + case logrus.PanicLevel: + return hook.events.Error(1002, line) + case logrus.FatalLevel: + return hook.events.Error(1001, line) + case logrus.ErrorLevel: + return hook.events.Error(1000, line) + case logrus.WarnLevel: + return hook.events.Warning(1000, line) + case logrus.InfoLevel: + return hook.events.Info(1000, line) + case logrus.DebugLevel, logrus.TraceLevel: + return hook.events.Info(1001, line) + default: + return nil + } +} + +func (hook *EventLogHook) Levels() []logrus.Level { + return logrus.AllLevels +} diff --git a/cmd/win-sshproxy/main.go b/cmd/win-sshproxy/main.go new file mode 100644 index 0000000..73d1fdd --- /dev/null +++ b/cmd/win-sshproxy/main.go @@ -0,0 +1,196 @@ +//go:build windows +// +build windows + +package main + +import ( + "context" + "fmt" + "net/url" + "os" + "path/filepath" + "strings" + "syscall" + "unsafe" + + "github.com/containers/gvisor-tap-vsock/pkg/sshclient" + "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/containers/winquit/pkg/winquit" + "github.com/sirupsen/logrus" + "golang.org/x/sync/errgroup" + "golang.org/x/sys/windows/svc/eventlog" +) + +const ( + ERR_BAD_ARGS = 0x000A + WM_QUIT = 0x12 +) + +var ( + stateDir string + debug bool +) + +func main() { + args := os.Args + if len(args) > 1 { + switch args[1] { + case "-version": + version := types.NewVersion("win-sshproxy") + fmt.Println(version.String()) + os.Exit(0) + case "-debug": + debug = true + args = args[2:] + default: + args = args[1:] + } + } + + if len(args) < 5 || (len(args)-2)%3 != 0 { + alert("Usage: " + filepath.Base(os.Args[0]) + "(-debug) [name] [statedir] ([source] [dest] [identity])... \n\nThis facilty proxies windows pipes and unix sockets over ssh using the specified identity.") + os.Exit(ERR_BAD_ARGS) + } + + log, err := setupLogging(args[0]) + if err != nil { + os.Exit(1) + } + defer log.Close() + + stateDir = args[1] + + var sources, dests, identities []string + for i := 2; i < len(args)-2; i += 3 { + sources = append(sources, args[i]) + dests = append(dests, args[i+1]) + identities = append(identities, args[i+2]) + } + + ctx, cancel := context.WithCancel(context.Background()) + group, ctx := errgroup.WithContext(ctx) + + quit := make(chan bool, 1) + // Wait for a WM_QUIT message to exit + winquit.NotifyOnQuit(quit) + go func() { + <-quit + cancel() + }() + + // Save thread for legacy callers which use it to post a quit + if _, err := saveThreadId(); err != nil { + logrus.Errorf("Error saving thread id: " + err.Error()) + } + + logrus.Debug("Setting up proxies") + setupProxies(ctx, group, sources, dests, identities) + + // Wait for cmopletion (cancellation) or error + if err := group.Wait(); err != nil { + logrus.Errorf("Error occured in execution group: " + err.Error()) + os.Exit(1) + } +} + +func setupLogging(name string) (*eventlog.Log, error) { + // Reuse the Built-in .NET Runtime Source so that we do not + // have to provide a messaage table and modify the system + // event configuration + log, err := eventlog.Open(".NET Runtime") + if err != nil { + return nil, err + } + + logrus.AddHook(NewEventHook(log, name)) + if debug { + logrus.SetLevel(logrus.DebugLevel) + } else { + logrus.SetLevel(logrus.InfoLevel) + } + + return log, nil +} + +func setupProxies(ctx context.Context, g *errgroup.Group, sources []string, dests []string, identities []string) error { + for i := 0; i < len(sources); i++ { + var ( + src *url.URL + dest *url.URL + err error + ) + if strings.Contains(sources[i], "://") { + src, err = url.Parse(sources[i]) + if err != nil { + return err + } + } else { + src = &url.URL{ + Scheme: "unix", + Path: sources[i], + } + } + + dest, err = url.Parse(dests[i]) + if err != nil { + return err + } + j := i + g.Go(func() error { + forward, err := sshclient.CreateSSHForward(ctx, src, dest, identities[j], nil) + if err != nil { + return err + } + go func() { + <-ctx.Done() + // Abort pending accepts + forward.Close() + }() + loop: + for { + select { + case <-ctx.Done(): + break loop + default: + // proceed + } + err := forward.AcceptAndTunnel(ctx) + if err != nil { + logrus.Debugf("Error occurred handling ssh forwarded connection: %q", err) + } + } + return nil + }) + } + + return nil +} + +func saveThreadId() (uint32, error) { + path := filepath.Join(stateDir, "win-sshproxy.tid") + file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) + if err != nil { + return 0, err + } + defer file.Close() + tid := winquit.GetCurrentMessageLoopThreadId() + fmt.Fprintf(file, "%d:%d\n", os.Getpid(), tid) + return tid, nil +} + +// Creates an "error" style pop-up window +func alert(caption string) int { + // Error box style + format := 0x10 + + user32 := syscall.NewLazyDLL("user32.dll") + captionPtr, _ := syscall.UTF16PtrFromString(caption) + titlePtr, _ := syscall.UTF16PtrFromString("winpath") + ret, _, _ := user32.NewProc("MessageBoxW").Call( + uintptr(0), + uintptr(unsafe.Pointer(captionPtr)), + uintptr(unsafe.Pointer(titlePtr)), + uintptr(format)) + + return int(ret) +} diff --git a/contrib/networkmanager/vsock0.nmconnection b/contrib/networkmanager/vsock0.nmconnection new file mode 100644 index 0000000..973eb48 --- /dev/null +++ b/contrib/networkmanager/vsock0.nmconnection @@ -0,0 +1,15 @@ +[connection] +id=vsock0 +type=tun +interface-name=vsock0 + +[tun] +mode=2 + +[802-3-ethernet] +cloned-mac-address=5A:94:EF:E4:0C:EE + +[ipv4] +method=auto + +[proxy] diff --git a/contrib/systemd/gv-user-network@.service b/contrib/systemd/gv-user-network@.service new file mode 100644 index 0000000..6aefeff --- /dev/null +++ b/contrib/systemd/gv-user-network@.service @@ -0,0 +1,13 @@ +[Unit] +Description=gvisor-tap-vsock Network Traffic Forwarder +After=NetworkManager.service +BindsTo=sys-devices-virtual-net-%i.device +After=sys-devices-virtual-net-%i.device + +[Service] +Environment=GV_VSOCK_PORT="1024" +EnvironmentFile=-/etc/sysconfig/gv-user-network +ExecStart=/usr/libexec/podman/gvforwarder -preexisting -iface %i -url vsock://2:${GV_VSOCK_PORT}/connect + +[Install] +WantedBy=multi-user.target diff --git a/doc/curl.png b/doc/curl.png Binary files differnew file mode 100644 index 0000000..3423b75 --- /dev/null +++ b/doc/curl.png diff --git a/doc/http.png b/doc/http.png Binary files differnew file mode 100644 index 0000000..5e2c59d --- /dev/null +++ b/doc/http.png @@ -0,0 +1,49 @@ +module github.com/containers/gvisor-tap-vsock + +go 1.20 + +require ( + github.com/Microsoft/go-winio v0.6.1 + github.com/apparentlymart/go-cidr v1.1.0 + github.com/containers/winquit v1.1.0 + github.com/coreos/stream-metadata-go v0.4.4 + github.com/dustin/go-humanize v1.0.1 + github.com/google/gopacket v1.1.19 + github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f + github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 + github.com/mdlayher/vsock v1.2.1 + github.com/miekg/dns v1.1.58 + github.com/onsi/ginkgo v1.16.5 + github.com/onsi/gomega v1.31.1 + github.com/opencontainers/go-digest v1.0.0 + github.com/pkg/errors v0.9.1 + github.com/sirupsen/logrus v1.9.3 + github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091 + github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 + github.com/stretchr/testify v1.8.4 + github.com/vishvananda/netlink v1.2.1-beta.2 + golang.org/x/crypto v0.18.0 + golang.org/x/sync v0.6.0 + golang.org/x/sys v0.16.0 + gvisor.dev/gvisor v0.0.0-20231023213702-2691a8f9b1cf + inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/mdlayher/socket v0.4.1 // indirect + github.com/nxadm/tail v1.4.8 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect + github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.17.0 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) @@ -0,0 +1,202 @@ +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= +github.com/containers/winquit v1.1.0 h1:jArun04BNDQvt2W0Y78kh9TazN2EIEMG5Im6/JY7+pE= +github.com/containers/winquit v1.1.0/go.mod h1:PsPeZlnbkmGGIToMPHF1zhWjBUkd8aHjMOr/vFcPxw8= +github.com/coreos/stream-metadata-go v0.4.4 h1:PM/6iNhofKGydsatiY1zdnMMHBT34skb5P7nfEFR4GU= +github.com/coreos/stream-metadata-go v0.4.4/go.mod h1:fMObQqQm8Ku91G04btKzEH3AsdP1mrAb986z9aaK0tE= +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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +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.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= +github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f h1:l1QCwn715k8nYkj4Ql50rzEog3WnMdrd4YYMMwemxEo= +github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E= +github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= +github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= +github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= +github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 h1:DZMFueDbfz6PNc1GwDRA8+6lBx1TB9UnxDQliCqR73Y= +github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2/go.mod h1:SWzULI85WerrFt3u+nIm5F9l7EvxZTKQvd0InF3nmgM= +github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE= +github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= +github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= +github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= +github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= +github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= +github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= +github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w= +github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= +github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo= +github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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/songgao/packets v0.0.0-20160404182456-549a10cd4091 h1:1zN6ImoqhSJhN8hGXFaJlSC8msLmIbX8bFqOfWLKw0w= +github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091/go.mod h1:N20Z5Y8oye9a7HmytmZ+tr8Q2vlP0tAHP13kTHzwvQY= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA= +github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= +github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= +github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= +github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +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/mod v0.3.0/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-20180906233101-161cd47e91fd/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-20190419010253-1f3472d942ba/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-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191007182048-72f939374954/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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/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.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/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-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +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/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= +gvisor.dev/gvisor v0.0.0-20231023213702-2691a8f9b1cf h1:0A28IFBR6VcMacM0m6Rn5/nr8pk8xa2TyIkjSaFAOPc= +gvisor.dev/gvisor v0.0.0-20231023213702-2691a8f9b1cf/go.mod h1:8hmigyCdYtw5xJGfQDJzSH5Ju8XEIDBnpyi8+O6GRt8= +inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0 h1:PqdHrvQRVK1zapJkd0qf6+tevvSIcWdfenVqJd3PHWU= +inet.af/tcpproxy v0.0.0-20220326234310-be3ee21c9fa0/go.mod h1:Tojt5kmHpDIR2jMojxzZK2w2ZR7OILODmUo2gaSwjrk= diff --git a/images/Dockerfile b/images/Dockerfile new file mode 100644 index 0000000..c5808e0 --- /dev/null +++ b/images/Dockerfile @@ -0,0 +1,10 @@ +FROM registry.access.redhat.com/ubi9/go-toolset:1.18 AS build +WORKDIR $APP_ROOT/src +COPY . . +RUN make + +FROM busybox +COPY images/udhcpc.sh /usr/share/udhcpc/default.script +RUN chmod +x /usr/share/udhcpc/default.script +COPY --from=build /opt/app-root/src/bin/gvforwarder . +ENTRYPOINT ["/gvforwarder"] diff --git a/images/udhcpc.sh b/images/udhcpc.sh new file mode 100644 index 0000000..327a54e --- /dev/null +++ b/images/udhcpc.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# Busybox udhcpc dispatcher script. Copyright (C) 2009 by Axel Beckert. +# +# Based on the busybox example scripts and the old udhcp source +# package default.* scripts. + +RESOLV_CONF="/etc/resolv.conf" + +case $1 in + bound|renew) + [ -n "$broadcast" ] && BROADCAST="broadcast $broadcast" + [ -n "$subnet" ] && NETMASK="netmask $subnet" + [ -n "$mtu" ] && MTU="mtu $mtu" + + ifconfig $interface $ip $BROADCAST $NETMASK $MTU + + if [ -n "$router" ]; then + echo "$0: Resetting default routes" + while route del default gw 0.0.0.0 dev $interface; do :; done + + metric=0 + for i in $router; do + if [ "$subnet" = "255.255.255.255" ]; then + ip route add default via $i dev $interface metric $metric onlink + else + ip route add default via $i dev $interface metric $metric + fi + metric=$(($metric + 1)) + done + fi + + # Update resolver configuration file + R="" + [ -n "$domain" ] && R="domain $domain +" + for i in $dns; do + echo "$0: Adding DNS $i" + R="${R}nameserver $i +" + done + + if [ -x resolvconf ]; then + echo -n "$R" | resolvconf -a "${interface}.udhcpc" + else + echo -n "$R" > "$RESOLV_CONF" + fi + ;; + + deconfig) + if [ -x resolvconf ]; then + resolvconf -d "${interface}.udhcpc" + fi + ifconfig $interface 0.0.0.0 + ;; + + leasefail) + echo "$0: Lease failed: $message" + ;; + + nak) + echo "$0: Received a NAK: $message" + ;; + + *) + echo "$0: Unknown udhcpc command: $1"; + exit 1; + ;; +esac diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 0000000..63a8042 --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,119 @@ +package client + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/containers/gvisor-tap-vsock/pkg/types" +) + +type Client struct { + client *http.Client + base string +} + +func New(client *http.Client, base string) *Client { + return &Client{ + client: client, + base: base, + } +} + +func (c *Client) List() ([]types.ExposeRequest, error) { + res, err := c.client.Get(fmt.Sprintf("%s%s", c.base, "/services/forwarder/all")) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status: %d", res.StatusCode) + } + dec := json.NewDecoder(res.Body) + var ports []types.ExposeRequest + if err := dec.Decode(&ports); err != nil { + return nil, err + } + return ports, nil +} + +func (c *Client) Expose(req *types.ExposeRequest) error { + bin, err := json.Marshal(req) + if err != nil { + return err + } + res, err := c.client.Post(fmt.Sprintf("%s%s", c.base, "/services/forwarder/expose"), "application/json", bytes.NewReader(bin)) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + err, readErr := io.ReadAll(res.Body) + if readErr != nil { + return fmt.Errorf("error while reading error message: %v", readErr) + } + return errors.New(strings.TrimSpace(string(err))) + } + return nil +} + +func (c *Client) Unexpose(req *types.UnexposeRequest) error { + bin, err := json.Marshal(req) + if err != nil { + return err + } + res, err := c.client.Post(fmt.Sprintf("%s%s", c.base, "/services/forwarder/unexpose"), "application/json", bytes.NewReader(bin)) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + err, readErr := io.ReadAll(res.Body) + if readErr != nil { + return fmt.Errorf("error while reading error message: %v", readErr) + } + return errors.New(strings.TrimSpace(string(err))) + } + return nil +} + +func (c *Client) ListDNS() ([]types.Zone, error) { + res, err := c.client.Get(fmt.Sprintf("%s%s", c.base, "/services/dns/all")) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status: %d", res.StatusCode) + } + dec := json.NewDecoder(res.Body) + var dnsZone []types.Zone + if err := dec.Decode(&dnsZone); err != nil { + return nil, err + } + return dnsZone, nil +} + +func (c *Client) AddDNS(req *types.Zone) error { + bin, err := json.Marshal(req) + if err != nil { + return err + } + res, err := c.client.Post(fmt.Sprintf("%s%s", c.base, "/services/dns/add"), "application/json", bytes.NewReader(bin)) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + err, readErr := io.ReadAll(res.Body) + if readErr != nil { + return fmt.Errorf("error while reading error message: %v", readErr) + } + return errors.New(strings.TrimSpace(string(err))) + } + return nil +} diff --git a/pkg/fs/umask_unix.go b/pkg/fs/umask_unix.go new file mode 100644 index 0000000..3607968 --- /dev/null +++ b/pkg/fs/umask_unix.go @@ -0,0 +1,10 @@ +//go:build !windows +// +build !windows + +package fs + +import "syscall" + +func Umask(mask int) int { + return syscall.Umask(mask) +} diff --git a/pkg/fs/umask_windows.go b/pkg/fs/umask_windows.go new file mode 100644 index 0000000..d99a9f1 --- /dev/null +++ b/pkg/fs/umask_windows.go @@ -0,0 +1,8 @@ +// +build windows + +package fs + +func Umask(mask int) int { + // no-op for now + return 0 +} diff --git a/pkg/net/stdio/dial.go b/pkg/net/stdio/dial.go new file mode 100644 index 0000000..51b69b7 --- /dev/null +++ b/pkg/net/stdio/dial.go @@ -0,0 +1,51 @@ +package stdio + +import ( + "net" + "os" + "os/exec" + "strconv" +) + +func Dial(endpoint string, arg ...string) (net.Conn, error) { + cmd := exec.Command(endpoint, arg...) + cmd.Stderr = os.Stderr + + stdin, err := cmd.StdinPipe() + if err != nil { + return nil, err + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + + err = cmd.Start() + if err != nil { + return nil, err + } + + local := IoAddr{path: strconv.Itoa(os.Getpid())} + remote := IoAddr{path: strconv.Itoa(cmd.Process.Pid)} + conn := IoConn{ + reader: stdout, + writer: stdin, + local: local, + remote: remote, + close: cmd.Process.Kill, + } + return conn, nil +} + +func GetStdioConn() net.Conn { + local := IoAddr{path: strconv.Itoa(os.Getpid())} + remote := IoAddr{path: "remote"} + conn := IoConn{ + writer: os.Stdout, + reader: os.Stdin, + local: local, + remote: remote, + } + return conn +} diff --git a/pkg/net/stdio/ioaddr.go b/pkg/net/stdio/ioaddr.go new file mode 100644 index 0000000..4ed69a5 --- /dev/null +++ b/pkg/net/stdio/ioaddr.go @@ -0,0 +1,12 @@ +package stdio + +type IoAddr struct { + path string +} + +func (a IoAddr) Network() string { + return "stdio" +} +func (a IoAddr) String() string { + return a.path +} diff --git a/pkg/net/stdio/ioconn.go b/pkg/net/stdio/ioconn.go new file mode 100644 index 0000000..6c180da --- /dev/null +++ b/pkg/net/stdio/ioconn.go @@ -0,0 +1,50 @@ +package stdio + +import ( + "io" + "net" + "time" +) + +type IoConn struct { + writer io.Writer + reader io.Reader + local net.Addr + remote net.Addr + close func() error +} + +func (c IoConn) Read(b []byte) (n int, err error) { + return c.reader.Read(b) +} + +func (c IoConn) Write(b []byte) (n int, err error) { + return c.writer.Write(b) +} + +func (c IoConn) Close() error { + if c.close != nil { + return c.close() + } + return nil +} + +func (c IoConn) LocalAddr() net.Addr { + return c.local +} + +func (c IoConn) RemoteAddr() net.Addr { + return c.remote +} + +func (c IoConn) SetDeadline(_ time.Time) error { + return nil +} + +func (c IoConn) SetReadDeadline(_ time.Time) error { + return nil +} + +func (c IoConn) SetWriteDeadline(_ time.Time) error { + return nil +} diff --git a/pkg/services/dhcp/dhcp.go b/pkg/services/dhcp/dhcp.go new file mode 100644 index 0000000..f464a31 --- /dev/null +++ b/pkg/services/dhcp/dhcp.go @@ -0,0 +1,127 @@ +package dhcp + +import ( + "encoding/json" + "errors" + "net" + "net/http" + "time" + + "github.com/containers/gvisor-tap-vsock/pkg/tap" + "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/insomniacslk/dhcp/dhcpv4/server4" + "github.com/insomniacslk/dhcp/rfc1035label" + log "github.com/sirupsen/logrus" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" + "gvisor.dev/gvisor/pkg/waiter" +) + +const serverPort = 67 + +func handler(configuration *types.Configuration, ipPool *tap.IPPool) server4.Handler { + return func(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) { + reply, err := dhcpv4.NewReplyFromRequest(m) + if err != nil { + log.Errorf("dhcp: cannot build reply from request: %v", err) + return + } + + ip, err := ipPool.GetOrAssign(m.ClientHWAddr.String()) + if err != nil { + log.Errorf("dhcp: cannot assign ip: %v", err) + return + } + + _, parsedSubnet, err := net.ParseCIDR(configuration.Subnet) + if err != nil { + log.Errorf("dhcp: invalid subnet %v", err) + return + } + + reply.YourIPAddr = ip + reply.UpdateOption(dhcpv4.OptServerIdentifier(net.ParseIP(configuration.GatewayIP))) + reply.UpdateOption(dhcpv4.OptIPAddressLeaseTime(time.Hour)) + + reply.UpdateOption(dhcpv4.Option{Code: dhcpv4.OptionSubnetMask, Value: dhcpv4.IP(parsedSubnet.Mask)}) + reply.UpdateOption(dhcpv4.Option{Code: dhcpv4.OptionRouter, Value: dhcpv4.IP(net.ParseIP(configuration.GatewayIP))}) + reply.UpdateOption(dhcpv4.Option{Code: dhcpv4.OptionDomainNameServer, Value: dhcpv4.IPs([]net.IP{net.ParseIP(configuration.GatewayIP)})}) + reply.UpdateOption(dhcpv4.Option{Code: dhcpv4.OptionInterfaceMTU, Value: dhcpv4.Uint16(configuration.MTU)}) + reply.UpdateOption(dhcpv4.Option{Code: dhcpv4.OptionDNSDomainSearchList, Value: &rfc1035label.Labels{ + Labels: configuration.DNSSearchDomains, + }}) + + switch mt := m.MessageType(); mt { + case dhcpv4.MessageTypeDiscover: + reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer)) + case dhcpv4.MessageTypeRequest: + reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) + default: + log.Errorf("dhcp: unhandled message type: %v", mt) + return + } + + if _, err := conn.WriteTo(reply.ToBytes(), peer); err != nil { + log.Errorf("dhcp: cannot reply to client: %v", err) + } + } +} + +func dial(s *stack.Stack, nic int) (*gonet.UDPConn, error) { + var wq waiter.Queue + ep, err := s.NewEndpoint(udp.ProtocolNumber, ipv4.ProtocolNumber, &wq) + if err != nil { + return nil, errors.New(err.String()) + } + + ep.SocketOptions().SetBroadcast(true) + + if err := ep.Bind(tcpip.FullAddress{ + NIC: tcpip.NICID(nic), + Addr: tcpip.Address{}, + Port: uint16(serverPort), + }); err != nil { + ep.Close() + return nil, errors.New(err.String()) + } + + return gonet.NewUDPConn(s, &wq, ep), nil +} + +type Server struct { + Underlying *server4.Server + IPPool *tap.IPPool +} + +func New(configuration *types.Configuration, stack *stack.Stack, ipPool *tap.IPPool) (*Server, error) { + ln, err := dial(stack, 1) + if err != nil { + return nil, err + } + + s, err := server4.NewServer("", nil, handler(configuration, ipPool), server4.WithConn(ln)) + if err != nil { + return nil, err + } + + return &Server{ + Underlying: s, + IPPool: ipPool, + }, nil +} + +func (s *Server) Serve() error { + return s.Underlying.Serve() +} + +func (s *Server) Mux() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/leases", func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(s.IPPool.Leases()) + }) + return mux +} diff --git a/pkg/services/dns/dns.go b/pkg/services/dns/dns.go new file mode 100644 index 0000000..15ed72f --- /dev/null +++ b/pkg/services/dns/dns.go @@ -0,0 +1,269 @@ +package dns + +import ( + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "strings" + "sync" + + "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/miekg/dns" + log "github.com/sirupsen/logrus" +) + +type dnsHandler struct { + zones []types.Zone + zonesLock sync.RWMutex +} + +func (h *dnsHandler) handle(w dns.ResponseWriter, r *dns.Msg, responseMessageSize int) { + m := new(dns.Msg) + m.SetReply(r) + m.RecursionAvailable = true + h.addAnswers(m) + edns0 := r.IsEdns0() + if edns0 != nil { + responseMessageSize = int(edns0.UDPSize()) + } + m.Truncate(responseMessageSize) + if err := w.WriteMsg(m); err != nil { + log.Error(err) + } +} + +func (h *dnsHandler) handleTCP(w dns.ResponseWriter, r *dns.Msg) { + h.handle(w, r, dns.MaxMsgSize) +} + +func (h *dnsHandler) handleUDP(w dns.ResponseWriter, r *dns.Msg) { + h.handle(w, r, dns.MinMsgSize) +} + +func (h *dnsHandler) addAnswers(m *dns.Msg) { + h.zonesLock.RLock() + defer h.zonesLock.RUnlock() + for _, q := range m.Question { + for _, zone := range h.zones { + zoneSuffix := fmt.Sprintf(".%s", zone.Name) + if strings.HasSuffix(q.Name, zoneSuffix) { + if q.Qtype != dns.TypeA { + return + } + for _, record := range zone.Records { + withoutZone := strings.TrimSuffix(q.Name, zoneSuffix) + if (record.Name != "" && record.Name == withoutZone) || + (record.Regexp != nil && record.Regexp.MatchString(withoutZone)) { + m.Answer = append(m.Answer, &dns.A{ + Hdr: dns.RR_Header{ + Name: q.Name, + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: 0, + }, + A: record.IP, + }) + return + } + } + if !zone.DefaultIP.Equal(net.IP("")) { + m.Answer = append(m.Answer, &dns.A{ + Hdr: dns.RR_Header{ + Name: q.Name, + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: 0, + }, + A: zone.DefaultIP, + }) + return + } + m.Rcode = dns.RcodeNameError + return + } + } + + resolver := net.Resolver{ + PreferGo: false, + } + switch q.Qtype { + case dns.TypeA: + ips, err := resolver.LookupIPAddr(context.TODO(), q.Name) + if err != nil { + m.Rcode = dns.RcodeNameError + return + } + for _, ip := range ips { + if len(ip.IP.To4()) != net.IPv4len { + continue + } + m.Answer = append(m.Answer, &dns.A{ + Hdr: dns.RR_Header{ + Name: q.Name, + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: 0, + }, + A: ip.IP.To4(), + }) + } + case dns.TypeCNAME: + cname, err := resolver.LookupCNAME(context.TODO(), q.Name) + if err != nil { + m.Rcode = dns.RcodeNameError + return + } + m.Answer = append(m.Answer, &dns.CNAME{ + Hdr: dns.RR_Header{ + Name: q.Name, + Rrtype: dns.TypeCNAME, + Class: dns.ClassINET, + Ttl: 0, + }, + Target: cname, + }) + case dns.TypeMX: + records, err := resolver.LookupMX(context.TODO(), q.Name) + if err != nil { + m.Rcode = dns.RcodeNameError + return + } + for _, mx := range records { + m.Answer = append(m.Answer, &dns.MX{ + Hdr: dns.RR_Header{ + Name: q.Name, + Rrtype: dns.TypeMX, + Class: dns.ClassINET, + Ttl: 0, + }, + Mx: mx.Host, + Preference: mx.Pref, + }) + } + case dns.TypeNS: + records, err := resolver.LookupNS(context.TODO(), q.Name) + if err != nil { + m.Rcode = dns.RcodeNameError + return + } + for _, ns := range records { + m.Answer = append(m.Answer, &dns.NS{ + Hdr: dns.RR_Header{ + Name: q.Name, + Rrtype: dns.TypeNS, + Class: dns.ClassINET, + Ttl: 0, + }, + Ns: ns.Host, + }) + } + case dns.TypeSRV: + _, records, err := resolver.LookupSRV(context.TODO(), "", "", q.Name) + if err != nil { + m.Rcode = dns.RcodeNameError + return + } + for _, srv := range records { + m.Answer = append(m.Answer, &dns.SRV{ + Hdr: dns.RR_Header{ + Name: q.Name, + Rrtype: dns.TypeSRV, + Class: dns.ClassINET, + Ttl: 0, + }, + Port: srv.Port, + Priority: srv.Priority, + Target: srv.Target, + Weight: srv.Weight, + }) + } + case dns.TypeTXT: + records, err := resolver.LookupTXT(context.TODO(), q.Name) + if err != nil { + m.Rcode = dns.RcodeNameError + return + } + m.Answer = append(m.Answer, &dns.TXT{ + Hdr: dns.RR_Header{ + Name: q.Name, + Rrtype: dns.TypeTXT, + Class: dns.ClassINET, + Ttl: 0, + }, + Txt: records, + }) + } + } +} + +type Server struct { + udpConn net.PacketConn + tcpLn net.Listener + handler *dnsHandler +} + +func New(udpConn net.PacketConn, tcpLn net.Listener, zones []types.Zone) (*Server, error) { + handler := &dnsHandler{zones: zones} + return &Server{udpConn: udpConn, tcpLn: tcpLn, handler: handler}, nil +} + +func (s *Server) Serve() error { + mux := dns.NewServeMux() + mux.HandleFunc(".", s.handler.handleUDP) + srv := &dns.Server{ + PacketConn: s.udpConn, + Handler: mux, + } + return srv.ActivateAndServe() +} + +func (s *Server) ServeTCP() error { + mux := dns.NewServeMux() + mux.HandleFunc(".", s.handler.handleTCP) + tcpSrv := &dns.Server{ + Listener: s.tcpLn, + Handler: mux, + } + return tcpSrv.ActivateAndServe() +} + +func (s *Server) Mux() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/all", func(w http.ResponseWriter, r *http.Request) { + s.handler.zonesLock.RLock() + _ = json.NewEncoder(w).Encode(s.handler.zones) + s.handler.zonesLock.RUnlock() + }) + + mux.HandleFunc("/add", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "post only", http.StatusBadRequest) + return + } + var req types.Zone + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + s.addZone(req) + w.WriteHeader(http.StatusOK) + }) + return mux +} + +func (s *Server) addZone(req types.Zone) { + s.handler.zonesLock.Lock() + defer s.handler.zonesLock.Unlock() + for i, zone := range s.handler.zones { + if zone.Name == req.Name { + req.Records = append(req.Records, zone.Records...) + s.handler.zones[i] = req + return + } + } + // No existing zone for req.Name, add new one + s.handler.zones = append(s.handler.zones, req) +} diff --git a/pkg/services/dns/dns_test.go b/pkg/services/dns/dns_test.go new file mode 100644 index 0000000..f01488d --- /dev/null +++ b/pkg/services/dns/dns_test.go @@ -0,0 +1,194 @@ +package dns + +import ( + "net" + "testing" + + "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/onsi/ginkgo" + "github.com/onsi/gomega" +) + +func TestSuite(t *testing.T) { + gomega.RegisterFailHandler(ginkgo.Fail) + ginkgo.RunSpecs(t, "gvisor-tap-vsock dns suit") +} + +var _ = ginkgo.Describe("dns add test", func() { + var server *Server + + ginkgo.BeforeEach(func() { + server, _ = New(nil, nil, []types.Zone{}) + }) + + ginkgo.It("should add dns zone with ip", func() { + req := types.Zone{ + Name: "internal.", + DefaultIP: net.ParseIP("192.168.0.1"), + } + server.addZone(req) + + gomega.Expect(server.handler.zones).To(gomega.Equal([]types.Zone{req})) + }) + + ginkgo.It("should add dns zone with record", func() { + req := types.Zone{ + Name: "internal.", + Records: []types.Record{{ + Name: "crc.testiing", + IP: net.ParseIP("192.168.0.2"), + }}, + } + server.addZone(req) + + gomega.Expect(server.handler.zones).To(gomega.Equal([]types.Zone{req})) + }) + + ginkgo.It("should add dns zone with record and ip", func() { + ipReq := types.Zone{ + Name: "dynamic.internal.", + DefaultIP: net.ParseIP("192.168.0.1"), + } + recordReq := types.Zone{ + Name: "internal.", + Records: []types.Record{{ + Name: "crc.testiing", + IP: net.ParseIP("192.168.0.2"), + }}, + } + server.addZone(ipReq) + server.addZone(recordReq) + + gomega.Expect(server.handler.zones).To(gomega.Equal([]types.Zone{ipReq, recordReq})) + }) + + ginkgo.It("should add new zone to existing zone with default ip", func() { + ipReq := types.Zone{ + Name: "internal.", + DefaultIP: net.ParseIP("192.168.0.1"), + } + server.addZone(ipReq) + recordReq := types.Zone{ + Name: "internal.", + Records: []types.Record{{ + Name: "crc.testing", + IP: net.ParseIP("192.168.0.2"), + }}, + } + server.addZone(recordReq) + + gomega.Expect(server.handler.zones).To(gomega.Equal([]types.Zone{{ + Name: "internal.", + Records: []types.Record{{ + Name: "crc.testing", + IP: net.ParseIP("192.168.0.2"), + }}, + }})) + }) + + ginkgo.It("should add new zone to existing zone with records", func() { + ipReq := types.Zone{ + Name: "internal.", + Records: []types.Record{{ + Name: "crc.testing", + IP: net.ParseIP("192.168.0.2"), + }}, + } + server.addZone(ipReq) + recordReq := types.Zone{ + Name: "internal.", + Records: []types.Record{{ + Name: "crc.testing", + IP: net.ParseIP("192.168.0.3"), + }}, + } + server.addZone(recordReq) + + gomega.Expect(server.handler.zones).To(gomega.Equal([]types.Zone{{ + Name: "internal.", + Records: []types.Record{{ + Name: "crc.testing", + IP: net.ParseIP("192.168.0.3"), + }, { + Name: "crc.testing", + IP: net.ParseIP("192.168.0.2"), + }}, + }})) + }) + + ginkgo.It("should add new zone to existing zone with records", func() { + ipReq := types.Zone{ + Name: "internal.", + Records: []types.Record{{ + Name: "crc.testing", + IP: net.ParseIP("192.168.0.2"), + }}, + } + server.addZone(ipReq) + recordReq := types.Zone{ + Name: "internal.", + Records: []types.Record{{ + Name: "crc.testing", + IP: net.ParseIP("192.168.0.3"), + }}, + } + server.addZone(recordReq) + + gomega.Expect(server.handler.zones).To(gomega.Equal([]types.Zone{{ + Name: "internal.", + Records: []types.Record{{ + Name: "crc.testing", + IP: net.ParseIP("192.168.0.3"), + }, { + Name: "crc.testing", + IP: net.ParseIP("192.168.0.2"), + }}, + }})) + }) + + ginkgo.It("should retain the order of zones", func() { + server, _ = New(nil, nil, []types.Zone{ + { + Name: "crc.testing.", + DefaultIP: net.ParseIP("192.168.127.2"), + }, + { + Name: "testing.", + Records: []types.Record{ + { + Name: "host", + IP: net.ParseIP("192.168.127.3"), + }, + }, + }, + }) + server.addZone(types.Zone{ + Name: "testing.", + Records: []types.Record{ + { + Name: "gateway", + IP: net.ParseIP("192.168.127.1"), + }, + }, + }) + gomega.Expect(server.handler.zones).To(gomega.Equal([]types.Zone{ + { + Name: "crc.testing.", + DefaultIP: net.ParseIP("192.168.127.2"), + }, + { + Name: "testing.", + Records: []types.Record{ + { + Name: "gateway", + IP: net.ParseIP("192.168.127.1"), + }, + { + Name: "host", + IP: net.ParseIP("192.168.127.3"), + }, + }, + }, + })) + }) +}) diff --git a/pkg/services/forwarder/ports.go b/pkg/services/forwarder/ports.go new file mode 100644 index 0000000..828c248 --- /dev/null +++ b/pkg/services/forwarder/ports.go @@ -0,0 +1,392 @@ +package forwarder + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "sort" + "strconv" + "strings" + "sync" + + "github.com/containers/gvisor-tap-vsock/pkg/sshclient" + "github.com/containers/gvisor-tap-vsock/pkg/types" + log "github.com/sirupsen/logrus" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "inet.af/tcpproxy" +) + +type PortsForwarder struct { + stack *stack.Stack + + proxiesLock sync.Mutex + proxies map[string]proxy +} + +type proxy struct { + Local string `json:"local"` + Remote string `json:"remote"` + Protocol string `json:"protocol"` + underlying io.Closer +} + +type gonetDialer struct { + stack *stack.Stack +} + +func (d *gonetDialer) DialContextTCP(ctx context.Context, addr string) (conn net.Conn, e error) { + address, err := tcpipAddress(1, addr) + if err != nil { + return nil, err + } + + return gonet.DialContextTCP(ctx, d.stack, address, ipv4.ProtocolNumber) +} + +type CloseWrapper func() error + +func (w CloseWrapper) Close() error { + return w() +} + +func NewPortsForwarder(s *stack.Stack) *PortsForwarder { + return &PortsForwarder{ + stack: s, + proxies: make(map[string]proxy), + } +} + +func (f *PortsForwarder) Expose(protocol types.TransportProtocol, local, remote string) error { + f.proxiesLock.Lock() + defer f.proxiesLock.Unlock() + if _, ok := f.proxies[local]; ok { + return errors.New("proxy already running") + } + + switch protocol { + case types.UNIX, types.NPIPE: + // parse URI for remote + remoteURI, err := url.Parse(remote) + if err != nil { + return fmt.Errorf("failed to parse remote uri :%s : %w", remote, err) + } + + // build the address from remoteURI + remoteAddr := fmt.Sprintf("%s:%s", remoteURI.Hostname(), remoteURI.Port()) + + // dialFn opens remote connection for the proxy + var dialFn func(ctx context.Context, network, addr string) (conn net.Conn, e error) + + var cleanup func() + + // dialFn is set based on the protocol provided by remoteURI.Scheme + switch remoteURI.Scheme { + case "ssh-tunnel": // unix-to-unix proxy (over SSH) + // query string to map for the remoteURI contains ssh config info + remoteQuery := remoteURI.Query() + + // key + sshkeypath := firstValueOrEmpty(remoteQuery["key"]) + if sshkeypath == "" { + return fmt.Errorf("key not provided for unix-ssh connection") + } + + // passphrase + passphrase := firstValueOrEmpty(remoteQuery["passphrase"]) + + // default ssh port if not set + if remoteURI.Port() == "" { + remoteURI.Host = fmt.Sprintf("%s:%s", remoteURI.Hostname(), "22") + } + + // check the remoteURI path provided for nonsense + if remoteURI.Path == "" || remoteURI.Path == "/" { + return fmt.Errorf("remote uri must contain a path to a socket file") + } + + // captured and used by dialFn + var sshForward *sshclient.SSHForward + var connLock sync.Mutex + + dialFn = func(ctx context.Context, network, addr string) (net.Conn, error) { + connLock.Lock() + defer connLock.Unlock() + + if sshForward == nil { + client, err := sshclient.CreateSSHForwardPassphrase(ctx, &url.URL{}, remoteURI, sshkeypath, passphrase, &gonetDialer{f.stack}) + if err != nil { + return nil, err + } + sshForward = client + } + + return sshForward.Tunnel(ctx) + } + + cleanup = func() { + if sshForward != nil { + sshForward.Close() + } + } + + case "tcp": // unix-to-tcp proxy + // build address + address, err := tcpipAddress(1, remoteAddr) + if err != nil { + return err + } + + dialFn = func(ctx context.Context, network, addr string) (conn net.Conn, e error) { + return gonet.DialContextTCP(ctx, f.stack, address, ipv4.ProtocolNumber) + } + + default: + return fmt.Errorf("remote protocol for unix forwarder is not implemented: %s", remoteURI.Scheme) + } + + // build the tcp proxy + var p tcpproxy.Proxy + switch protocol { + case types.UNIX: + p.ListenFunc = func(_, socketPath string) (net.Listener, error) { + // remove existing socket file + if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) { + return nil, err + } + return net.Listen("unix", socketPath) // override tcp to use unix socket + } + case types.NPIPE: + p.ListenFunc = func(_, socketPath string) (net.Listener, error) { + npipeURI, err := url.Parse(socketPath) + if err != nil { + return nil, err + } + return sshclient.ListenNpipe(npipeURI) + } + } + p.AddRoute(local, &tcpproxy.DialProxy{ + Addr: remoteAddr, + DialContext: dialFn, + }) + if err := p.Start(); err != nil { + return err + } + go func() { + if err := p.Wait(); err != nil { + log.Error(err) + } + }() + f.proxies[key(protocol, local)] = proxy{ + Protocol: string(protocol), + Local: local, + Remote: remote, + underlying: CloseWrapper(func() error { + if cleanup != nil { + cleanup() + } + return p.Close() + }), + } + case types.UDP: + address, err := tcpipAddress(1, remote) + if err != nil { + return err + } + + addr, err := net.ResolveUDPAddr("udp", local) + if err != nil { + return err + } + listener, err := net.ListenUDP("udp", addr) + if err != nil { + return err + } + p, err := NewUDPProxy(listener, func() (net.Conn, error) { + return gonet.DialUDP(f.stack, nil, &address, ipv4.ProtocolNumber) + }) + if err != nil { + return err + } + go p.Run() + f.proxies[key(protocol, local)] = proxy{ + Protocol: "udp", + Local: local, + Remote: remote, + underlying: p, + } + case types.TCP: + address, err := tcpipAddress(1, remote) + if err != nil { + return err + } + + var p tcpproxy.Proxy + p.AddRoute(local, &tcpproxy.DialProxy{ + Addr: remote, + DialContext: func(ctx context.Context, network, addr string) (conn net.Conn, e error) { + return gonet.DialContextTCP(ctx, f.stack, address, ipv4.ProtocolNumber) + }, + }) + if err := p.Start(); err != nil { + return err + } + go func() { + if err := p.Wait(); err != nil { + log.Error(err) + } + }() + f.proxies[key(protocol, local)] = proxy{ + Protocol: "tcp", + Local: local, + Remote: remote, + underlying: &p, + } + default: + return fmt.Errorf("unknown protocol %s", protocol) + } + return nil +} + +func key(protocol types.TransportProtocol, local string) string { + return fmt.Sprintf("%s/%s", protocol, local) +} + +func (f *PortsForwarder) Unexpose(protocol types.TransportProtocol, local string) error { + f.proxiesLock.Lock() + defer f.proxiesLock.Unlock() + proxy, ok := f.proxies[key(protocol, local)] + if !ok { + return errors.New("proxy not found") + } + delete(f.proxies, key(protocol, local)) + return proxy.underlying.Close() +} + +func (f *PortsForwarder) Mux() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/all", func(w http.ResponseWriter, r *http.Request) { + f.proxiesLock.Lock() + defer f.proxiesLock.Unlock() + ret := make([]proxy, 0) + for _, proxy := range f.proxies { + ret = append(ret, proxy) + } + sort.Slice(ret, func(i, j int) bool { + if ret[i].Local == ret[j].Local { + return ret[i].Protocol < ret[j].Protocol + } + return ret[i].Local < ret[j].Local + }) + _ = json.NewEncoder(w).Encode(ret) + }) + mux.HandleFunc("/expose", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "post only", http.StatusBadRequest) + return + } + var req types.ExposeRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if req.Protocol == "" { + req.Protocol = types.TCP + } + + // contains unparsed remote field + remoteAddr := req.Remote + + // TCP and UDP rely on remote() to preparse the remote field + if req.Protocol != types.UNIX && req.Protocol != types.NPIPE { + var err error + remoteAddr, err = remote(req, r.RemoteAddr) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + } + + if err := f.Expose(req.Protocol, req.Local, remoteAddr); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + }) + mux.HandleFunc("/unexpose", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "post only", http.StatusBadRequest) + return + } + var req types.UnexposeRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if req.Protocol == "" { + req.Protocol = types.TCP + } + if err := f.Unexpose(req.Protocol, req.Local); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + }) + return mux +} + +// if the request doesn't have an IP in the remote field, use the IP from the incoming http request. +func remote(req types.ExposeRequest, ip string) (string, error) { + remoteIP, _, err := net.SplitHostPort(req.Remote) + if err != nil { + return "", err + } + if remoteIP == "" { + host, _, err := net.SplitHostPort(ip) + if err != nil { + return "", err + } + return fmt.Sprintf("%s%s", host, req.Remote), nil + } + return req.Remote, nil +} + +// helper function for parsed URL query strings +func firstValueOrEmpty(x []string) string { + if len(x) > 0 { + return x[0] + } + return "" +} + +// helper function to build tcpip address +func tcpipAddress(nicID tcpip.NICID, remote string) (address tcpip.FullAddress, err error) { + + // build the address manual way + split := strings.Split(remote, ":") + if len(split) != 2 { + return address, errors.New("invalid remote addr") + } + + port, err := strconv.Atoi(split[1]) + if err != nil { + return address, err + + } + + address = tcpip.FullAddress{ + NIC: nicID, + Addr: tcpip.AddrFrom4Slice(net.ParseIP(split[0]).To4()), + Port: uint16(port), + } + + return address, err +} diff --git a/pkg/services/forwarder/tcp.go b/pkg/services/forwarder/tcp.go new file mode 100644 index 0000000..e60936e --- /dev/null +++ b/pkg/services/forwarder/tcp.go @@ -0,0 +1,62 @@ +package forwarder + +import ( + "context" + "fmt" + "net" + "sync" + + log "github.com/sirupsen/logrus" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/waiter" + "inet.af/tcpproxy" +) + +const linkLocalSubnet = "169.254.0.0/16" + +func TCP(s *stack.Stack, nat map[tcpip.Address]tcpip.Address, natLock *sync.Mutex) *tcp.Forwarder { + return tcp.NewForwarder(s, 0, 10, func(r *tcp.ForwarderRequest) { + localAddress := r.ID().LocalAddress + + if linkLocal().Contains(localAddress) { + r.Complete(true) + return + } + + natLock.Lock() + if replaced, ok := nat[localAddress]; ok { + localAddress = replaced + } + natLock.Unlock() + outbound, err := net.Dial("tcp", fmt.Sprintf("%s:%d", localAddress, r.ID().LocalPort)) + if err != nil { + log.Tracef("net.Dial() = %v", err) + r.Complete(true) + return + } + + var wq waiter.Queue + ep, tcpErr := r.CreateEndpoint(&wq) + r.Complete(false) + if tcpErr != nil { + log.Errorf("r.CreateEndpoint() = %v", tcpErr) + return + } + + remote := tcpproxy.DialProxy{ + DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return outbound, nil + }, + } + remote.HandleConn(gonet.NewTCPConn(&wq, ep)) + }) +} + +func linkLocal() *tcpip.Subnet { + _, parsedSubnet, _ := net.ParseCIDR(linkLocalSubnet) // CoreOS VM tries to connect to Amazon EC2 metadata service + subnet, _ := tcpip.NewSubnet(tcpip.AddrFromSlice(parsedSubnet.IP), tcpip.MaskFromBytes(parsedSubnet.Mask)) + return &subnet +} diff --git a/pkg/services/forwarder/udp.go b/pkg/services/forwarder/udp.go new file mode 100644 index 0000000..3226b14 --- /dev/null +++ b/pkg/services/forwarder/udp.go @@ -0,0 +1,43 @@ +package forwarder + +import ( + "fmt" + "net" + "sync" + + log "github.com/sirupsen/logrus" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" + "gvisor.dev/gvisor/pkg/waiter" +) + +func UDP(s *stack.Stack, nat map[tcpip.Address]tcpip.Address, natLock *sync.Mutex) *udp.Forwarder { + return udp.NewForwarder(s, func(r *udp.ForwarderRequest) { + localAddress := r.ID().LocalAddress + + if linkLocal().Contains(localAddress) || localAddress == header.IPv4Broadcast { + return + } + + natLock.Lock() + if replaced, ok := nat[localAddress]; ok { + localAddress = replaced + } + natLock.Unlock() + + var wq waiter.Queue + ep, tcpErr := r.CreateEndpoint(&wq) + if tcpErr != nil { + log.Errorf("r.CreateEndpoint() = %v", tcpErr) + return + } + + p, _ := NewUDPProxy(&autoStoppingListener{underlying: gonet.NewUDPConn(s, &wq, ep)}, func() (net.Conn, error) { + return net.Dial("udp", fmt.Sprintf("%s:%d", localAddress, r.ID().LocalPort)) + }) + go p.Run() + }) +} diff --git a/pkg/services/forwarder/udp_proxy.go b/pkg/services/forwarder/udp_proxy.go new file mode 100644 index 0000000..fbb0029 --- /dev/null +++ b/pkg/services/forwarder/udp_proxy.go @@ -0,0 +1,192 @@ +package forwarder + +// Modified version of https://github.com/moby/moby/blob/master/cmd/docker-proxy/udp_proxy.go and +// https://github.com/moby/vpnkit/blob/master/go/pkg/libproxy/udp_proxy.go + +import ( + "encoding/binary" + "io" + "net" + "strings" + "sync" + "syscall" + "time" + + log "github.com/sirupsen/logrus" +) + +const ( + // UDPConnTrackTimeout is the timeout used for UDP connection tracking + UDPConnTrackTimeout = 90 * time.Second + // UDPBufSize is the buffer size for the UDP proxy + UDPBufSize = 65507 +) + +// A net.Addr where the IP is split into two fields so you can use it as a key +// in a map: +type connTrackKey struct { + IPHigh uint64 + IPLow uint64 + Port int +} + +func newConnTrackKey(addr *net.UDPAddr) *connTrackKey { + if len(addr.IP) == net.IPv4len { + return &connTrackKey{ + IPHigh: 0, + IPLow: uint64(binary.BigEndian.Uint32(addr.IP)), + Port: addr.Port, + } + } + return &connTrackKey{ + IPHigh: binary.BigEndian.Uint64(addr.IP[:8]), + IPLow: binary.BigEndian.Uint64(addr.IP[8:]), + Port: addr.Port, + } +} + +type connTrackMap map[connTrackKey]net.Conn + +// UDPProxy is proxy for which handles UDP datagrams. It implements the Proxy +// interface to handle UDP traffic forwarding between the frontend and backend +// addresses. +type UDPProxy struct { + listener udpConn + dialer func() (net.Conn, error) + connTrackTable connTrackMap + connTrackLock sync.Mutex +} + +// NewUDPProxy creates a new UDPProxy. +func NewUDPProxy(listener udpConn, dialer func() (net.Conn, error)) (*UDPProxy, error) { + return &UDPProxy{ + listener: listener, + connTrackTable: make(connTrackMap), + dialer: dialer, + }, nil +} + +func (proxy *UDPProxy) replyLoop(proxyConn net.Conn, clientAddr net.Addr, clientKey *connTrackKey) { + defer func() { + proxy.connTrackLock.Lock() + delete(proxy.connTrackTable, *clientKey) + proxy.connTrackLock.Unlock() + proxyConn.Close() + }() + + readBuf := make([]byte, UDPBufSize) + for { + _ = proxyConn.SetReadDeadline(time.Now().Add(UDPConnTrackTimeout)) + again: + read, err := proxyConn.Read(readBuf) + if err != nil { + if err, ok := err.(*net.OpError); ok && err.Err == syscall.ECONNREFUSED { + // This will happen if the last write failed + // (e.g: nothing is actually listening on the + // proxied port on the container), ignore it + // and continue until UDPConnTrackTimeout + // expires: + goto again + } + return + } + for i := 0; i != read; { + written, err := proxy.listener.WriteTo(readBuf[i:read], clientAddr) + if err != nil { + return + } + i += written + } + } +} + +// Run starts forwarding the traffic using UDP. +func (proxy *UDPProxy) Run() { + readBuf := make([]byte, UDPBufSize) + for { + read, from, err := proxy.listener.ReadFrom(readBuf) + if err != nil { + // NOTE: Apparently ReadFrom doesn't return + // ECONNREFUSED like Read do (see comment in + // UDPProxy.replyLoop) + if !isClosedError(err) { + log.Debugf("Stopping udp proxy (%s)", err) + } + break + } + + fromKey := newConnTrackKey(from.(*net.UDPAddr)) + proxy.connTrackLock.Lock() + proxyConn, hit := proxy.connTrackTable[*fromKey] + if !hit { + proxyConn, err = proxy.dialer() + if err != nil { + log.Errorf("Can't proxy a datagram to udp: %s\n", err) + proxy.connTrackLock.Unlock() + continue + } + proxy.connTrackTable[*fromKey] = proxyConn + go proxy.replyLoop(proxyConn, from, fromKey) + } + proxy.connTrackLock.Unlock() + for i := 0; i != read; { + _ = proxyConn.SetReadDeadline(time.Now().Add(UDPConnTrackTimeout)) + written, err := proxyConn.Write(readBuf[i:read]) + if err != nil { + log.Errorf("Can't proxy a datagram to udp: %s\n", err) + break + } + i += written + } + } +} + +// Close stops forwarding the traffic. +func (proxy *UDPProxy) Close() error { + proxy.listener.Close() + proxy.connTrackLock.Lock() + defer proxy.connTrackLock.Unlock() + for _, conn := range proxy.connTrackTable { + conn.Close() + } + return nil +} + +func isClosedError(err error) bool { + /* This comparison is ugly, but unfortunately, net.go doesn't export errClosing. + * See: + * http://golang.org/src/pkg/net/net.go + * https://code.google.com/p/go/issues/detail?id=4337 + * https://groups.google.com/forum/#!msg/golang-nuts/0_aaCvBmOcM/SptmDyX1XJMJ + */ + return strings.HasSuffix(err.Error(), "use of closed network connection") +} + +type udpConn interface { + ReadFrom(b []byte) (int, net.Addr, error) + WriteTo(b []byte, addr net.Addr) (int, error) + SetReadDeadline(t time.Time) error + io.Closer +} + +type autoStoppingListener struct { + underlying udpConn +} + +func (l *autoStoppingListener) ReadFrom(b []byte) (int, net.Addr, error) { + _ = l.underlying.SetReadDeadline(time.Now().Add(UDPConnTrackTimeout)) + return l.underlying.ReadFrom(b) +} + +func (l *autoStoppingListener) WriteTo(b []byte, addr net.Addr) (int, error) { + _ = l.underlying.SetReadDeadline(time.Now().Add(UDPConnTrackTimeout)) + return l.underlying.WriteTo(b, addr) +} + +func (l *autoStoppingListener) SetReadDeadline(t time.Time) error { + return l.underlying.SetReadDeadline(t) +} + +func (l *autoStoppingListener) Close() error { + return l.underlying.Close() +} diff --git a/pkg/sshclient/bastion.go b/pkg/sshclient/bastion.go new file mode 100644 index 0000000..f10bddd --- /dev/null +++ b/pkg/sshclient/bastion.go @@ -0,0 +1,199 @@ +package sshclient + +import ( + "bufio" + "context" + "fmt" + "net" + "net/url" + "os" + "os/user" + "path/filepath" + "strconv" + "sync" + "time" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/knownhosts" +) + +var ( + homedirOnce sync.Once + homedir string +) + +// Modified version of podman ssh client library, until a shared module exists + +type Bastion struct { + Client *ssh.Client + Config *ssh.ClientConfig + Host string + Port string + Path string + connect ConnectCallback +} + +type ConnectCallback func(ctx context.Context, bastion *Bastion) (net.Conn, error) + +func PublicKey(path string, passphrase []byte) (ssh.Signer, error) { + key, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + signer, err := ssh.ParsePrivateKey(key) + if err != nil { + if _, ok := err.(*ssh.PassphraseMissingError); !ok { + return nil, err + } + return ssh.ParsePrivateKeyWithPassphrase(key, passphrase) + } + return signer, nil +} + +func HostKey(host string) ssh.PublicKey { + // parse OpenSSH known_hosts file + // ssh or use ssh-keyscan to get initial key + knownHosts := filepath.Join(getHome(), ".ssh", "known_hosts") + fd, err := os.Open(knownHosts) + if err != nil { + logrus.Error(err) + return nil + } + + // support -H parameter for ssh-keyscan + hashhost := knownhosts.HashHostname(host) + + scanner := bufio.NewScanner(fd) + for scanner.Scan() { + _, hosts, key, _, _, err := ssh.ParseKnownHosts(scanner.Bytes()) + if err != nil { + logrus.Errorf("Failed to parse known_hosts: %s", scanner.Text()) + continue + } + + for _, h := range hosts { + if h == host || h == hashhost { + return key + } + } + } + + return nil +} + +func CreateBastion(_url *url.URL, passPhrase string, identity string, initial net.Conn, connect ConnectCallback) (*Bastion, error) { + var authMethods []ssh.AuthMethod + + if len(identity) > 0 { + s, err := PublicKey(identity, []byte(passPhrase)) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse identity %q", identity) + } + authMethods = append(authMethods, ssh.PublicKeys(s)) + } + + if pw, found := _url.User.Password(); found { + authMethods = append(authMethods, ssh.Password(pw)) + } + + if len(authMethods) == 0 { + return nil, errors.New("No available auth methods") + } + + port := _url.Port() + if port == "" { + port = "22" + } + + secure, _ := strconv.ParseBool(_url.Query().Get("secure")) + + callback := ssh.InsecureIgnoreHostKey() // #nosec + if secure { + host := _url.Hostname() + if port != "22" { + host = fmt.Sprintf("[%s]:%s", host, port) + } + key := HostKey(host) + if key != nil { + callback = ssh.FixedHostKey(key) + } + } + + config := &ssh.ClientConfig{ + User: _url.User.Username(), + Auth: authMethods, + HostKeyCallback: callback, + HostKeyAlgorithms: []string{ + ssh.KeyAlgoRSA, + ssh.KeyAlgoDSA, + ssh.KeyAlgoECDSA256, + ssh.KeyAlgoECDSA384, + ssh.KeyAlgoECDSA521, + ssh.KeyAlgoED25519, + }, + Timeout: 5 * time.Second, + } + + if connect == nil { + connect = func(ctx context.Context, bastion *Bastion) (net.Conn, error) { + conn, err := net.DialTimeout("tcp", + net.JoinHostPort(bastion.Host, bastion.Port), + bastion.Config.Timeout, + ) + + return conn, err + } + } + + bastion := Bastion{nil, config, _url.Hostname(), port, _url.Path, connect} + return &bastion, bastion.reconnect(context.Background(), initial) +} + +func (bastion *Bastion) Reconnect(ctx context.Context) error { + return bastion.reconnect(ctx, nil) +} + +func (bastion *Bastion) Close() { + if bastion.Client != nil { + bastion.Client.Close() + } +} + +func (bastion *Bastion) reconnect(ctx context.Context, conn net.Conn) error { + var err error + if conn == nil { + conn, err = bastion.connect(ctx, bastion) + } + if err != nil { + return errors.Wrapf(err, "Connection to bastion host (%s) failed", bastion.Host) + } + addr := net.JoinHostPort(bastion.Host, bastion.Port) + c, chans, reqs, err := ssh.NewClientConn(conn, addr, bastion.Config) + if err != nil { + return err + } + bastion.Client = ssh.NewClient(c, chans, reqs) + return nil +} + +func getHome() string { + homedirOnce.Do(func() { + env, err := os.UserHomeDir() + if env == "" || err != nil { + usr, err := user.LookupId(fmt.Sprintf("%d", os.Getuid())) + if err != nil { + logrus.Error("Could not determine user home directory!") + homedir = "" + return + } + + homedir = usr.HomeDir + return + } + homedir = env + }) + return homedir +} diff --git a/pkg/sshclient/npipe_unsupported.go b/pkg/sshclient/npipe_unsupported.go new file mode 100644 index 0000000..1cb4ee7 --- /dev/null +++ b/pkg/sshclient/npipe_unsupported.go @@ -0,0 +1,14 @@ +//go:build !windows +// +build !windows + +package sshclient + +import ( + "errors" + "net" + "net/url" +) + +func ListenNpipe(_ *url.URL) (net.Listener, error) { + return nil, errors.New("named pipes are not supported by this platform") +} diff --git a/pkg/sshclient/npipe_windows.go b/pkg/sshclient/npipe_windows.go new file mode 100644 index 0000000..ac83170 --- /dev/null +++ b/pkg/sshclient/npipe_windows.go @@ -0,0 +1,43 @@ +package sshclient + +import ( + "fmt" + "net" + "net/url" + "os/user" + "strings" + + winio "github.com/Microsoft/go-winio" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/sddl-for-device-objects +// Allow built-in admins and system/kernel components +const SddlDevObjSysAllAdmAll = "D:P(A;;GA;;;SY)(A;;GA;;;BA)" + +func ListenNpipe(socketURI *url.URL) (net.Listener, error) { + user, err := user.Current() + if err != nil { + return nil, err + } + + // Also allow current user + sddl := fmt.Sprintf("%s(A;;GA;;;%s)", SddlDevObjSysAllAdmAll, user.Uid) + config := winio.PipeConfig{ + SecurityDescriptor: sddl, + MessageMode: true, + InputBufferSize: 65536, + OutputBufferSize: 65536, + } + path := strings.Replace(socketURI.Path, "/", "\\", -1) + + listener, err := winio.ListenPipe(path, &config) + if err != nil { + return listener, errors.Wrapf(err, "Error listening on socket: %s", socketURI) + } + + logrus.Info("Listening on: " + path) + + return listener, nil +} diff --git a/pkg/sshclient/ssh_forwarder.go b/pkg/sshclient/ssh_forwarder.go new file mode 100644 index 0000000..b994f24 --- /dev/null +++ b/pkg/sshclient/ssh_forwarder.go @@ -0,0 +1,279 @@ +package sshclient + +import ( + "context" + "fmt" + "io" + "net" + "net/url" + "os" + "runtime" + "strings" + "sync" + "time" + + "github.com/containers/gvisor-tap-vsock/pkg/fs" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +type CloseWriteStream interface { + io.Reader + io.WriteCloser + CloseWrite() error +} + +type CloseWriteConn interface { + net.Conn + CloseWriteStream +} + +type SSHForward struct { + listener net.Listener + bastion *Bastion + sock *url.URL +} + +type SSHDialer interface { + DialContextTCP(ctx context.Context, addr string) (net.Conn, error) +} + +type genericTCPDialer struct { +} + +var defaultTCPDialer genericTCPDialer + +func (dialer *genericTCPDialer) DialContextTCP(ctx context.Context, addr string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, "tcp", addr) +} + +func CreateSSHForward(ctx context.Context, src *url.URL, dest *url.URL, identity string, dialer SSHDialer) (*SSHForward, error) { + if dialer == nil { + dialer = &defaultTCPDialer + } + + return setupProxy(ctx, src, dest, identity, "", dialer) +} + +func CreateSSHForwardPassphrase(ctx context.Context, src *url.URL, dest *url.URL, identity string, passphrase string, dialer SSHDialer) (*SSHForward, error) { + if dialer == nil { + dialer = &defaultTCPDialer + } + + return setupProxy(ctx, src, dest, identity, passphrase, dialer) +} + +func (forward *SSHForward) AcceptAndTunnel(ctx context.Context) error { + return acceptConnection(ctx, forward.listener, forward.bastion, forward.sock) +} + +func (forward *SSHForward) Tunnel(ctx context.Context) (CloseWriteConn, error) { + return connectForward(ctx, forward.bastion) +} + +func (forward *SSHForward) Close() { + if forward.listener != nil { + forward.listener.Close() + } + if forward.bastion != nil { + forward.bastion.Close() + } +} + +func connectForward(ctx context.Context, bastion *Bastion) (CloseWriteConn, error) { + for retries := 1; ; retries++ { + forward, err := bastion.Client.Dial("unix", bastion.Path) + if err == nil { + return forward.(CloseWriteConn), nil + } + if retries > 2 { + return nil, errors.Wrapf(err, "Couldn't reestablish ssh tunnel on path: %s", bastion.Path) + } + // Check if ssh connection is still alive + _, _, err = bastion.Client.Conn.SendRequest("alive@gvproxy", true, nil) + if err != nil { + for bastionRetries := 1; ; bastionRetries++ { + err = bastion.Reconnect(ctx) + if err == nil { + break + } + if bastionRetries > 2 || !sleep(ctx, 200*time.Millisecond) { + return nil, errors.Wrapf(err, "Couldn't reestablish ssh connection: %s", bastion.Host) + } + } + } + + if !sleep(ctx, 200*time.Millisecond) { + retries = 3 + } + } +} + +func listenUnix(socketURI *url.URL) (net.Listener, error) { + path := socketURI.Path + if runtime.GOOS == "windows" { + path = strings.TrimPrefix(path, "/") + } + + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + return nil, err + } + + oldmask := fs.Umask(0177) + defer fs.Umask(oldmask) + listener, err := net.Listen("unix", path) + if err != nil { + return listener, errors.Wrapf(err, "Error listening on socket: %s", socketURI.Path) + } + + return listener, nil +} + +func setupProxy(ctx context.Context, socketURI *url.URL, dest *url.URL, identity string, passphrase string, dialer SSHDialer) (*SSHForward, error) { + var ( + listener net.Listener + err error + ) + switch socketURI.Scheme { + case "unix": + listener, err = listenUnix(socketURI) + if err != nil { + return &SSHForward{}, err + } + case "npipe": + listener, err = ListenNpipe(socketURI) + if err != nil { + return &SSHForward{}, err + } + case "": + // empty URL = Tunnel Only, no Accept + default: + return &SSHForward{}, errors.Errorf("URI scheme not supported: %s", socketURI.Scheme) + } + + connectFunc := func(ctx context.Context, bastion *Bastion) (net.Conn, error) { + timeout := 5 * time.Second + if bastion != nil { + timeout = bastion.Config.Timeout + } + ctx, cancel := context.WithTimeout(ctx, timeout) + conn, err := dialer.DialContextTCP(ctx, dest.Host) + if cancel != nil { + cancel() + } + + return conn, err + } + + createBastion := func() (*Bastion, error) { + conn, err := connectFunc(ctx, nil) + if err != nil { + return nil, err + } + return CreateBastion(dest, passphrase, identity, conn, connectFunc) + } + bastion, err := retry(ctx, createBastion, "Waiting for sshd") + if err != nil { + return &SSHForward{}, fmt.Errorf("setupProxy failed: %w", err) + } + + logrus.Debugf("Socket forward established: %s -> %s\n", socketURI.Path, dest.Path) + + return &SSHForward{listener, bastion, socketURI}, nil +} + +const maxRetries = 60 +const initialBackoff = 100 * time.Millisecond + +func retry[T comparable](ctx context.Context, retryFunc func() (T, error), retryMsg string) (T, error) { + var ( + returnVal T + err error + ) + + backoff := initialBackoff + +loop: + for i := 0; i < maxRetries; i++ { + select { + case <-ctx.Done(): + break loop + default: + // proceed + } + + returnVal, err = retryFunc() + if err == nil { + return returnVal, nil + } + logrus.Debugf("%s (%s)", retryMsg, backoff) + sleep(ctx, backoff) + backoff = backOff(backoff) + } + return returnVal, fmt.Errorf("timeout: %w", err) +} + +func acceptConnection(ctx context.Context, listener net.Listener, bastion *Bastion, socketURI *url.URL) error { + con, err := listener.Accept() + if err != nil { + return errors.Wrapf(err, "Error accepting on socket: %s", socketURI.Path) + } + + src, ok := con.(CloseWriteStream) + if !ok { + con.Close() + return errors.Wrapf(err, "Underlying socket does not support half-close %s", socketURI.Path) + } + + var dest CloseWriteStream + + dest, err = connectForward(ctx, bastion) + if err != nil { + con.Close() + logrus.Error(err) + return nil // eat + } + + complete := new(sync.WaitGroup) + complete.Add(2) + go forward(src, dest, complete) + go forward(dest, src, complete) + + go func() { + complete.Wait() + src.Close() + dest.Close() + }() + + return nil +} + +func forward(src io.ReadCloser, dest CloseWriteStream, complete *sync.WaitGroup) { + defer complete.Done() + _, _ = io.Copy(dest, src) + + // Trigger an EOF on the other end + _ = dest.CloseWrite() +} + +func backOff(delay time.Duration) time.Duration { + if delay == 0 { + delay = 5 * time.Millisecond + } else { + delay *= 2 + } + if delay > time.Second { + delay = time.Second + } + return delay +} + +func sleep(ctx context.Context, wait time.Duration) bool { + select { + case <-ctx.Done(): + return false + case <-time.After(wait): + return true + } +} diff --git a/pkg/tap/connection.go b/pkg/tap/connection.go new file mode 100644 index 0000000..1ab6e87 --- /dev/null +++ b/pkg/tap/connection.go @@ -0,0 +1,10 @@ +package tap + +import ( + "net" +) + +type protocolConn struct { + net.Conn + protocolImpl protocol +} diff --git a/pkg/tap/ip_pool.go b/pkg/tap/ip_pool.go new file mode 100644 index 0000000..87e470c --- /dev/null +++ b/pkg/tap/ip_pool.go @@ -0,0 +1,86 @@ +package tap + +import ( + "errors" + "net" + "sync" + + "github.com/apparentlymart/go-cidr/cidr" +) + +type IPPool struct { + base *net.IPNet + count uint64 + leases map[string]string + lock sync.Mutex +} + +func NewIPPool(base *net.IPNet) *IPPool { + return &IPPool{ + base: base, + count: cidr.AddressCount(base), + leases: make(map[string]string), + } +} + +func (p *IPPool) Leases() map[string]string { + p.lock.Lock() + defer p.lock.Unlock() + leases := map[string]string{} + for key, value := range p.leases { + leases[key] = value + } + return leases +} + +func (p *IPPool) Mask() int { + ones, _ := p.base.Mask.Size() + return ones +} + +func (p *IPPool) GetOrAssign(mac string) (net.IP, error) { + p.lock.Lock() + defer p.lock.Unlock() + + for ip, candidate := range p.leases { + if candidate == mac { + return net.ParseIP(ip), nil + } + } + + var i uint64 + for i = 1; i < p.count; i++ { + candidate, err := cidr.Host(p.base, int(i)) + if err != nil { + continue + } + if _, ok := p.leases[candidate.String()]; !ok { + p.leases[candidate.String()] = mac + return candidate, nil + } + } + return nil, errors.New("cannot find available IP") +} + +func (p *IPPool) Reserve(ip net.IP, mac string) { + p.lock.Lock() + defer p.lock.Unlock() + + p.leases[ip.String()] = mac +} + +func (p *IPPool) Release(given string) { + p.lock.Lock() + defer p.lock.Unlock() + + var found string + for ip, mac := range p.leases { + if mac == given { + found = ip + break + } + } + if found != "" { + delete(p.leases, found) + } +} diff --git a/pkg/tap/ip_pool_test.go b/pkg/tap/ip_pool_test.go new file mode 100644 index 0000000..1f40098 --- /dev/null +++ b/pkg/tap/ip_pool_test.go @@ -0,0 +1,41 @@ +package tap + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIPPool(t *testing.T) { + _, network, _ := net.ParseCIDR("10.0.0.0/8") + pool := NewIPPool(network) + + ip1, err := pool.GetOrAssign("mac1") + assert.NoError(t, err) + assert.Equal(t, "10.0.0.1", ip1.String()) + + ip1, err = pool.GetOrAssign("mac1") + assert.NoError(t, err) + assert.Equal(t, "10.0.0.1", ip1.String()) + + ip2, err := pool.GetOrAssign("mac2") + assert.NoError(t, err) + assert.Equal(t, "10.0.0.2", ip2.String()) + + assert.Equal(t, map[string]string{"10.0.0.1": "mac1", "10.0.0.2": "mac2"}, pool.Leases()) + + pool.Release("mac1") + + assert.Equal(t, map[string]string{"10.0.0.2": "mac2"}, pool.Leases()) + + ip3, err := pool.GetOrAssign("mac3") + assert.NoError(t, err) + assert.Equal(t, "10.0.0.1", ip3.String()) + + ip4, err := pool.GetOrAssign("mac4") + assert.NoError(t, err) + assert.Equal(t, "10.0.0.3", ip4.String()) + + assert.Equal(t, map[string]string{"10.0.0.1": "mac3", "10.0.0.2": "mac2", "10.0.0.3": "mac4"}, pool.Leases()) +} diff --git a/pkg/tap/link.go b/pkg/tap/link.go new file mode 100644 index 0000000..b85ce43 --- /dev/null +++ b/pkg/tap/link.go @@ -0,0 +1,137 @@ +package tap + +import ( + "net" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + log "github.com/sirupsen/logrus" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +type LinkEndpoint struct { + debug bool + mtu int + mac tcpip.LinkAddress + ip string + virtualIPs map[string]struct{} + + dispatcher stack.NetworkDispatcher + networkSwitch NetworkSwitch +} + +func NewLinkEndpoint(debug bool, mtu int, macAddress string, ip string, virtualIPs []string) (*LinkEndpoint, error) { + linkAddr, err := net.ParseMAC(macAddress) + if err != nil { + return nil, err + } + set := make(map[string]struct{}) + for _, virtualIP := range virtualIPs { + set[virtualIP] = struct{}{} + } + return &LinkEndpoint{ + debug: debug, + mtu: mtu, + mac: tcpip.LinkAddress(linkAddr), + ip: ip, + virtualIPs: set, + }, nil +} + +func (e *LinkEndpoint) ARPHardwareType() header.ARPHardwareType { + return header.ARPHardwareEther +} + +func (e *LinkEndpoint) Connect(networkSwitch NetworkSwitch) { + e.networkSwitch = networkSwitch +} + +func (e *LinkEndpoint) Attach(dispatcher stack.NetworkDispatcher) { + e.dispatcher = dispatcher +} + +func (e *LinkEndpoint) IsAttached() bool { + return e.dispatcher != nil +} + +func (e *LinkEndpoint) DeliverNetworkPacket(protocol tcpip.NetworkProtocolNumber, pkt stack.PacketBufferPtr) { + e.dispatcher.DeliverNetworkPacket(protocol, pkt) +} + +func (e *LinkEndpoint) AddHeader(_ stack.PacketBufferPtr) { +} + +func (e *LinkEndpoint) ParseHeader(stack.PacketBufferPtr) bool { return true } + +func (e *LinkEndpoint) Capabilities() stack.LinkEndpointCapabilities { + return stack.CapabilityResolutionRequired | stack.CapabilityRXChecksumOffload +} + +func (e *LinkEndpoint) LinkAddress() tcpip.LinkAddress { + return e.mac +} + +func (e *LinkEndpoint) MaxHeaderLength() uint16 { + return uint16(header.EthernetMinimumSize) +} + +func (e *LinkEndpoint) MTU() uint32 { + return uint32(e.mtu) +} + +func (e *LinkEndpoint) Wait() { +} + +func (e *LinkEndpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error) { + n := 0 + for _, p := range pkts.AsSlice() { + if err := e.writePacket(p.EgressRoute, p.NetworkProtocolNumber, p); err != nil { + return n, err + } + n++ + } + return n, nil +} + +func (e *LinkEndpoint) writePacket(r stack.RouteInfo, protocol tcpip.NetworkProtocolNumber, pkt stack.PacketBufferPtr) tcpip.Error { + // Preserve the src address if it's set in the route. + srcAddr := e.LinkAddress() + if r.LocalLinkAddress != "" { + srcAddr = r.LocalLinkAddress + } + eth := header.Ethernet(pkt.LinkHeader().Push(header.EthernetMinimumSize)) + eth.Encode(&header.EthernetFields{ + Type: protocol, + SrcAddr: srcAddr, + DstAddr: r.RemoteLinkAddress, + }) + + h := header.ARP(pkt.NetworkHeader().Slice()) + if h.IsValid() && + h.Op() == header.ARPReply { + ip := tcpip.AddrFromSlice(h.ProtocolAddressSender()).String() + _, ok := e.virtualIPs[ip] + if ip != e.IP() && !ok { + log.Debugf("dropping spoofing packets from the gateway about IP %s", ip) + return nil + } + } + + if e.debug { + packet := gopacket.NewPacket(pkt.ToView().AsSlice(), layers.LayerTypeEthernet, gopacket.Default) + log.Info(packet.String()) + } + + e.networkSwitch.DeliverNetworkPacket(protocol, pkt) + return nil +} + +func (e *LinkEndpoint) WriteRawPacket(_ stack.PacketBufferPtr) tcpip.Error { + return &tcpip.ErrNotSupported{} +} + +func (e *LinkEndpoint) IP() string { + return e.ip +} diff --git a/pkg/tap/protocols.go b/pkg/tap/protocols.go new file mode 100644 index 0000000..6402abf --- /dev/null +++ b/pkg/tap/protocols.go @@ -0,0 +1,68 @@ +package tap + +import ( + "encoding/binary" +) + +type protocol interface { + Stream() bool +} + +type streamProtocol interface { + protocol + Buf() []byte + Write(buf []byte, size int) + Read(buf []byte) int +} + +type hyperkitProtocol struct { +} + +func (s *hyperkitProtocol) Stream() bool { + return true +} + +func (s *hyperkitProtocol) Buf() []byte { + return make([]byte, 2) +} + +func (s *hyperkitProtocol) Write(buf []byte, size int) { + binary.LittleEndian.PutUint16(buf, uint16(size)) +} + +func (s *hyperkitProtocol) Read(buf []byte) int { + return int(binary.LittleEndian.Uint16(buf[0:2])) +} + +type qemuProtocol struct { +} + +func (s *qemuProtocol) Stream() bool { + return true +} + +func (s *qemuProtocol) Buf() []byte { + return make([]byte, 4) +} + +func (s *qemuProtocol) Write(buf []byte, size int) { + binary.BigEndian.PutUint32(buf, uint32(size)) +} + +func (s *qemuProtocol) Read(buf []byte) int { + return int(binary.BigEndian.Uint32(buf[0:4])) +} + +type bessProtocol struct { +} + +func (s *bessProtocol) Stream() bool { + return false +} + +type vfkitProtocol struct { +} + +func (s *vfkitProtocol) Stream() bool { + return false +} diff --git a/pkg/tap/switch.go b/pkg/tap/switch.go new file mode 100644 index 0000000..10fe59e --- /dev/null +++ b/pkg/tap/switch.go @@ -0,0 +1,296 @@ +package tap + +import ( + "bufio" + "context" + "io" + "net" + "sync" + "sync/atomic" + + "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "gvisor.dev/gvisor/pkg/buffer" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +type VirtualDevice interface { + DeliverNetworkPacket(protocol tcpip.NetworkProtocolNumber, pkt stack.PacketBufferPtr) + LinkAddress() tcpip.LinkAddress + IP() string +} + +type NetworkSwitch interface { + DeliverNetworkPacket(protocol tcpip.NetworkProtocolNumber, pkt stack.PacketBufferPtr) +} + +type Switch struct { + Sent uint64 + Received uint64 + + debug bool + maxTransmissionUnit int + + nextConnID int + conns map[int]protocolConn + connLock sync.Mutex + + cam map[tcpip.LinkAddress]int + camLock sync.RWMutex + + writeLock sync.Mutex + + gateway VirtualDevice +} + +func NewSwitch(debug bool, mtu int) *Switch { + return &Switch{ + debug: debug, + maxTransmissionUnit: mtu, + conns: make(map[int]protocolConn), + cam: make(map[tcpip.LinkAddress]int), + } +} + +func (e *Switch) CAM() map[string]int { + e.camLock.RLock() + defer e.camLock.RUnlock() + ret := make(map[string]int) + for address, port := range e.cam { + ret[address.String()] = port + } + return ret +} + +func (e *Switch) Connect(ep VirtualDevice) { + e.gateway = ep +} + +func (e *Switch) DeliverNetworkPacket(_ tcpip.NetworkProtocolNumber, pkt stack.PacketBufferPtr) { + if err := e.tx(pkt); err != nil { + log.Error(err) + } +} + +func (e *Switch) Accept(ctx context.Context, rawConn net.Conn, protocol types.Protocol) error { + conn := protocolConn{Conn: rawConn, protocolImpl: protocolImplementation(protocol)} + log.Infof("new connection from %s to %s", conn.RemoteAddr().String(), conn.LocalAddr().String()) + id, failed := e.connect(conn) + if failed { + log.Error("connection failed") + return conn.Close() + + } + + defer func() { + e.connLock.Lock() + defer e.connLock.Unlock() + e.disconnect(id, conn) + }() + if err := e.rx(ctx, id, conn); err != nil { + log.Error(errors.Wrapf(err, "cannot receive packets from %s, disconnecting", conn.RemoteAddr().String())) + return err + } + return nil +} + +func (e *Switch) connect(conn protocolConn) (int, bool) { + e.connLock.Lock() + defer e.connLock.Unlock() + + id := e.nextConnID + e.nextConnID++ + + e.conns[id] = conn + return id, false +} + +func (e *Switch) tx(pkt stack.PacketBufferPtr) error { + return e.txPkt(pkt) +} + +func (e *Switch) txPkt(pkt stack.PacketBufferPtr) error { + e.writeLock.Lock() + defer e.writeLock.Unlock() + + e.connLock.Lock() + defer e.connLock.Unlock() + + buf := pkt.ToView().AsSlice() + eth := header.Ethernet(buf) + dst := eth.DestinationAddress() + src := eth.SourceAddress() + + if dst == header.EthernetBroadcastAddress { + e.camLock.RLock() + srcID, ok := e.cam[src] + if !ok { + srcID = -1 + } + e.camLock.RUnlock() + for id, conn := range e.conns { + if id == srcID { + continue + } + + err := e.txBuf(id, conn, buf) + if err != nil { + return err + } + + atomic.AddUint64(&e.Sent, uint64(pkt.Size())) + } + } else { + e.camLock.RLock() + id, ok := e.cam[dst] + if !ok { + e.camLock.RUnlock() + return nil + } + e.camLock.RUnlock() + conn := e.conns[id] + err := e.txBuf(id, conn, buf) + if err != nil { + return err + } + atomic.AddUint64(&e.Sent, uint64(pkt.Size())) + } + return nil +} + +func (e *Switch) txBuf(id int, conn protocolConn, buf []byte) error { + if conn.protocolImpl.Stream() { + size := conn.protocolImpl.(streamProtocol).Buf() + conn.protocolImpl.(streamProtocol).Write(size, len(buf)) + + if _, err := conn.Write(append(size, buf...)); err != nil { + e.disconnect(id, conn) + return err + } + } else { + if _, err := conn.Write(buf); err != nil { + e.disconnect(id, conn) + return err + } + } + return nil +} + +func (e *Switch) disconnect(id int, conn net.Conn) { + e.camLock.Lock() + defer e.camLock.Unlock() + + for address, targetConn := range e.cam { + if targetConn == id { + delete(e.cam, address) + } + } + _ = conn.Close() + delete(e.conns, id) +} + +func (e *Switch) rx(ctx context.Context, id int, conn protocolConn) error { + if conn.protocolImpl.Stream() { + return e.rxStream(ctx, id, conn, conn.protocolImpl.(streamProtocol)) + } + return e.rxNonStream(ctx, id, conn) +} + +func (e *Switch) rxNonStream(ctx context.Context, id int, conn net.Conn) error { + bufSize := 1024 * 128 + buf := make([]byte, bufSize) +loop: + for { + select { + case <-ctx.Done(): + break loop + default: + // passthrough + } + n, err := conn.Read(buf) + if err != nil { + return errors.Wrap(err, "cannot read size from socket") + } + e.rxBuf(ctx, id, buf[:n]) + } + return nil +} + +func (e *Switch) rxStream(ctx context.Context, id int, conn net.Conn, sProtocol streamProtocol) error { + reader := bufio.NewReader(conn) + sizeBuf := sProtocol.Buf() +loop: + for { + select { + case <-ctx.Done(): + break loop + default: + // passthrough + } + _, err := io.ReadFull(reader, sizeBuf) + if err != nil { + return errors.Wrap(err, "cannot read size from socket") + } + size := sProtocol.Read(sizeBuf) + + buf := make([]byte, size) + _, err = io.ReadFull(reader, buf) + if err != nil { + return errors.Wrap(err, "cannot read packet from socket") + } + e.rxBuf(ctx, id, buf) + } + return nil +} + +func (e *Switch) rxBuf(_ context.Context, id int, buf []byte) { + if e.debug { + packet := gopacket.NewPacket(buf, layers.LayerTypeEthernet, gopacket.Default) + log.Info(packet.String()) + } + + eth := header.Ethernet(buf) + + e.camLock.Lock() + e.cam[eth.SourceAddress()] = id + e.camLock.Unlock() + + if eth.DestinationAddress() != e.gateway.LinkAddress() { + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(buf), + }) + if err := e.tx(pkt); err != nil { + log.Error(err) + } + pkt.DecRef() + } + if eth.DestinationAddress() == e.gateway.LinkAddress() || eth.DestinationAddress() == header.EthernetBroadcastAddress { + data := buffer.MakeWithData(buf) + data.TrimFront(header.EthernetMinimumSize) + pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: data, + }) + e.gateway.DeliverNetworkPacket(eth.Type(), pkt) + pkt.DecRef() + } + + atomic.AddUint64(&e.Received, uint64(len(buf))) +} + +func protocolImplementation(protocol types.Protocol) protocol { + switch protocol { + case types.QemuProtocol: + return &qemuProtocol{} + case types.BessProtocol: + return &bessProtocol{} + case types.VfkitProtocol: + return &vfkitProtocol{} + default: + return &hyperkitProtocol{} + } +} diff --git a/pkg/transport/dial_darwin.go b/pkg/transport/dial_darwin.go new file mode 100644 index 0000000..2556a38 --- /dev/null +++ b/pkg/transport/dial_darwin.go @@ -0,0 +1,11 @@ +package transport + +import ( + "net" + + "github.com/pkg/errors" +) + +func Dial(endpoint string) (net.Conn, string, error) { + return nil, "", errors.New("unsupported") +} diff --git a/pkg/transport/dial_linux.go b/pkg/transport/dial_linux.go new file mode 100644 index 0000000..6030bf0 --- /dev/null +++ b/pkg/transport/dial_linux.go @@ -0,0 +1,46 @@ +package transport + +import ( + "fmt" + "net" + "net/url" + "strconv" + + "github.com/containers/gvisor-tap-vsock/pkg/net/stdio" + mdlayhervsock "github.com/mdlayher/vsock" + "github.com/pkg/errors" +) + +func Dial(endpoint string) (net.Conn, string, error) { + parsed, err := url.Parse(endpoint) + if err != nil { + return nil, "", err + } + switch parsed.Scheme { + case "vsock": + contextID, err := strconv.Atoi(parsed.Hostname()) + if err != nil { + return nil, "", err + } + port, err := strconv.Atoi(parsed.Port()) + if err != nil { + return nil, "", err + } + conn, err := mdlayhervsock.Dial(uint32(contextID), uint32(port), nil) + return conn, parsed.Path, err + case "unix": + conn, err := net.Dial("unix", parsed.Path) + return conn, "/connect", err + case "stdio": + var values []string + for k, vs := range parsed.Query() { + for _, v := range vs { + values = append(values, fmt.Sprintf("-%s=%s", k, v)) + } + } + conn, err := stdio.Dial(parsed.Path, values...) + return conn, "", err + default: + return nil, "", errors.New("unexpected scheme") + } +} diff --git a/pkg/transport/listen.go b/pkg/transport/listen.go new file mode 100644 index 0000000..a0909bb --- /dev/null +++ b/pkg/transport/listen.go @@ -0,0 +1,26 @@ +package transport + +import ( + "errors" + "net" + "net/url" +) + +func defaultListenURL(url *url.URL) (net.Listener, error) { + switch url.Scheme { + case "unix": + return net.Listen(url.Scheme, url.Path) + case "tcp": + return net.Listen("tcp", url.Host) + default: + return nil, errors.New("unexpected scheme") + } +} + +func Listen(endpoint string) (net.Listener, error) { + parsed, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + return listenURL(parsed) +} diff --git a/pkg/transport/listen_darwin.go b/pkg/transport/listen_darwin.go new file mode 100644 index 0000000..bea3042 --- /dev/null +++ b/pkg/transport/listen_darwin.go @@ -0,0 +1,47 @@ +package transport + +import ( + "errors" + "fmt" + "net" + "net/url" + "os" + "path" + "strconv" +) + +const DefaultURL = "vsock://null:1024/vm_directory" + +func listenURL(parsed *url.URL) (net.Listener, error) { + switch parsed.Scheme { + case "vsock": + port, err := strconv.Atoi(parsed.Port()) + if err != nil { + return nil, err + } + path := path.Join(parsed.Path, fmt.Sprintf("00000002.%08x", port)) + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + return nil, err + } + return net.ListenUnix("unix", &net.UnixAddr{ + Name: path, + Net: "unix", + }) + default: + return defaultListenURL(parsed) + } +} + +func ListenUnixgram(endpoint string) (*net.UnixConn, error) { + parsed, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + if parsed.Scheme != "unixgram" { + return nil, errors.New("unexpected scheme") + } + return net.ListenUnixgram("unixgram", &net.UnixAddr{ + Name: parsed.Path, + Net: "unixgram", + }) +} diff --git a/pkg/transport/listen_generic.go b/pkg/transport/listen_generic.go new file mode 100644 index 0000000..5553cd3 --- /dev/null +++ b/pkg/transport/listen_generic.go @@ -0,0 +1,13 @@ +//go:build !darwin && !linux && !windows +// +build !darwin,!linux,!windows + +package transport + +import ( + "net" + "net/url" +) + +func listenURL(url *url.URL) (net.Listener, error) { + return defaultListenURL(url) +} diff --git a/pkg/transport/listen_linux.go b/pkg/transport/listen_linux.go new file mode 100644 index 0000000..dfefecd --- /dev/null +++ b/pkg/transport/listen_linux.go @@ -0,0 +1,35 @@ +package transport + +import ( + "net" + "net/url" + "strconv" + + mdlayhervsock "github.com/mdlayher/vsock" +) + +const DefaultURL = "vsock://:1024" + +func listenURL(parsed *url.URL) (net.Listener, error) { + switch parsed.Scheme { + case "vsock": + port, err := strconv.Atoi(parsed.Port()) + if err != nil { + return nil, err + } + + if parsed.Hostname() != "" { + cid, err := strconv.Atoi(parsed.Hostname()) + if err != nil { + return nil, err + } + return mdlayhervsock.ListenContextID(uint32(cid), uint32(port), nil) + } + + return mdlayhervsock.Listen(uint32(port), nil) + case "unixpacket": + return net.Listen(parsed.Scheme, parsed.Path) + default: + return defaultListenURL(parsed) + } +} diff --git a/pkg/transport/listen_windows.go b/pkg/transport/listen_windows.go new file mode 100644 index 0000000..20c5d03 --- /dev/null +++ b/pkg/transport/listen_windows.go @@ -0,0 +1,26 @@ +package transport + +import ( + "net" + "net/url" + + "github.com/linuxkit/virtsock/pkg/hvsock" +) + +const DefaultURL = "vsock://00000400-FACB-11E6-BD58-64006A7986D3" + +func listenURL(parsed *url.URL) (net.Listener, error) { + switch parsed.Scheme { + case "vsock": + svcid, err := hvsock.GUIDFromString(parsed.Hostname()) + if err != nil { + return nil, err + } + return hvsock.Listen(hvsock.Addr{ + VMID: hvsock.GUIDWildcard, + ServiceID: svcid, + }) + default: + return defaultListenURL(parsed) + } +} diff --git a/pkg/transport/tunnel.go b/pkg/transport/tunnel.go new file mode 100644 index 0000000..06644bf --- /dev/null +++ b/pkg/transport/tunnel.go @@ -0,0 +1,28 @@ +package transport + +import ( + "errors" + "fmt" + "io" + "net" + "net/http" +) + +func Tunnel(conn net.Conn, ip string, port int) error { + req, err := http.NewRequest("POST", fmt.Sprintf("/tunnel?ip=%s&port=%d", ip, port), nil) + if err != nil { + return err + } + if err := req.Write(conn); err != nil { + return err + } + + ok := make([]byte, 2) + if _, err := io.ReadFull(conn, ok); err != nil { + return err + } + if string(ok) != "OK" { + return errors.New("handshake failed") + } + return nil +} diff --git a/pkg/transport/unixgram_darwin.go b/pkg/transport/unixgram_darwin.go new file mode 100644 index 0000000..12d3c50 --- /dev/null +++ b/pkg/transport/unixgram_darwin.go @@ -0,0 +1,67 @@ +//go:build darwin +// +build darwin + +package transport + +import ( + "bytes" + "encoding/hex" + "fmt" + "net" + "syscall" +) + +type connectedUnixgramConn struct { + *net.UnixConn + remoteAddr *net.UnixAddr +} + +func connectListeningUnixgramConn(conn *net.UnixConn, remoteAddr *net.UnixAddr) (*connectedUnixgramConn, error) { + rawConn, err := conn.SyscallConn() + if err != nil { + return nil, err + } + err = rawConn.Control(func(fd uintptr) { + if err = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_SNDBUF, 1*1024*1024); err != nil { + return + } + if err = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_RCVBUF, 4*1024*1024); err != nil { + return + } + }) + if err != nil { + return nil, err + } + + return &connectedUnixgramConn{ + UnixConn: conn, + remoteAddr: remoteAddr, + }, nil +} + +func (conn *connectedUnixgramConn) RemoteAddr() net.Addr { + return conn.remoteAddr +} + +func (conn *connectedUnixgramConn) Write(b []byte) (int, error) { + return conn.WriteTo(b, conn.remoteAddr) +} + +func AcceptVfkit(listeningConn *net.UnixConn) (net.Conn, error) { + vfkitMagic := make([]byte, 4) + // the main reason for this magic check is to get the address to use to send data to the vfkit VM + bytesRead, vfkitAddr, err := listeningConn.ReadFrom(vfkitMagic) + if bytesRead != len(vfkitMagic) { + return nil, fmt.Errorf("invalid magic length: %d", len(vfkitMagic)) + } + if err != nil { + return nil, err + } + if _, ok := vfkitAddr.(*net.UnixAddr); !ok { + return nil, fmt.Errorf("unexpected type for vfkit unix sockaddr: %t", vfkitAddr) + } + if !bytes.Equal(vfkitMagic, []byte("VFKT")) { + return nil, fmt.Errorf("invalid magic from the vfkit process: %s", hex.EncodeToString(vfkitMagic)) + } + return connectListeningUnixgramConn(listeningConn, vfkitAddr.(*net.UnixAddr)) +} diff --git a/pkg/transport/unixgram_nondarwin.go b/pkg/transport/unixgram_nondarwin.go new file mode 100644 index 0000000..09a9948 --- /dev/null +++ b/pkg/transport/unixgram_nondarwin.go @@ -0,0 +1,17 @@ +//go:build !darwin +// +build !darwin + +package transport + +import ( + "errors" + "net" +) + +func ListenUnixgram(_ string) (net.Conn, error) { + return nil, errors.New("unsupported 'unixgram' scheme") +} + +func AcceptVfkit(_ net.Conn) (net.Conn, error) { + return nil, errors.New("vfkit is unsupported on this platform") +} diff --git a/pkg/types/configuration.go b/pkg/types/configuration.go new file mode 100644 index 0000000..912fcb9 --- /dev/null +++ b/pkg/types/configuration.go @@ -0,0 +1,80 @@ +package types + +import ( + "net" + "regexp" +) + +type Configuration struct { + // Print packets on stderr + Debug bool + + // Record all packets coming in and out in a file that can be read by Wireshark (pcap) + CaptureFile string + + // Length of packet + // Larger packets means less packets to exchange for the same amount of data (and less protocol overhead) + MTU int + + // Network reserved for the virtual network + Subnet string + + // IP address of the virtual gateway + GatewayIP string + + // MAC address of the virtual gateway + GatewayMacAddress string + + // Built-in DNS records that will be served by the DNS server embedded in the gateway + DNS []Zone + + // List of search domains that will be added in all DHCP replies + DNSSearchDomains []string + + // Port forwarding between the machine running the gateway and the virtual network. + Forwards map[string]string + + // Address translation of incoming traffic. + // Useful for reaching the host itself (localhost) from the virtual network. + NAT map[string]string + + // IPs assigned to the gateway that can answer to ARP requests + GatewayVirtualIPs []string + + // DHCP static leases. Allow to assign pre-defined IP to virtual machine based on the MAC address + DHCPStaticLeases map[string]string + + // Only for Hyperkit + // Allow to assign a pre-defined MAC address to an Hyperkit VM + VpnKitUUIDMacAddresses map[string]string + + // Protocol to be used. Only for /connect mux + Protocol Protocol +} + +type Protocol string + +const ( + // HyperKitProtocol is handshake, then 16bits little endian size of packet, then the packet. + HyperKitProtocol Protocol = "hyperkit" + // QemuProtocol is 32bits big endian size of the packet, then the packet. + QemuProtocol Protocol = "qemu" + // BessProtocol transfers bare L2 packets as SOCK_SEQPACKET. + BessProtocol Protocol = "bess" + // StdioProtocol is HyperKitProtocol without the handshake + StdioProtocol Protocol = "stdio" + // VfkitProtocol transfers bare L2 packets as SOCK_DGRAM. + VfkitProtocol Protocol = "vfkit" +) + +type Zone struct { + Name string + Records []Record + DefaultIP net.IP +} + +type Record struct { + Name string + IP net.IP + Regexp *regexp.Regexp +} diff --git a/pkg/types/gvproxy_command.go b/pkg/types/gvproxy_command.go new file mode 100644 index 0000000..13df098 --- /dev/null +++ b/pkg/types/gvproxy_command.go @@ -0,0 +1,197 @@ +package types + +import ( + "os/exec" + "strconv" +) + +type GvproxyCommand struct { + // Print packets on stderr + Debug bool + + // Length of packet + // Larger packets means less packets to exchange for the same amount of data (and less protocol overhead) + MTU int + + // Values passed in by forward-xxx flags in commandline (forward-xxx:info) + forwardInfo map[string][]string + + // List of endpoints the user wants to listen to + endpoints []string + + // Map of different sockets provided by user (socket-type flag:socket) + sockets map[string]string + + // Logfile where gvproxy should redirect logs + LogFile string + + // File where gvproxy's pid is stored + PidFile string + + // SSHPort to access the guest VM + SSHPort int +} + +func NewGvproxyCommand() GvproxyCommand { + return GvproxyCommand{ + MTU: 1500, + SSHPort: 2222, + endpoints: []string{}, + forwardInfo: map[string][]string{}, + sockets: map[string]string{}, + } +} + +func (c *GvproxyCommand) checkSocketsInitialized() { + if len(c.sockets) < 1 { + c.sockets = map[string]string{} + } +} + +func (c *GvproxyCommand) checkForwardInfoInitialized() { + if len(c.forwardInfo) < 1 { + c.forwardInfo = map[string][]string{} + } +} + +func (c *GvproxyCommand) AddEndpoint(endpoint string) { + if len(c.endpoints) < 1 { + c.endpoints = []string{} + } + + c.endpoints = append(c.endpoints, endpoint) +} + +func (c *GvproxyCommand) AddVpnkitSocket(socket string) { + c.checkSocketsInitialized() + c.sockets["listen-vpnkit"] = socket +} + +func (c *GvproxyCommand) AddQemuSocket(socket string) { + c.checkSocketsInitialized() + c.sockets["listen-qemu"] = socket +} + +func (c *GvproxyCommand) AddBessSocket(socket string) { + c.checkSocketsInitialized() + c.sockets["listen-bess"] = socket +} + +func (c *GvproxyCommand) AddStdioSocket(socket string) { + c.checkSocketsInitialized() + c.sockets["listen-stdio"] = socket +} + +func (c *GvproxyCommand) AddVfkitSocket(socket string) { + c.checkSocketsInitialized() + c.sockets["listen-vfkit"] = socket +} + +func (c *GvproxyCommand) addForwardInfo(flag, value string) { + c.forwardInfo[flag] = append(c.forwardInfo[flag], value) +} + +func (c *GvproxyCommand) AddForwardSock(socket string) { + c.checkForwardInfoInitialized() + c.addForwardInfo("forward-sock", socket) +} + +func (c *GvproxyCommand) AddForwardDest(dest string) { + c.checkForwardInfoInitialized() + c.addForwardInfo("forward-dest", dest) +} + +func (c *GvproxyCommand) AddForwardUser(user string) { + c.checkForwardInfoInitialized() + c.addForwardInfo("forward-user", user) +} + +func (c *GvproxyCommand) AddForwardIdentity(identity string) { + c.checkForwardInfoInitialized() + c.addForwardInfo("forward-identity", identity) +} + +// socketsToCmdline converts Command.sockets to a commandline format +func (c *GvproxyCommand) socketsToCmdline() []string { + args := []string{} + + for socketFlag, socket := range c.sockets { + if socket != "" { + args = append(args, "-"+socketFlag, socket) + } + } + + return args +} + +// forwardInfoToCmdline converts Command.forwardInfo to a commandline format +func (c *GvproxyCommand) forwardInfoToCmdline() []string { + args := []string{} + + for forwardInfoFlag, forwardInfo := range c.forwardInfo { + for _, i := range forwardInfo { + if i != "" { + args = append(args, "-"+forwardInfoFlag, i) + } + } + } + + return args +} + +// endpointsToCmdline converts Command.endpoints to a commandline format +func (c *GvproxyCommand) endpointsToCmdline() []string { + args := []string{} + + for _, endpoint := range c.endpoints { + if endpoint != "" { + args = append(args, "-listen", endpoint) + } + } + + return args +} + +// ToCmdline converts Command to a properly formatted command for gvproxy based +// on its fields +func (c *GvproxyCommand) ToCmdline() []string { + args := []string{} + + // listen (endpoints) + args = append(args, c.endpointsToCmdline()...) + + // debug + if c.Debug { + args = append(args, "-debug") + } + + // mtu + args = append(args, "-mtu", strconv.Itoa(c.MTU)) + + // ssh-port + args = append(args, "-ssh-port", strconv.Itoa(c.SSHPort)) + + // sockets + args = append(args, c.socketsToCmdline()...) + + // forward info + args = append(args, c.forwardInfoToCmdline()...) + + // pid-file + if c.PidFile != "" { + args = append(args, "-pid-file", c.PidFile) + } + + // log-file + if c.LogFile != "" { + args = append(args, "-log-file", c.LogFile) + } + + return args +} + +// Cmd converts Command to a commandline format and returns an exec.Cmd which +// can be executed by os/exec +func (c *GvproxyCommand) Cmd(gvproxyPath string) *exec.Cmd { + return exec.Command(gvproxyPath, c.ToCmdline()...) // #nosec G204 +} diff --git a/pkg/types/handshake.go b/pkg/types/handshake.go new file mode 100644 index 0000000..e9aa780 --- /dev/null +++ b/pkg/types/handshake.go @@ -0,0 +1,21 @@ +package types + +type TransportProtocol string + +const ( + UDP TransportProtocol = "udp" + TCP TransportProtocol = "tcp" + UNIX TransportProtocol = "unix" + NPIPE TransportProtocol = "npipe" +) + +type ExposeRequest struct { + Local string `json:"local"` + Remote string `json:"remote"` + Protocol TransportProtocol `json:"protocol"` +} + +type UnexposeRequest struct { + Local string `json:"local"` + Protocol TransportProtocol `json:"protocol"` +} diff --git a/pkg/types/paths.go b/pkg/types/paths.go new file mode 100644 index 0000000..d50e5ae --- /dev/null +++ b/pkg/types/paths.go @@ -0,0 +1,3 @@ +package types + +const ConnectPath = "/connect" diff --git a/pkg/types/version.go b/pkg/types/version.go new file mode 100644 index 0000000..eca3a65 --- /dev/null +++ b/pkg/types/version.go @@ -0,0 +1,74 @@ +package types + +import ( + "flag" + "fmt" + "runtime/debug" + "strings" +) + +var ( + // set using the '-X github.com/containers/gvisor-tap-vsock/pkg/types.gitVersion' linker flag + gitVersion = "" + // set through .gitattributes when `git archive` is used + // see https://icinga.com/blog/2022/05/25/embedding-git-commit-information-in-go-binaries/ + gitArchiveVersion = "v0.7.3" +) + +type version struct { + binaryName string + showVersion bool +} + +func NewVersion(binaryName string) *version { //nolint:revive + return &version{ + binaryName: binaryName, + } +} + +func (ver *version) String() string { + return fmt.Sprintf("%s version %s", ver.binaryName, moduleVersion()) +} + +func (ver *version) AddFlag() { + flag.BoolVar(&ver.showVersion, "version", false, "Print version information") +} + +func (ver *version) ShowVersion() bool { + return ver.showVersion +} + +func moduleVersion() string { + switch { + // This will be substituted when building from a GitHub tarball + case !strings.HasPrefix(gitArchiveVersion, "$Format:"): + return gitArchiveVersion + // This will be set when building from git using make + case gitVersion != "": + if !strings.HasPrefix(gitVersion, "v") { + // if an annotated tag is found, the git describe string will be similar to: + // v0.7.2-15-g2c897d90 + // When using shallow clones, the commit being built + // may not have an annotated tag in its history, + // `git describe` will only be the abbreviated commit hash in this case: + // 2c897d90 + return fmt.Sprintf("git%s", gitVersion) + + } + return gitVersion + // moduleVersionFromBuildInfo() will be set when using `go install` + default: + return moduleVersionFromBuildInfo() + } +} + +func moduleVersionFromBuildInfo() string { + info, ok := debug.ReadBuildInfo() + if !ok { + return "" + } + if info.Main.Version == "(devel)" { + return "" + } + return info.Main.Version +} diff --git a/pkg/virtualnetwork/bess.go b/pkg/virtualnetwork/bess.go new file mode 100644 index 0000000..a797731 --- /dev/null +++ b/pkg/virtualnetwork/bess.go @@ -0,0 +1,12 @@ +package virtualnetwork + +import ( + "context" + "net" + + "github.com/containers/gvisor-tap-vsock/pkg/types" +) + +func (n *VirtualNetwork) AcceptBess(ctx context.Context, conn net.Conn) error { + return n.networkSwitch.Accept(ctx, conn, types.BessProtocol) +} diff --git a/pkg/virtualnetwork/conn.go b/pkg/virtualnetwork/conn.go new file mode 100644 index 0000000..038d8c5 --- /dev/null +++ b/pkg/virtualnetwork/conn.go @@ -0,0 +1,69 @@ +package virtualnetwork + +import ( + "context" + "errors" + "net" + "strconv" + + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" +) + +func (n *VirtualNetwork) Dial(network, addr string) (net.Conn, error) { + ip, port, err := splitIPPort(network, addr) + if err != nil { + return nil, err + } + return gonet.DialTCP(n.stack, tcpip.FullAddress{ + NIC: 1, + Addr: tcpip.AddrFrom4Slice(ip.To4()), + Port: uint16(port), + }, ipv4.ProtocolNumber) +} + +func (n *VirtualNetwork) DialContextTCP(ctx context.Context, addr string) (net.Conn, error) { + ip, port, err := splitIPPort("tcp", addr) + if err != nil { + return nil, err + } + + return gonet.DialContextTCP(ctx, n.stack, + tcpip.FullAddress{ + NIC: 1, + Addr: tcpip.AddrFrom4Slice(ip.To4()), + Port: uint16(port), + }, ipv4.ProtocolNumber) +} + +func (n *VirtualNetwork) Listen(network, addr string) (net.Listener, error) { + ip, port, err := splitIPPort(network, addr) + if err != nil { + return nil, err + } + return gonet.ListenTCP(n.stack, tcpip.FullAddress{ + NIC: 1, + Addr: tcpip.AddrFrom4Slice(ip.To4()), + Port: uint16(port), + }, ipv4.ProtocolNumber) +} + +func splitIPPort(network string, addr string) (net.IP, uint64, error) { + if network != "tcp" { + return nil, 0, errors.New("only tcp is supported") + } + host, portString, err := net.SplitHostPort(addr) + if err != nil { + return nil, 0, err + } + port, err := strconv.ParseUint(portString, 10, 16) + if err != nil { + return nil, 0, err + } + ip := net.ParseIP(host) + if ip == nil { + return nil, 0, errors.New("invalid address, must be an IP") + } + return ip, port, nil +} diff --git a/pkg/virtualnetwork/mux.go b/pkg/virtualnetwork/mux.go new file mode 100644 index 0000000..c671177 --- /dev/null +++ b/pkg/virtualnetwork/mux.go @@ -0,0 +1,100 @@ +package virtualnetwork + +import ( + "context" + "encoding/json" + "net" + "net/http" + "strconv" + + "github.com/containers/gvisor-tap-vsock/pkg/types" + log "github.com/sirupsen/logrus" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" + "inet.af/tcpproxy" +) + +func (n *VirtualNetwork) Mux() *http.ServeMux { + mux := http.NewServeMux() + mux.Handle("/services/", http.StripPrefix("/services", n.servicesMux)) + mux.HandleFunc("/stats", func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(statsAsJSON(n.networkSwitch.Sent, n.networkSwitch.Received, n.stack.Stats())) + }) + mux.HandleFunc("/cam", func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(n.networkSwitch.CAM()) + }) + mux.HandleFunc("/leases", func(w http.ResponseWriter, r *http.Request) { + _ = json.NewEncoder(w).Encode(n.ipPool.Leases()) + }) + mux.HandleFunc(types.ConnectPath, func(w http.ResponseWriter, r *http.Request) { + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError) + return + } + conn, bufrw, err := hj.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer conn.Close() + + if err := bufrw.Flush(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + _ = n.networkSwitch.Accept(context.Background(), conn, n.configuration.Protocol) + }) + mux.HandleFunc("/tunnel", func(w http.ResponseWriter, r *http.Request) { + ip := r.URL.Query().Get("ip") + if ip == "" { + http.Error(w, "ip is mandatory", http.StatusInternalServerError) + return + } + port, err := strconv.Atoi(r.URL.Query().Get("port")) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError) + return + } + + conn, bufrw, err := hj.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer conn.Close() + + if err := bufrw.Flush(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if _, err := conn.Write([]byte(`OK`)); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + remote := tcpproxy.DialProxy{ + DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return gonet.DialContextTCP(ctx, n.stack, tcpip.FullAddress{ + NIC: 1, + Addr: tcpip.AddrFrom4Slice(net.ParseIP(ip).To4()), + Port: uint16(port), + }, ipv4.ProtocolNumber) + }, + OnDialError: func(src net.Conn, dstDialErr error) { + log.Errorf("cannot dial: %v", dstDialErr) + }, + } + remote.HandleConn(conn) + }) + return mux +} diff --git a/pkg/virtualnetwork/qemu.go b/pkg/virtualnetwork/qemu.go new file mode 100644 index 0000000..20fbfed --- /dev/null +++ b/pkg/virtualnetwork/qemu.go @@ -0,0 +1,12 @@ +package virtualnetwork + +import ( + "context" + "net" + + "github.com/containers/gvisor-tap-vsock/pkg/types" +) + +func (n *VirtualNetwork) AcceptQemu(ctx context.Context, conn net.Conn) error { + return n.networkSwitch.Accept(ctx, conn, types.QemuProtocol) +} diff --git a/pkg/virtualnetwork/services.go b/pkg/virtualnetwork/services.go new file mode 100644 index 0000000..722d258 --- /dev/null +++ b/pkg/virtualnetwork/services.go @@ -0,0 +1,123 @@ +package virtualnetwork + +import ( + "net" + "net/http" + "strings" + "sync" + + "github.com/containers/gvisor-tap-vsock/pkg/services/dhcp" + "github.com/containers/gvisor-tap-vsock/pkg/services/dns" + "github.com/containers/gvisor-tap-vsock/pkg/services/forwarder" + "github.com/containers/gvisor-tap-vsock/pkg/tap" + "github.com/containers/gvisor-tap-vsock/pkg/types" + log "github.com/sirupsen/logrus" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" +) + +func addServices(configuration *types.Configuration, s *stack.Stack, ipPool *tap.IPPool) (http.Handler, error) { + var natLock sync.Mutex + translation := parseNATTable(configuration) + + tcpForwarder := forwarder.TCP(s, translation, &natLock) + s.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket) + udpForwarder := forwarder.UDP(s, translation, &natLock) + s.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket) + + dnsMux, err := dnsServer(configuration, s) + if err != nil { + return nil, err + } + + dhcpMux, err := dhcpServer(configuration, s, ipPool) + if err != nil { + return nil, err + } + + forwarderMux, err := forwardHostVM(configuration, s) + if err != nil { + return nil, err + } + mux := http.NewServeMux() + mux.Handle("/forwarder/", http.StripPrefix("/forwarder", forwarderMux)) + mux.Handle("/dhcp/", http.StripPrefix("/dhcp", dhcpMux)) + mux.Handle("/dns/", http.StripPrefix("/dns", dnsMux)) + return mux, nil +} + +func parseNATTable(configuration *types.Configuration) map[tcpip.Address]tcpip.Address { + translation := make(map[tcpip.Address]tcpip.Address) + for source, destination := range configuration.NAT { + translation[tcpip.AddrFrom4Slice(net.ParseIP(source).To4())] = tcpip.AddrFrom4Slice(net.ParseIP(destination).To4()) + } + return translation +} + +func dnsServer(configuration *types.Configuration, s *stack.Stack) (http.Handler, error) { + udpConn, err := gonet.DialUDP(s, &tcpip.FullAddress{ + NIC: 1, + Addr: tcpip.AddrFrom4Slice(net.ParseIP(configuration.GatewayIP).To4()), + Port: uint16(53), + }, nil, ipv4.ProtocolNumber) + if err != nil { + return nil, err + } + + tcpLn, err := gonet.ListenTCP(s, tcpip.FullAddress{ + NIC: 1, + Addr: tcpip.AddrFrom4Slice(net.ParseIP(configuration.GatewayIP).To4()), + Port: uint16(53), + }, ipv4.ProtocolNumber) + if err != nil { + return nil, err + } + + server, err := dns.New(udpConn, tcpLn, configuration.DNS) + if err != nil { + return nil, err + } + + go func() { + if err := server.Serve(); err != nil { + log.Error(err) + } + }() + go func() { + if err := server.ServeTCP(); err != nil { + log.Error(err) + } + }() + return server.Mux(), nil +} + +func dhcpServer(configuration *types.Configuration, s *stack.Stack, ipPool *tap.IPPool) (http.Handler, error) { + server, err := dhcp.New(configuration, s, ipPool) + if err != nil { + return nil, err + } + go func() { + log.Error(server.Serve()) + }() + return server.Mux(), nil +} + +func forwardHostVM(configuration *types.Configuration, s *stack.Stack) (http.Handler, error) { + fw := forwarder.NewPortsForwarder(s) + for local, remote := range configuration.Forwards { + if strings.HasPrefix(local, "udp:") { + if err := fw.Expose(types.UDP, strings.TrimPrefix(local, "udp:"), remote); err != nil { + return nil, err + } + } else { + if err := fw.Expose(types.TCP, local, remote); err != nil { + return nil, err + } + } + } + return fw.Mux(), nil +} diff --git a/pkg/virtualnetwork/stats.go b/pkg/virtualnetwork/stats.go new file mode 100644 index 0000000..db67689 --- /dev/null +++ b/pkg/virtualnetwork/stats.go @@ -0,0 +1,31 @@ +package virtualnetwork + +import ( + "reflect" + + "gvisor.dev/gvisor/pkg/tcpip" +) + +func iterateFields(ret map[string]interface{}, valueOf reflect.Value) { + for i := 0; i < valueOf.NumField(); i++ { + field := valueOf.Field(i) + fieldName := valueOf.Type().Field(i).Name + if field.Kind() == reflect.Struct { + m := make(map[string]interface{}) + ret[fieldName] = m + iterateFields(m, field) + continue + } + if counter, ok := field.Interface().(*tcpip.StatCounter); ok { + ret[fieldName] = counter.Value() + } + } +} + +func statsAsJSON(sent, received uint64, stats tcpip.Stats) map[string]interface{} { + root := make(map[string]interface{}) + iterateFields(root, reflect.ValueOf(stats)) + root["BytesSent"] = sent + root["BytesReceived"] = received + return root +} diff --git a/pkg/virtualnetwork/stdio.go b/pkg/virtualnetwork/stdio.go new file mode 100644 index 0000000..cf193d5 --- /dev/null +++ b/pkg/virtualnetwork/stdio.go @@ -0,0 +1,12 @@ +package virtualnetwork + +import ( + "context" + "net" + + "github.com/containers/gvisor-tap-vsock/pkg/types" +) + +func (n *VirtualNetwork) AcceptStdio(ctx context.Context, conn net.Conn) error { + return n.networkSwitch.Accept(ctx, conn, types.StdioProtocol) +} diff --git a/pkg/virtualnetwork/vfkit.go b/pkg/virtualnetwork/vfkit.go new file mode 100644 index 0000000..905e200 --- /dev/null +++ b/pkg/virtualnetwork/vfkit.go @@ -0,0 +1,12 @@ +package virtualnetwork + +import ( + "context" + "net" + + "github.com/containers/gvisor-tap-vsock/pkg/types" +) + +func (n *VirtualNetwork) AcceptVfkit(ctx context.Context, conn net.Conn) error { + return n.networkSwitch.Accept(ctx, conn, types.VfkitProtocol) +} diff --git a/pkg/virtualnetwork/virtualnetwork.go b/pkg/virtualnetwork/virtualnetwork.go new file mode 100644 index 0000000..4133717 --- /dev/null +++ b/pkg/virtualnetwork/virtualnetwork.go @@ -0,0 +1,144 @@ +package virtualnetwork + +import ( + "math" + "net" + "net/http" + "os" + + "github.com/containers/gvisor-tap-vsock/pkg/tap" + "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/pkg/errors" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/link/sniffer" + "gvisor.dev/gvisor/pkg/tcpip/network/arp" + "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/icmp" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" +) + +type VirtualNetwork struct { + configuration *types.Configuration + stack *stack.Stack + networkSwitch *tap.Switch + servicesMux http.Handler + ipPool *tap.IPPool +} + +func New(configuration *types.Configuration) (*VirtualNetwork, error) { + _, subnet, err := net.ParseCIDR(configuration.Subnet) + if err != nil { + return nil, errors.Wrap(err, "cannot parse subnet cidr") + } + + var endpoint stack.LinkEndpoint + + ipPool := tap.NewIPPool(subnet) + ipPool.Reserve(net.ParseIP(configuration.GatewayIP), configuration.GatewayMacAddress) + for ip, mac := range configuration.DHCPStaticLeases { + ipPool.Reserve(net.ParseIP(ip), mac) + } + + tapEndpoint, err := tap.NewLinkEndpoint(configuration.Debug, configuration.MTU, configuration.GatewayMacAddress, configuration.GatewayIP, configuration.GatewayVirtualIPs) + if err != nil { + return nil, errors.Wrap(err, "cannot create tap endpoint") + } + networkSwitch := tap.NewSwitch(configuration.Debug, configuration.MTU) + tapEndpoint.Connect(networkSwitch) + networkSwitch.Connect(tapEndpoint) + + if configuration.CaptureFile != "" { + _ = os.Remove(configuration.CaptureFile) + fd, err := os.Create(configuration.CaptureFile) + if err != nil { + return nil, errors.Wrap(err, "cannot create capture file") + } + endpoint, err = sniffer.NewWithWriter(tapEndpoint, fd, math.MaxUint32) + if err != nil { + return nil, errors.Wrap(err, "cannot create sniffer") + } + } else { + endpoint = tapEndpoint + } + + stack, err := createStack(configuration, endpoint) + if err != nil { + return nil, errors.Wrap(err, "cannot create network stack") + } + + mux, err := addServices(configuration, stack, ipPool) + if err != nil { + return nil, errors.Wrap(err, "cannot add network services") + } + + return &VirtualNetwork{ + configuration: configuration, + stack: stack, + networkSwitch: networkSwitch, + servicesMux: mux, + ipPool: ipPool, + }, nil +} + +func (n *VirtualNetwork) BytesSent() uint64 { + if n.networkSwitch == nil { + return 0 + } + return n.networkSwitch.Sent +} + +func (n *VirtualNetwork) BytesReceived() uint64 { + if n.networkSwitch == nil { + return 0 + } + return n.networkSwitch.Received +} + +func createStack(configuration *types.Configuration, endpoint stack.LinkEndpoint) (*stack.Stack, error) { + s := stack.New(stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ + ipv4.NewProtocol, + arp.NewProtocol, + }, + TransportProtocols: []stack.TransportProtocolFactory{ + tcp.NewProtocol, + udp.NewProtocol, + icmp.NewProtocol4, + }, + }) + + if err := s.CreateNIC(1, endpoint); err != nil { + return nil, errors.New(err.String()) + } + + if err := s.AddProtocolAddress(1, tcpip.ProtocolAddress{ + Protocol: ipv4.ProtocolNumber, + AddressWithPrefix: tcpip.AddrFrom4Slice(net.ParseIP(configuration.GatewayIP).To4()).WithPrefix(), + }, stack.AddressProperties{}); err != nil { + return nil, errors.New(err.String()) + } + + s.SetSpoofing(1, true) + s.SetPromiscuousMode(1, true) + + _, parsedSubnet, err := net.ParseCIDR(configuration.Subnet) + if err != nil { + return nil, errors.Wrap(err, "cannot parse cidr") + } + + subnet, err := tcpip.NewSubnet(tcpip.AddrFromSlice(parsedSubnet.IP), tcpip.MaskFromBytes(parsedSubnet.Mask)) + if err != nil { + return nil, errors.Wrap(err, "cannot parse subnet") + } + s.SetRouteTable([]tcpip.Route{ + { + Destination: subnet, + Gateway: tcpip.Address{}, + NIC: 1, + }, + }) + + return s, nil +} diff --git a/pkg/virtualnetwork/vpnkit.go b/pkg/virtualnetwork/vpnkit.go new file mode 100644 index 0000000..577670c --- /dev/null +++ b/pkg/virtualnetwork/vpnkit.go @@ -0,0 +1,82 @@ +package virtualnetwork + +import ( + "context" + "crypto/rand" + "encoding/binary" + "io" + "net" + + "github.com/containers/gvisor-tap-vsock/pkg/types" + log "github.com/sirupsen/logrus" + "gvisor.dev/gvisor/pkg/tcpip/header" +) + +func (n *VirtualNetwork) AcceptVpnKit(conn net.Conn) error { + if err := vpnkitHandshake(conn, n.configuration); err != nil { + log.Error(err) + } + _ = n.networkSwitch.Accept(context.Background(), conn, types.HyperKitProtocol) + return nil +} + +func vpnkitHandshake(conn net.Conn, configuration *types.Configuration) error { + // https://github.com/moby/hyperkit/blob/2f061e447e1435cdf1b9eda364cea6414f2c606b/src/lib/pci_virtio_net_vpnkit.c#L91 + msgInit := make([]byte, 49) + if _, err := io.ReadFull(conn, msgInit); err != nil { + return err + } + if _, err := conn.Write(msgInit); err != nil { + return err + } + + // https://github.com/moby/hyperkit/blob/2f061e447e1435cdf1b9eda364cea6414f2c606b/src/lib/pci_virtio_net_vpnkit.c#L123 + msgCommand := make([]byte, 41) + if _, err := io.ReadFull(conn, msgCommand); err != nil { + return err + } + vpnkitUUID := string(msgCommand[1:37]) + log.Debugf("UUID sent by Hyperkit: %s", vpnkitUUID) + + // https://github.com/moby/hyperkit/blob/2f061e447e1435cdf1b9eda364cea6414f2c606b/src/lib/pci_virtio_net_vpnkit.c#L131 + resp := make([]byte, 258) + resp[0] = 0x01 + mtu := uint16(configuration.MTU) + binary.LittleEndian.PutUint16(resp[1:3], mtu) + binary.LittleEndian.PutUint16(resp[3:5], mtu+header.EthernetMinimumSize) + + mac, err := macAddr(configuration, vpnkitUUID) + if err != nil { + return err + } + log.Debugf("Sending mac address: %s", mac.String()) + + copy(resp[5:11], mac) + + _, err = conn.Write(resp) + return err +} + +func macAddr(configuration *types.Configuration, vpnkitUUID string) (net.HardwareAddr, error) { + macStr, ok := configuration.VpnKitUUIDMacAddresses[vpnkitUUID] + if !ok { + return randomMac() + } + return net.ParseMAC(macStr) +} + +func randomMac() (net.HardwareAddr, error) { + buf := make([]byte, 6) + _, err := rand.Read(buf) + if err != nil { + return nil, err + } + + // Set the local bit + buf[0] |= 2 + + // Set the single address bit + buf[0] &= ^byte(1) + + return buf, nil +} diff --git a/rpm/gvisor-tap-vsock.spec b/rpm/gvisor-tap-vsock.spec new file mode 100644 index 0000000..c691dae --- /dev/null +++ b/rpm/gvisor-tap-vsock.spec @@ -0,0 +1,127 @@ +%global with_debug 1 + +%if 0%{?with_debug} +%global _find_debuginfo_dwz_opts %{nil} +%global _dwz_low_mem_die_limit 0 +%else +%global debug_package %{nil} +%endif + +%global gomodulesmode GO111MODULE=on + +%global _gvisor_installdir %{_libexecdir}/podman + +%global desc_gvforwarder Forward traffic from a tap interface over vsock + +Name: gvisor-tap-vsock +%if %{defined copr_username} +Epoch: 103 +%else +Epoch: 6 +%endif +# DO NOT TOUCH the Version string! +# The TRUE source of this specfile is: +# https://github.com/containers/podman/blob/main/rpm/podman.spec +# If that's what you're reading, Version must be 0, and will be updated by Packit for +# copr and koji builds. +# If you're reading this on dist-git, the version is automatically filled in by Packit. +Version: 0 +License: Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND MIT +%if %{defined autorelease} +Release: %autorelease +%else +Release: 1 +%endif +%if %{defined golang_arches_future} +ExclusiveArch: %{golang_arches_future} +%else +ExclusiveArch: aarch64 ppc64le s390x x86_64 +%endif +Summary: Go replacement for libslirp and VPNKit +URL: https://github.com/containers/%{name} +# All SourceN files fetched from upstream +Source0: %{url}/archive/refs/tags/v%{version}.tar.gz +BuildRequires: gcc +BuildRequires: glib2-devel +BuildRequires: glibc-devel +BuildRequires: glibc-static +BuildRequires: golang +BuildRequires: git-core +%if %{defined rhel} && 0%{?rhel} == 8 +BuildRequires: go-srpm-macros +%else +BuildRequires: go-rpm-macros +%endif +BuildRequires: make +%if %{defined copr_username} +Obsoletes: podman-gvproxy < 102:4.7.0-1 +%else +Obsoletes: podman-gvproxy < 5:4.7.0-1 +%endif +Provides: podman-gvproxy = %{epoch}:%{version}-%{release} +Requires: %{name}-gvforwarder = %{epoch}:%{version}-%{release} + +%description +A replacement for libslirp and VPNKit, written in pure Go. +It is based on the network stack of gVisor. Compared to libslirp, +gvisor-tap-vsock brings a configurable DNS server and +dynamic port forwarding. + +%package gvforwarder +Summary: %{desc_gvforwarder} +Provides: gvforwarder = %{epoch}:%{version}-%{release} +Obsoletes: %{name} < 6:0.7.0-6 +Recommends: %{name} = %{epoch}:%{version}-%{release} + +%description gvforwarder +%{desc_gvforwarder} + +%prep +%autosetup -Sgit -n %{name}-%{version} + +%build +%set_build_flags +export CGO_CFLAGS=$CFLAGS + +# These extra flags present in $CFLAGS have been skipped for now as they break the build +CGO_CFLAGS=$(echo $CGO_CFLAGS | sed 's/-flto=auto//g') +CGO_CFLAGS=$(echo $CGO_CFLAGS | sed 's/-Wp,D_GLIBCXX_ASSERTIONS//g') +CGO_CFLAGS=$(echo $CGO_CFLAGS | sed 's/-specs=\/usr\/lib\/rpm\/redhat\/redhat-annobin-cc1//g') + +%ifarch x86_64 +export CGO_CFLAGS+=" -m64 -mtune=generic -fcf-protection=full" +%endif + +# reset LDFLAGS for plugins and gvisor binaries +LDFLAGS='' + +# build gvisor-tap-vsock binaries +%gobuild -o bin/gvproxy ./cmd/gvproxy +%gobuild -o bin/gvforwarder ./cmd/vm + +%install +# install gvproxy +install -dp %{buildroot}%{_gvisor_installdir} +install -p -m0755 bin/gvproxy %{buildroot}%{_gvisor_installdir} +install -p -m0755 bin/gvforwarder %{buildroot}%{_gvisor_installdir} + +#define license tag if not already defined +%{!?_licensedir:%global license %doc} + +%files +%license LICENSE +%doc README.md +%dir %{_gvisor_installdir} +%{_gvisor_installdir}/gvproxy + +%files gvforwarder +%dir %{_gvisor_installdir} +%{_gvisor_installdir}/gvforwarder + +%changelog +%if %{defined autochangelog} +%autochangelog +%else +* Mon Jul 24 2023 RH Container Bot <rhcontainerbot@fedoraproject.org> +- Placeholder changelog for envs that are not autochangelog-ready +%endif diff --git a/test-win-sshproxy/basic_test.go b/test-win-sshproxy/basic_test.go new file mode 100644 index 0000000..e9541c5 --- /dev/null +++ b/test-win-sshproxy/basic_test.go @@ -0,0 +1,88 @@ +// +build windows + +package e2e + +import ( + "context" + "io" + "net" + "net/http" + "os" + "os/exec" + "strings" + "time" + + winio "github.com/Microsoft/go-winio" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var timeout = 1 * time.Minute + +var _ = Describe("connectivity", func() { + It("proxy exits as requested, without a kill", func() { + err := startProxy() + Expect(err).ShouldNot(HaveOccurred()) + + var pid uint32 + for i := 0; i < 20; i++ { + pid, _, err = readTid() + if err == nil { + break + } + time.Sleep(100 * time.Millisecond) + } + + Expect(err).ShouldNot(HaveOccurred()) + proc, err := os.FindProcess(int(pid)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(proc).ShouldNot(BeNil()) + err = stopProxy(true) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("proxies over a windows pipe", func() { + err := startProxy() + Expect(err).ShouldNot(HaveOccurred()) + defer stopProxy(false) + httpClient := &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return winio.DialPipe(`\\.\pipe\fake_docker_engine`, &timeout) + }, + }, + } + + Eventually(func(g Gomega) { + resp, err := httpClient.Get("http://host/ping") + g.Expect(err).ShouldNot(HaveOccurred()) + defer resp.Body.Close() + + g.Expect(resp.StatusCode).To(Equal(http.StatusOK)) + g.Expect(resp.ContentLength).To(Equal(int64(4))) + + reply := make([]byte, resp.ContentLength) + _, err = io.ReadAtLeast(resp.Body, reply, len(reply)) + + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(string(reply)).To(Equal("pong")) + + }).Should(Succeed()) + + err = stopProxy(true) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("windows event logs were created", func() { + cmd := exec.Command("powershell", "-Command", "&{Get-WinEvent -ProviderName \".NET Runtime\" -MaxEvents 10 | Where-Object -Property Message -Match \"test:\"}") + reader, err := cmd.StdoutPipe() + Expect(err).ShouldNot(HaveOccurred()) + cmd.Start() + output, err := io.ReadAll(reader) + Expect(err).ShouldNot(HaveOccurred()) + cmd.Wait() + Expect(strings.Contains(string(output), `[info ] test: Listening on: \\.\pipe\fake_docker_engine`)).Should(BeTrue()) + Expect(strings.Contains(string(output), `[debug] test: Socket forward established`)).Should(BeTrue()) + }) +}) diff --git a/test-win-sshproxy/mock_sshserver.go b/test-win-sshproxy/mock_sshserver.go new file mode 100644 index 0000000..f19c444 --- /dev/null +++ b/test-win-sshproxy/mock_sshserver.go @@ -0,0 +1,130 @@ +// +build windows + +package e2e + +import ( + "bufio" + "context" + "fmt" + "io" + "net" + "net/http" + "strings" + + "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh" +) + +const fakeHostKey = `-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAkXGLzDNnY5+xdAgnt8FlBIZtoFOZEdTUkNxkdSM05PgAAAJg9WMAvPVjA +LwAAAAtzc2gtZWQyNTUxOQAAACAkXGLzDNnY5+xdAgnt8FlBIZtoFOZEdTUkNxkdSM05Pg +AAAEAFvLprhpMPdNsxSwo1Cs5VP5joCh9XLicRqKE0JJzdxCRcYvMM2djn7F0CCe3wWUEh +m2gU5kR1NSQ3GR1IzTk+AAAAEmphc29uQFRyaXBlbC5sb2NhbAECAw== +-----END OPENSSH PRIVATE KEY-----` + +type streamLocalDirect struct { + SocketPath string + Reserved0 string + Reserved1 uint32 +} + +var cancel context.CancelFunc + +func startMockServer() { + sshConfig := &ssh.ServerConfig{ + NoClientAuth: true, + } + + key, err := ssh.ParsePrivateKey([]byte(fakeHostKey)) + if err != nil { + logrus.Errorf("Could not parse key: %s", err) + } + sshConfig.AddHostKey(key) + + listener, err := net.Listen("tcp", ":2134") + if err != nil { + panic(err) + } + + var ctx context.Context + ctx, cancel = context.WithCancel(context.Background()) + + go func() { + loop: + for { + select { + case <-ctx.Done(): + break loop + default: + // proceed + } + conn, err := listener.Accept() + if err != nil { + panic(err) + } + + // From a standard TCP connection to an encrypted SSH connection + _, chans, reqs, err := ssh.NewServerConn(conn, sshConfig) + if err != nil { + panic(err) + } + + go handleRequests(reqs) + // Accept all channels + go handleChannels(chans) + } + listener.Close() + }() +} + +func stopMockServer() { + cancel() +} + +func handleRequests(reqs <-chan *ssh.Request) { + for _ = range reqs { + } +} + +func handleChannels(chans <-chan ssh.NewChannel) { + directMsg := streamLocalDirect{} + for newChannel := range chans { + if t := newChannel.ChannelType(); t != "direct-streamlocal@openssh.com" { + newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t)) + continue + } + + if err := ssh.Unmarshal(newChannel.ExtraData(), &directMsg); err != nil { + logrus.Errorf("could not direct-streamlocal data: %s", err) + + newChannel.Reject(ssh.Prohibited, "invalid format") + return + } + + channel, _, err := newChannel.Accept() + if err != nil { + logrus.Errorf("could not accept channel: %s", err) + continue + } + + req, err := http.ReadRequest(bufio.NewReader(channel)) + if err != nil { + logrus.Errorf("could not process http request: %s", err) + } + + resp := http.Response{} + resp.Close = true + switch req.RequestURI { + case "/ping": + resp.StatusCode = 200 + resp.ContentLength = 4 + resp.Body = io.NopCloser(strings.NewReader("pong")) + default: + resp.StatusCode = 404 + resp.ContentLength = 0 + } + resp.Write(channel) + channel.CloseWrite() + } +} diff --git a/test-win-sshproxy/suite_test.go b/test-win-sshproxy/suite_test.go new file mode 100644 index 0000000..e671ed1 --- /dev/null +++ b/test-win-sshproxy/suite_test.go @@ -0,0 +1,123 @@ +// +build windows + +package e2e + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "syscall" + "testing" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +const ( + WM_QUIT = 0x12 +) + +var ( + tmpDir string + binDir string + keyFile string + winSshProxy string + tidFile string +) + +func TestSuite(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "win-sshproxy suite") +} + +func init() { + flag.StringVar(&tmpDir, "tmpDir", "../tmp", "temporary working directory") + flag.StringVar(&binDir, "bin", "../bin", "directory with compiled binaries") + _ = os.MkdirAll(tmpDir, 0755) + keyFile = filepath.Join(tmpDir, "id.key") + _ = os.WriteFile(keyFile, []byte(fakeHostKey), 0600) + winSshProxy = filepath.Join(binDir, "win-sshproxy.exe") + tidFile = filepath.Join(tmpDir, "win-sshproxy.tid") +} + +var _ = BeforeSuite(func() { + startMockServer() +}) + +var _ = AfterSuite(func() { + stopMockServer() +}) + +func startProxy() error { + os.Remove(tidFile) + cmd := exec.Command(winSshProxy, "-debug", "test", tmpDir, "npipe:////./pipe/fake_docker_engine", "ssh://localhost:2134/run/podman/podman.sock", keyFile) + return cmd.Start() +} + +func readTid() (uint32, uint32, error) { + contents, err := os.ReadFile(tidFile) + if err != nil { + return 0, 0, err + } + + var pid, tid uint32 + fmt.Sscanf(string(contents), "%d:%d", &pid, &tid) + return pid, tid, nil +} + +func sendQuit(tid uint32) { + user32 := syscall.NewLazyDLL("user32.dll") + postMessage := user32.NewProc("PostThreadMessageW") + postMessage.Call(uintptr(tid), WM_QUIT, 0, 0) +} + +func stopProxy(noKill bool) error { + pid, tid, err := readTid() + if err != nil { + return err + } + + proc, err := os.FindProcess(int(pid)) + if err != nil { + return err + } + sendQuit(tid) + state := waitTimeout(proc, 20*time.Second) + if state == nil || !state.Exited() { + if noKill { + return fmt.Errorf("proxy did not exit on request") + } + _ = proc.Kill() + state = waitTimeout(proc, 20*time.Second) + } + + if state == nil || !state.Exited() { + return fmt.Errorf("Stop proxy failed: %d", pid) + } + + _ = os.Remove(tidFile) + return nil +} + +func waitTimeout(proc *os.Process, timeout time.Duration) *os.ProcessState { + return doTimeout(func(complete chan *os.ProcessState) { + state, _ := proc.Wait() + complete <- state + }, timeout) +} + +func doTimeout(action func(complete chan *os.ProcessState), timeout time.Duration) *os.ProcessState { + complete := make(chan *os.ProcessState) + + go action(complete) + select { + case <-time.After(timeout): + return nil + + case state := <-complete: + return state + } +} diff --git a/test/basic_test.go b/test/basic_test.go new file mode 100644 index 0000000..9d3faf0 --- /dev/null +++ b/test/basic_test.go @@ -0,0 +1,222 @@ +package e2e + +import ( + "context" + "net" + "net/http" + + gvproxyclient "github.com/containers/gvisor-tap-vsock/pkg/client" + "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/onsi/ginkgo" + "github.com/onsi/gomega" +) + +var _ = ginkgo.Describe("connectivity", func() { + ginkgo.It("should configure the interface", func() { + out, err := sshExec("ifconfig $(route | grep '^default' | grep -o '[^ ]*$')") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring("mtu 1500")) + gomega.Expect(string(out)).To(gomega.ContainSubstring("inet 192.168.127.2")) + gomega.Expect(string(out)).To(gomega.ContainSubstring("netmask 255.255.255.0")) + gomega.Expect(string(out)).To(gomega.ContainSubstring("broadcast 192.168.127.255")) + }) + + ginkgo.It("should configure the default route", func() { + out, err := sshExec("ip route show") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.MatchRegexp(`default via 192\.168\.127\.1 dev (.*?) proto dhcp (src 192\.168\.127\.2 )?metric 100`)) + }) + + ginkgo.It("should configure dns settings", func() { + out, err := sshExec("cat /etc/resolv.conf") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring("nameserver 192.168.127.1")) + }) + + ginkgo.It("should ping the tap device", func() { + out, err := sshExec("ping -c2 192.168.127.2") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring("2 packets transmitted, 2 received, 0% packet loss")) + }) + + ginkgo.It("should ping the gateway", func() { + out, err := sshExec("ping -c2 192.168.127.1") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring("2 packets transmitted, 2 received, 0% packet loss")) + }) +}) + +var _ = ginkgo.Describe("dns", func() { + ginkgo.It("should resolve redhat.com", func() { + out, err := sshExec("nslookup redhat.com") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 52.200.142.250")) + }) + + ginkgo.It("should resolve CNAME record for www.wikipedia.org", func() { + out, err := sshExec("nslookup -query=cname www.wikipedia.org") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring("www.wikipedia.org canonical name = dyna.wikimedia.org.")) + }) + ginkgo.It("should resolve MX record for wikipedia.org", func() { + out, err := sshExec("nslookup -query=mx wikipedia.org") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring("wikipedia.org mail exchanger = 10 mx1001.wikimedia.org.")) + }) + + ginkgo.It("should resolve NS record for wikipedia.org", func() { + out, err := sshExec("nslookup -query=ns wikipedia.org") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring("wikipedia.org nameserver = ns0.wikimedia.org.")) + }) + ginkgo.It("should resolve LDAP SRV record for google.com", func() { + out, err := sshExec("nslookup -query=srv _ldap._tcp.google.com") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring(`_ldap._tcp.google.com service = 5 0 389 ldap.google.com.`)) + }) + ginkgo.It("should resolve TXT for wikipedia.org", func() { + out, err := sshExec("nslookup -query=txt wikipedia.org") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring(`"v=spf1 include:wikimedia.org ~all"`)) + }) + + ginkgo.It("should resolve gateway.containers.internal", func() { + out, err := sshExec("nslookup gateway.containers.internal") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.1")) + }) + + ginkgo.It("should resolve host.containers.internal", func() { + out, err := sshExec("nslookup host.containers.internal") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.254")) + }) + + ginkgo.It("should resolve dynamically added dns entry test.dynamic.internal", func() { + client := gvproxyclient.New(&http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return net.Dial("unix", sock) + }, + }, + }, "http://base") + err := client.AddDNS(&types.Zone{ + Name: "dynamic.internal.", + Records: []types.Record{ + { + Name: "test", + IP: net.ParseIP("192.168.127.254"), + }, + }, + }) + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + + out, err := sshExec("nslookup test.dynamic.internal") + + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.254")) + }) + + ginkgo.It("should resolve recently added dns entry test.dynamic.internal", func() { + client := gvproxyclient.New(&http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return net.Dial("unix", sock) + }, + }, + }, "http://base") + err := client.AddDNS(&types.Zone{ + Name: "dynamic.internal.", + Records: []types.Record{ + { + Name: "test", + IP: net.ParseIP("192.168.127.254"), + }, + }, + }) + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + + err = client.AddDNS(&types.Zone{ + Name: "dynamic.internal.", + Records: []types.Record{ + { + Name: "test", + IP: net.ParseIP("192.168.127.253"), + }, + }, + }) + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + + out, err := sshExec("nslookup test.dynamic.internal") + + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.253")) + }) + + ginkgo.It("should retain order of existing zone", func() { + client := gvproxyclient.New(&http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return net.Dial("unix", sock) + }, + }, + }, "http://base") + _ = client.AddDNS(&types.Zone{ + Name: "dynamic.testing.", + DefaultIP: net.ParseIP("192.168.127.2"), + }) + _ = client.AddDNS(&types.Zone{ + Name: "testing.", + Records: []types.Record{ + { + Name: "host", + IP: net.ParseIP("192.168.127.3"), + }, + }, + }) + out, err := sshExec("nslookup test.dynamic.internal") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.2")) + + _ = client.AddDNS(&types.Zone{ + Name: "testing.", + Records: []types.Record{ + { + Name: "gateway", + IP: net.ParseIP("192.168.127.1"), + }, + }, + }) + out, err = sshExec("nslookup *.dynamic.testing") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.2")) + + out, err = sshExec("nslookup gateway.testing") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.1")) + }) +}) + +var _ = ginkgo.Describe("command-line format", func() { + ginkgo.It("should convert Command to command line format", func() { + command := types.NewGvproxyCommand() + command.AddEndpoint("unix:///tmp/network.sock") + command.Debug = true + command.AddQemuSocket("tcp://0.0.0.0:1234") + command.PidFile = "~/gv-pidfile.txt" + command.LogFile = "~/gv.log" + command.AddForwardUser("demouser") + + cmd := command.ToCmdline() + gomega.Expect(cmd).To(gomega.Equal([]string{ + "-listen", "unix:///tmp/network.sock", + "-debug", + "-mtu", "1500", + "-ssh-port", "2222", + "-listen-qemu", "tcp://0.0.0.0:1234", + "-forward-user", "demouser", + "-pid-file", "~/gv-pidfile.txt", + "-log-file", "~/gv.log", + })) + }) +}) diff --git a/test/fcos.go b/test/fcos.go new file mode 100644 index 0000000..e9cb9ac --- /dev/null +++ b/test/fcos.go @@ -0,0 +1,80 @@ +package e2e + +import ( + "os" + "path/filepath" + "strings" + + "github.com/opencontainers/go-digest" +) + +type FcosDownload struct { + DataDir string +} + +type fcosDownloadInfo struct { + Location string + Sha256Sum string +} + +func NewFcosDownloader(dataDir string) (*FcosDownload, error) { + return &FcosDownload{ + DataDir: dataDir, + }, nil +} + +func imageName(info *fcosDownloadInfo) string { + urlSplit := strings.Split(info.Location, "/") + return urlSplit[len(urlSplit)-1] +} + +func (downloader *FcosDownload) DownloadImage() (string, error) { + info, err := getFCOSDownload() + if err != nil { + return "", err + } + + compressedImage := filepath.Join(downloader.DataDir, imageName(info)) + uncompressedImage := strings.TrimSuffix(filepath.Join(filepath.Dir(compressedImage), imageName(info)), ".xz") + + // check if the latest image is already present + ok, err := downloader.updateAvailable(info, compressedImage) + if err != nil { + return "", err + } + if !ok { + if err := DownloadVMImage(info.Location, compressedImage); err != nil { + return "", err + } + } + + if _, err := os.Stat(uncompressedImage); err == nil { + return uncompressedImage, nil + } + if err := Decompress(compressedImage, uncompressedImage); err != nil { + return "", err + } + return uncompressedImage, nil +} + +func (downloader *FcosDownload) updateAvailable(info *fcosDownloadInfo, compressedImage string) (bool, error) { + // check the sha of the local image if it exists + // get the sha of the remote image + // == dont bother to pull + if _, err := os.Stat(compressedImage); os.IsNotExist(err) { + return false, nil + } + fd, err := os.Open(compressedImage) + if err != nil { + return false, err + } + defer fd.Close() + sum, err := digest.SHA256.FromReader(fd) + if err != nil { + return false, err + } + if sum.Encoded() == info.Sha256Sum { + return true, nil + } + return false, nil +} diff --git a/test/fcos_amd64.go b/test/fcos_amd64.go new file mode 100644 index 0000000..3ffb906 --- /dev/null +++ b/test/fcos_amd64.go @@ -0,0 +1,64 @@ +package e2e + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/coreos/stream-metadata-go/fedoracoreos" + "github.com/coreos/stream-metadata-go/stream" + "github.com/sirupsen/logrus" +) + +// This should get Exported and stay put as it will apply to all fcos downloads +// getFCOS parses fedoraCoreOS's stream and returns the image download URL and the release version +func getFCOSDownload() (*fcosDownloadInfo, error) { + streamurl := fedoracoreos.GetStreamURL(fedoracoreos.StreamNext) + resp, err := http.Get(streamurl.String()) + if err != nil { + return nil, err + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + defer func() { + if err := resp.Body.Close(); err != nil { + logrus.Error(err) + } + }() + + var fcosstable stream.Stream + if err := json.Unmarshal(body, &fcosstable); err != nil { + return nil, err + } + arch, ok := fcosstable.Architectures["x86_64"] + if !ok { + return nil, fmt.Errorf("unable to pull VM image: no targetArch in stream") + } + artifacts := arch.Artifacts + if artifacts == nil { + return nil, fmt.Errorf("unable to pull VM image: no artifact in stream") + } + qemu, ok := artifacts["qemu"] + if !ok { + return nil, fmt.Errorf("unable to pull VM image: no qemu artifact in stream") + } + formats := qemu.Formats + if formats == nil { + return nil, fmt.Errorf("unable to pull VM image: no formats in stream") + } + qcow, ok := formats["qcow2.xz"] + if !ok { + return nil, fmt.Errorf("unable to pull VM image: no qcow2.xz format in stream") + } + disk := qcow.Disk + if disk == nil { + return nil, fmt.Errorf("unable to pull VM image: no disk in stream") + } + return &fcosDownloadInfo{ + Location: disk.Location, + Sha256Sum: disk.Sha256, + }, nil +} diff --git a/test/ignition.go b/test/ignition.go new file mode 100644 index 0000000..a9dcbff --- /dev/null +++ b/test/ignition.go @@ -0,0 +1,162 @@ +package e2e + +import ( + "encoding/json" + "net/url" + "os" +) + +var ( + mode = 0644 + dirMode = 0744 + root = "root" + test = "test" + yes = true + no = false +) + +func CreateIgnition(ignitionFile string, publicKey string, user string, password string) error { + + linger := `[Unit] +Description=Activate podman socket +Wants=podman.socket +[Service] +ExecStart=/usr/bin/sleep infinity +` + + systemd := Systemd{ + Units: []Unit{ + { + Name: "systemd-resolved.service", + Enabled: &no, + Mask: &yes, + }, + { + Name: "podman.socket", + Enabled: &yes, + }, + }, + } + + passwd := Passwd{ + Users: []PasswdUser{ + { + Name: user, + PasswordHash: &password, + SSHAuthorizedKeys: []SSHAuthorizedKey{ + SSHAuthorizedKey(publicKey), + }, + Groups: []Group{ + "wheel", + "sudo", + }, + }, + { + Name: "root", + PasswordHash: &password, + SSHAuthorizedKeys: []SSHAuthorizedKey{ + SSHAuthorizedKey(publicKey), + }, + }, + }, + } + + storage := Storage{ + // Replaces resolv.conf with an empty file that will be overwritten by NetworkManager + Files: []File{ + { + Node: Node{ + Group: NodeGroup{Name: &root}, + Path: "/etc/resolv.conf", + User: NodeUser{Name: &root}, + Overwrite: &yes, + }, + FileEmbedded1: FileEmbedded1{ + Contents: Resource{ + Source: encodeData(""), + }, + Mode: &mode, + }, + }, + { + Node: Node{ + Group: NodeGroup{Name: &test}, + Path: "/home/" + test + "/.config/systemd/user/linger-podman.service", + User: NodeUser{Name: &test}, + Overwrite: &yes, + }, + FileEmbedded1: FileEmbedded1{ + Contents: Resource{ + Source: encodeData(linger), + }, + Mode: &mode, + }, + }, + { + Node: Node{ + Group: NodeGroup{Name: &test}, + Path: "/var/lib/systemd/linger/" + test, + User: NodeUser{Name: &test}, + Overwrite: &yes, + }, + FileEmbedded1: FileEmbedded1{ + Contents: Resource{ + Source: encodeData(""), + }, + Mode: &mode, + }, + }, + }, + Directories: []Directory{ + dir("/home/" + test + "/.config"), + dir("/home/" + test + "/.config/containers"), + dir("/home/" + test + "/.config/systemd"), + dir("/home/" + test + "/.config/systemd/user"), + dir("/home/" + test + "/.config/systemd/user/default.target.wants"), + }, + Links: []Link{ + { + Node: Node{ + Group: NodeGroup{Name: &test}, + Path: "/home/" + test + "/.config/systemd/user/default.target.wants/linger-podman.service", + User: NodeUser{Name: &test}, + }, + LinkEmbedded1: LinkEmbedded1{ + Hard: &no, + Target: "/home/" + test + "/.config/systemd/user/linger-podman.service", + }, + }, + }, + } + + config := Config{ + Ignition: Ignition{Version: "3.2.0"}, + Systemd: systemd, + Passwd: passwd, + Storage: storage, + } + + contents, err := json.Marshal(config) + if err != nil { + return err + } + + // #nosec + return os.WriteFile(ignitionFile, contents, 0644) +} + +func dir(path string) Directory { + return Directory{ + Node: Node{ + Group: NodeGroup{Name: &test}, + Path: path, + User: NodeUser{Name: &test}, + }, + DirectoryEmbedded1: DirectoryEmbedded1{Mode: &dirMode}, + } +} + +func encodeData(data string) *string { + str := "data:," + url.PathEscape(data) + return &str +} diff --git a/test/ignition_schema.go b/test/ignition_schema.go new file mode 100644 index 0000000..5fb9d75 --- /dev/null +++ b/test/ignition_schema.go @@ -0,0 +1,248 @@ +package e2e + +// Taken from https://github.com/coreos/ignition/blob/master/config/v3_2/types/schema.go + +// generated by "schematyper --package=types config/v3_2/schema/ignition.json -o config/v3_2/types/schema.go --root-type=Config" -- DO NOT EDIT + +type Clevis struct { + Custom *Custom `json:"custom,omitempty"` + Tang []Tang `json:"tang,omitempty"` + Threshold *int `json:"threshold,omitempty"` + Tpm2 *bool `json:"tpm2,omitempty"` +} + +type Config struct { + Ignition Ignition `json:"ignition"` + Passwd Passwd `json:"passwd,omitempty"` + Storage Storage `json:"storage,omitempty"` + Systemd Systemd `json:"systemd,omitempty"` +} + +type Custom struct { + Config string `json:"config"` + NeedsNetwork *bool `json:"needsNetwork,omitempty"` + Pin string `json:"pin"` +} + +type Device string + +type Directory struct { + Node + DirectoryEmbedded1 +} + +type DirectoryEmbedded1 struct { + Mode *int `json:"mode,omitempty"` +} + +type Disk struct { + Device string `json:"device"` + Partitions []Partition `json:"partitions,omitempty"` + WipeTable *bool `json:"wipeTable,omitempty"` +} + +type Dropin struct { + Contents *string `json:"contents,omitempty"` + Name string `json:"name"` +} + +type File struct { + Node + FileEmbedded1 +} + +type FileEmbedded1 struct { + Append []Resource `json:"append,omitempty"` + Contents Resource `json:"contents,omitempty"` + Mode *int `json:"mode,omitempty"` +} + +type Filesystem struct { + Device string `json:"device"` + Format *string `json:"format,omitempty"` + Label *string `json:"label,omitempty"` + MountOptions []MountOption `json:"mountOptions,omitempty"` + Options []FilesystemOption `json:"options,omitempty"` + Path *string `json:"path,omitempty"` + UUID *string `json:"uuid,omitempty"` + WipeFilesystem *bool `json:"wipeFilesystem,omitempty"` +} + +type FilesystemOption string + +type Group string + +type HTTPHeader struct { + Name string `json:"name"` + Value *string `json:"value,omitempty"` +} + +type HTTPHeaders []HTTPHeader + +type Ignition struct { + Config IgnitionConfig `json:"config,omitempty"` + Proxy Proxy `json:"proxy,omitempty"` + Security Security `json:"security,omitempty"` + Timeouts Timeouts `json:"timeouts,omitempty"` + Version string `json:"version,omitempty"` +} + +type IgnitionConfig struct { + Merge []Resource `json:"merge,omitempty"` + Replace Resource `json:"replace,omitempty"` +} + +type Link struct { + Node + LinkEmbedded1 +} + +type LinkEmbedded1 struct { + Hard *bool `json:"hard,omitempty"` + Target string `json:"target"` +} + +type Luks struct { + Clevis *Clevis `json:"clevis,omitempty"` + Device *string `json:"device,omitempty"` + KeyFile Resource `json:"keyFile,omitempty"` + Label *string `json:"label,omitempty"` + Name string `json:"name"` + Options []LuksOption `json:"options,omitempty"` + UUID *string `json:"uuid,omitempty"` + WipeVolume *bool `json:"wipeVolume,omitempty"` +} + +type LuksOption string + +type MountOption string + +type NoProxyItem string + +type Node struct { + Group NodeGroup `json:"group,omitempty"` + Overwrite *bool `json:"overwrite,omitempty"` + Path string `json:"path"` + User NodeUser `json:"user,omitempty"` +} + +type NodeGroup struct { + ID *int `json:"id,omitempty"` + Name *string `json:"name,omitempty"` +} + +type NodeUser struct { + ID *int `json:"id,omitempty"` + Name *string `json:"name,omitempty"` +} + +type Partition struct { + GUID *string `json:"guid,omitempty"` + Label *string `json:"label,omitempty"` + Number int `json:"number,omitempty"` + Resize *bool `json:"resize,omitempty"` + ShouldExist *bool `json:"shouldExist,omitempty"` + SizeMiB *int `json:"sizeMiB,omitempty"` + StartMiB *int `json:"startMiB,omitempty"` + TypeGUID *string `json:"typeGuid,omitempty"` + WipePartitionEntry *bool `json:"wipePartitionEntry,omitempty"` +} + +type Passwd struct { + Groups []PasswdGroup `json:"groups,omitempty"` + Users []PasswdUser `json:"users,omitempty"` +} + +type PasswdGroup struct { + Gid *int `json:"gid,omitempty"` + Name string `json:"name"` + PasswordHash *string `json:"passwordHash,omitempty"` + ShouldExist *bool `json:"shouldExist,omitempty"` + System *bool `json:"system,omitempty"` +} + +type PasswdUser struct { + Gecos *string `json:"gecos,omitempty"` + Groups []Group `json:"groups,omitempty"` + HomeDir *string `json:"homeDir,omitempty"` + Name string `json:"name"` + NoCreateHome *bool `json:"noCreateHome,omitempty"` + NoLogInit *bool `json:"noLogInit,omitempty"` + NoUserGroup *bool `json:"noUserGroup,omitempty"` + PasswordHash *string `json:"passwordHash,omitempty"` + PrimaryGroup *string `json:"primaryGroup,omitempty"` + SSHAuthorizedKeys []SSHAuthorizedKey `json:"sshAuthorizedKeys,omitempty"` + Shell *string `json:"shell,omitempty"` + ShouldExist *bool `json:"shouldExist,omitempty"` + System *bool `json:"system,omitempty"` + UID *int `json:"uid,omitempty"` +} + +type Proxy struct { + HTTPProxy *string `json:"httpProxy,omitempty"` + HTTPSProxy *string `json:"httpsProxy,omitempty"` + NoProxy []NoProxyItem `json:"noProxy,omitempty"` +} + +type Raid struct { + Devices []Device `json:"devices"` + Level string `json:"level"` + Name string `json:"name"` + Options []RaidOption `json:"options,omitempty"` + Spares *int `json:"spares,omitempty"` +} + +type RaidOption string + +type Resource struct { + Compression *string `json:"compression,omitempty"` + HTTPHeaders HTTPHeaders `json:"httpHeaders,omitempty"` + Source *string `json:"source,omitempty"` + Verification Verification `json:"verification,omitempty"` +} + +type SSHAuthorizedKey string + +type Security struct { + TLS TLS `json:"tls,omitempty"` +} + +type Storage struct { + Directories []Directory `json:"directories,omitempty"` + Disks []Disk `json:"disks,omitempty"` + Files []File `json:"files,omitempty"` + Filesystems []Filesystem `json:"filesystems,omitempty"` + Links []Link `json:"links,omitempty"` + Luks []Luks `json:"luks,omitempty"` + Raid []Raid `json:"raid,omitempty"` +} + +type Systemd struct { + Units []Unit `json:"units,omitempty"` +} + +type TLS struct { + CertificateAuthorities []Resource `json:"certificateAuthorities,omitempty"` +} + +type Tang struct { + Thumbprint *string `json:"thumbprint,omitempty"` + URL string `json:"url,omitempty"` +} + +type Timeouts struct { + HTTPResponseHeaders *int `json:"httpResponseHeaders,omitempty"` + HTTPTotal *int `json:"httpTotal,omitempty"` +} + +type Unit struct { + Contents *string `json:"contents,omitempty"` + Dropins []Dropin `json:"dropins,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + Mask *bool `json:"mask,omitempty"` + Name string `json:"name"` +} + +type Verification struct { + Hash *string `json:"hash,omitempty"` +}
\ No newline at end of file diff --git a/test/performance.sh b/test/performance.sh new file mode 100755 index 0000000..4f4c413 --- /dev/null +++ b/test/performance.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +set -x + +VM=$1 +GVPROXY_SOCKET=$2 + +echo "Testing Internet access with a server running on the host" + +nohup iperf3 -s > /dev/null 2>&1 & +serverPID=$! + +ssh $VM curl https://iperf.fr/download/ubuntu/libiperf.so.0_3.1.3 -o libiperf.so.0 +ssh $VM curl https://iperf.fr/download/ubuntu/iperf3_3.1.3 -o iperf3 +ssh $VM chmod +x iperf3 + +echo "TCP: sending data" +ssh $VM LD_LIBRARY_PATH=. ./iperf3 -c host.crc.testing +echo "TCP: receiving data" +ssh $VM LD_LIBRARY_PATH=. ./iperf3 -c host.crc.testing -R + +echo "UDP: sending data" +ssh $VM LD_LIBRARY_PATH=. ./iperf3 -c host.crc.testing -u +echo "UDP: receiving data" +ssh $VM LD_LIBRARY_PATH=. ./iperf3 -c host.crc.testing -R -u + +kill $serverPID + +echo "Testing forwarder with a server running in the VM" + +curl --unix-socket $GVPROXY_SOCKET http:/unix/services/forwarder/expose -X POST \ + -d'{"local":":5201", "protocol": "udp", "remote": "192.168.127.2:5201"}' +curl --unix-socket $GVPROXY_SOCKET http:/unix/services/forwarder/expose -X POST \ + -d'{"local":":5201", "protocol": "tcp", "remote": "192.168.127.2:5201"}' + +ssh $VM LD_LIBRARY_PATH=. ./iperf3 -s > /dev/null 2>&1 & +sleep 1 + +echo "TCP: sending data" +iperf3 -c 127.0.0.1 +echo "TCP: receiving data" +iperf3 -c 127.0.0.1 -R + +echo "UDP: sending data" +iperf3 -c 127.0.0.1 -u -l 9216 +echo "UDP: receiving data" +iperf3 -c 127.0.0.1 -R -u -l 9216 + +ssh $VM pkill iperf3 + +curl --unix-socket $GVPROXY_SOCKET http:/unix/services/forwarder/unexpose -X POST \ + -d'{"local":":5201", "protocol": "udp"}' +curl --unix-socket $GVPROXY_SOCKET http:/unix/services/forwarder/unexpose -X POST \ + -d'{"local":":5201", "protocol": "tcp"}' diff --git a/test/port_forwarding_test.go b/test/port_forwarding_test.go new file mode 100644 index 0000000..c38e4f6 --- /dev/null +++ b/test/port_forwarding_test.go @@ -0,0 +1,269 @@ +package e2e + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "time" + + gvproxyclient "github.com/containers/gvisor-tap-vsock/pkg/client" + "github.com/containers/gvisor-tap-vsock/pkg/transport" + "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/onsi/ginkgo" + "github.com/onsi/gomega" + log "github.com/sirupsen/logrus" +) + +var _ = ginkgo.Describe("port forwarding", func() { + client := gvproxyclient.New(&http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return net.Dial("unix", sock) + }, + }, + }, "http://base") + + ginkgo.It("should reach a http server on the host", func() { + ln, err := net.Listen("tcp", "127.0.0.1:9090") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + defer ln.Close() + + mux := http.NewServeMux() + mux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { + _, _ = writer.Write([]byte("Hello from the host")) + }) + go func() { + s := &http.Server{ + Handler: mux, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + } + err := s.Serve(ln) + if err != nil { + log.Error(err) + } + }() + + out, err := sshExec("curl http://host.containers.internal:9090") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring("Hello from the host")) + + out, err = sshExec("curl http://host.docker.internal:9090") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + gomega.Expect(string(out)).To(gomega.ContainSubstring("Hello from the host")) + }) + + ginkgo.It("should reach a http server in the VM using dynamic port forwarding", func() { + _, err := net.Dial("tcp", "127.0.0.1:9090") + gomega.Expect(err).Should(gomega.HaveOccurred()) + gomega.Expect(err.Error()).To(gomega.HaveSuffix("connection refused")) + + gomega.Expect(client.Expose(&types.ExposeRequest{ + Local: "127.0.0.1:9090", + Remote: "192.168.127.2:8080", + })).Should(gomega.Succeed()) + + gomega.Eventually(func(g gomega.Gomega) { + resp, err := http.Get("http://127.0.0.1:9090") + g.Expect(err).ShouldNot(gomega.HaveOccurred()) + g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK)) + }).Should(gomega.Succeed()) + + gomega.Expect(client.Unexpose(&types.UnexposeRequest{ + Local: "127.0.0.1:9090", + })).Should(gomega.Succeed()) + + gomega.Eventually(func(g gomega.Gomega) { + _, err = net.Dial("tcp", "127.0.0.1:9090") + g.Expect(err).Should(gomega.HaveOccurred()) + g.Expect(err.Error()).To(gomega.HaveSuffix("connection refused")) + }).Should(gomega.Succeed()) + }) + + ginkgo.It("should reach a dns server in the VM using dynamic port forwarding", func() { + gomega.Expect(client.Expose(&types.ExposeRequest{ + Local: ":1053", + Remote: "192.168.127.2:53", + Protocol: "udp", + })).Should(gomega.Succeed()) + + gomega.Eventually(func(g gomega.Gomega) { + cmd := exec.Command("nslookup", "-timeout=1", "-port=1053", "foobar", "127.0.0.1") + out, err := cmd.CombinedOutput() + g.Expect(err).ShouldNot(gomega.HaveOccurred()) + g.Expect(string(out)).To(gomega.ContainSubstring("Address: 1.2.3.4")) + }).Should(gomega.Succeed()) + + gomega.Expect(client.Unexpose(&types.UnexposeRequest{ + Local: ":1053", + Protocol: "udp", + })).Should(gomega.Succeed()) + }) + + ginkgo.It("should reach a http server in the VM using the tunneling of the daemon", func() { + httpClient := &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + conn, err := net.Dial("unix", sock) + if err != nil { + return nil, err + } + return conn, transport.Tunnel(conn, "192.168.127.2", 8080) + }, + }, + } + + gomega.Eventually(func(g gomega.Gomega) { + resp, err := httpClient.Get("http://placeholder/") + g.Expect(err).ShouldNot(gomega.HaveOccurred()) + g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK)) + }).Should(gomega.Succeed()) + }) + + ginkgo.It("should reach a http server in the VM using dynamic port forwarding configured within the VM", func() { + _, err := net.Dial("tcp", "127.0.0.1:9090") + gomega.Expect(err).Should(gomega.HaveOccurred()) + gomega.Expect(err.Error()).To(gomega.HaveSuffix("connection refused")) + + _, err = sshExec(`curl http://gateway.containers.internal/services/forwarder/expose -X POST -d'{"local":":9090", "remote":":8080"}'`) + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + + gomega.Eventually(func(g gomega.Gomega) { + resp, err := http.Get("http://127.0.0.1:9090") + g.Expect(err).ShouldNot(gomega.HaveOccurred()) + g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK)) + }).Should(gomega.Succeed()) + + _, err = sshExec(`curl http://gateway.containers.internal/services/forwarder/unexpose -X POST -d'{"local":":9090"}'`) + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + + gomega.Eventually(func(g gomega.Gomega) { + _, err = net.Dial("tcp", "127.0.0.1:9090") + g.Expect(err).Should(gomega.HaveOccurred()) + g.Expect(err.Error()).To(gomega.HaveSuffix("connection refused")) + }).Should(gomega.Succeed()) + }) + + ginkgo.It("should reach rootless podman API using unix socket forwarding over ssh", func() { + httpClient := &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return net.Dial("unix", forwardSock) + }, + }, + } + + gomega.Eventually(func(g gomega.Gomega) { + resp, err := httpClient.Get("http://host/_ping") + g.Expect(err).ShouldNot(gomega.HaveOccurred()) + g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK)) + g.Expect(resp.ContentLength).To(gomega.Equal(int64(2))) + + reply := make([]byte, resp.ContentLength) + _, err = io.ReadAtLeast(resp.Body, reply, len(reply)) + + g.Expect(err).ShouldNot(gomega.HaveOccurred()) + g.Expect(string(reply)).To(gomega.Equal("OK")) + }).Should(gomega.Succeed()) + }) + + ginkgo.It("should reach rootful podman API using unix socket forwarding over ssh", func() { + httpClient := &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return net.Dial("unix", forwardRootSock) + }, + }, + } + + gomega.Eventually(func(g gomega.Gomega) { + resp, err := httpClient.Get("http://host/_ping") + g.Expect(err).ShouldNot(gomega.HaveOccurred()) + g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK)) + g.Expect(resp.ContentLength).To(gomega.Equal(int64(2))) + + reply := make([]byte, resp.ContentLength) + _, err = io.ReadAtLeast(resp.Body, reply, len(reply)) + + g.Expect(err).ShouldNot(gomega.HaveOccurred()) + g.Expect(string(reply)).To(gomega.Equal("OK")) + }).Should(gomega.Succeed()) + }) + + ginkgo.It("should expose and reach an http service using unix to tcp forwarding", func() { + if runtime.GOOS == "windows" { + ginkgo.Skip("AF_UNIX not supported on Windows") + } + + unix2tcpfwdsock, _ := filepath.Abs(filepath.Join(tmpDir, "podman-unix-to-unix-forwarding.sock")) + + out, err := sshExec(`curl http://gateway.containers.internal/services/forwarder/expose -X POST -d'{"protocol":"unix","local":"` + unix2tcpfwdsock + `","remote":"tcp://192.168.127.2:8080"}'`) + gomega.Expect(string(out)).Should(gomega.Equal("")) + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + + gomega.Eventually(func(g gomega.Gomega) { + sockfile, err := os.Stat(unix2tcpfwdsock) + g.Expect(err).ShouldNot(gomega.HaveOccurred()) + g.Expect(sockfile.Mode().Type().String()).To(gomega.Equal(os.ModeSocket.String())) + }).Should(gomega.Succeed()) + + httpClient := &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return net.Dial("unix", unix2tcpfwdsock) + }, + }, + } + + gomega.Eventually(func(g gomega.Gomega) { + resp, err := httpClient.Get("http://placeholder/") + g.Expect(err).ShouldNot(gomega.HaveOccurred()) + g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK)) + }).Should(gomega.Succeed()) + }) + + ginkgo.It("should expose and reach rootless podman API using unix to unix forwarding over ssh", func() { + if runtime.GOOS == "windows" { + ginkgo.Skip("AF_UNIX not supported on Windows") + } + + unix2unixfwdsock, _ := filepath.Abs(filepath.Join(tmpDir, "podman-unix-to-unix-forwarding.sock")) + + remoteuri := fmt.Sprintf(`ssh-tunnel://root@%s:%d%s?key=%s`, "192.168.127.2", 22, podmanSock, privateKeyFile) + _, err := sshExec(`curl http://192.168.127.1/services/forwarder/expose -X POST -d'{"protocol":"unix","local":"` + unix2unixfwdsock + `","remote":"` + remoteuri + `"}'`) + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + + gomega.Eventually(func(g gomega.Gomega) { + sockfile, err := os.Stat(unix2unixfwdsock) + g.Expect(err).ShouldNot(gomega.HaveOccurred()) + g.Expect(sockfile.Mode().Type().String()).To(gomega.Equal(os.ModeSocket.String())) + }).Should(gomega.Succeed()) + + httpClient := &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return net.Dial("unix", unix2unixfwdsock) + }, + }, + } + + gomega.Eventually(func(g gomega.Gomega) { + resp, err := httpClient.Get("http://host/_ping") + g.Expect(err).ShouldNot(gomega.HaveOccurred()) + g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK)) + g.Expect(resp.ContentLength).To(gomega.Equal(int64(2))) + + reply := make([]byte, resp.ContentLength) + _, err = io.ReadAtLeast(resp.Body, reply, len(reply)) + + g.Expect(err).ShouldNot(gomega.HaveOccurred()) + g.Expect(string(reply)).To(gomega.Equal("OK")) + }).Should(gomega.Succeed()) + }) +}) diff --git a/test/pull.go b/test/pull.go new file mode 100644 index 0000000..f1fa236 --- /dev/null +++ b/test/pull.go @@ -0,0 +1,81 @@ +package e2e + +import ( + "fmt" + "io" + "net/http" + "os" + "os/exec" + "strings" + + "github.com/sirupsen/logrus" +) + +// DownloadVMImage downloads a VM image from url to given path +// with download status +func DownloadVMImage(downloadURL string, localImagePath string) error { + fmt.Println("Downloading VM image: " + downloadURL) + + out, err := os.Create(localImagePath) + if err != nil { + return err + } + defer func() { + if err := out.Close(); err != nil { + logrus.Error(err) + } + }() + + // #nosec + resp, err := http.Get(downloadURL) + if err != nil { + return err + } + defer func() { + if err := resp.Body.Close(); err != nil { + logrus.Error(err) + } + }() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("error downloading VM image %s: %s", downloadURL, resp.Status) + } + + if _, err := io.Copy(out, resp.Body); err != nil { + return err + } + + return nil +} + +func Decompress(localPath, uncompressedPath string) error { + uncompressedFileWriter, err := os.OpenFile(uncompressedPath, os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + return err + } + + if !strings.HasSuffix(localPath, ".xz") { + return fmt.Errorf("unsupported compression for %s", localPath) + } + + fmt.Printf("Extracting %s\n", localPath) + return decompressXZ(localPath, uncompressedFileWriter) +} + +// Will error out if file without .xz already exists +// Maybe extracting then renameing is a good idea here.. +// depends on xz: not pre-installed on mac, so it becomes a brew dependency +func decompressXZ(src string, output io.Writer) error { + cmd := exec.Command("xzcat", "-T0", "-k", src) + stdOut, err := cmd.StdoutPipe() + if err != nil { + return err + } + cmd.Stderr = os.Stderr + go func() { + if _, err := io.Copy(output, stdOut); err != nil { + logrus.Error(err) + } + }() + return cmd.Run() +} diff --git a/test/suite_test.go b/test/suite_test.go new file mode 100644 index 0000000..5c08d5f --- /dev/null +++ b/test/suite_test.go @@ -0,0 +1,242 @@ +package e2e + +import ( + "flag" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "testing" + "time" + + "github.com/onsi/ginkgo" + "github.com/onsi/gomega" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +func TestSuite(t *testing.T) { + gomega.RegisterFailHandler(ginkgo.Fail) + ginkgo.RunSpecs(t, "gvisor-tap-vsock suite") +} + +const ( + sock = "/tmp/gvproxy-api.sock" + qemuPort = 5555 + sshPort = 2222 + ignitionUser = "test" + qconLog = "qcon.log" + podmanSock = "/run/user/1001/podman/podman.sock" + podmanRootSock = "/run/podman/podman.sock" + + // #nosec "test" (for manual usage) + ignitionPasswordHash = "$y$j9T$TqJWt3/mKJbH0sYi6B/LD1$QjVRuUgntjTHjAdAkqhkr4F73m.Be4jBXdAaKw98sPC" +) + +var ( + tmpDir string + binDir string + host *exec.Cmd + client *exec.Cmd + privateKeyFile string + publicKeyFile string + ignFile string + forwardSock string + forwardRootSock string +) + +func init() { + flag.StringVar(&tmpDir, "tmpDir", "../tmp", "temporary working directory") + flag.StringVar(&binDir, "bin", "../bin", "directory with compiled binaries") + privateKeyFile = filepath.Join(tmpDir, "id_test") + publicKeyFile = privateKeyFile + ".pub" + ignFile = filepath.Join(tmpDir, "test.ign") + forwardSock = filepath.Join(tmpDir, "podman-remote.sock") + forwardRootSock = filepath.Join(tmpDir, "podman-root-remote.sock") + +} + +var _ = ginkgo.BeforeSuite(func() { + gomega.Expect(os.MkdirAll(filepath.Join(tmpDir, "disks"), os.ModePerm)).Should(gomega.Succeed()) + + downloader, err := NewFcosDownloader(filepath.Join(tmpDir, "disks")) + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + qemuImage, err := downloader.DownloadImage() + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + + publicKey, err := createSSHKeys() + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + + err = CreateIgnition(ignFile, publicKey, ignitionUser, ignitionPasswordHash) + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + +outer: + for panics := 0; ; panics++ { + _ = os.Remove(sock) + + // #nosec + host = exec.Command(filepath.Join(binDir, "gvproxy"), fmt.Sprintf("--listen=unix://%s", sock), fmt.Sprintf("--listen-qemu=tcp://127.0.0.1:%d", qemuPort), + fmt.Sprintf("--forward-sock=%s", forwardSock), fmt.Sprintf("--forward-dest=%s", podmanSock), fmt.Sprintf("--forward-user=%s", ignitionUser), + fmt.Sprintf("--forward-identity=%s", privateKeyFile), + fmt.Sprintf("--forward-sock=%s", forwardRootSock), fmt.Sprintf("--forward-dest=%s", podmanRootSock), fmt.Sprintf("--forward-user=%s", "root"), + fmt.Sprintf("--forward-identity=%s", privateKeyFile)) + + host.Stderr = os.Stderr + host.Stdout = os.Stdout + gomega.Expect(host.Start()).Should(gomega.Succeed()) + go func() { + if err := host.Wait(); err != nil { + log.Error(err) + } + }() + + for { + _, err := os.Stat(sock) + if os.IsNotExist(err) { + log.Info("waiting for socket") + time.Sleep(100 * time.Millisecond) + continue + } + break + } + + template := `%s -m 2048 -nographic -serial file:%s -snapshot -drive if=virtio,file=%s -fw_cfg name=opt/com.coreos/config,file=%s -netdev socket,id=vlan,connect=127.0.0.1:%d -device virtio-net-pci,netdev=vlan,mac=5a:94:ef:e4:0c:ee` + // #nosec + client = exec.Command(qemuExecutable(), strings.Split(fmt.Sprintf(template, qemuArgs(), qconLog, qemuImage, ignFile, qemuPort), " ")...) + client.Stderr = os.Stderr + client.Stdout = os.Stdout + gomega.Expect(client.Start()).Should(gomega.Succeed()) + go func() { + if err := client.Wait(); err != nil { + log.Error(err) + } + }() + + for { + _, err := sshExec("whoami") + if err == nil { + break outer + } + + // Check for panic + didPanic, err := panicCheck(qconLog) + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + + if didPanic { + gomega.Expect(panics).ToNot(gomega.BeNumerically(">", 15), "No more than 15 panics allowed") + log.Info("Detected Kernel panic, retrying...") + _ = client.Process.Kill() + _ = host.Process.Kill() + _ = os.Remove(qconLog) + continue outer + } + + log.Infof("waiting for client to connect: %v", err) + time.Sleep(time.Second) + } + } + + err = scp(filepath.Join(binDir, "test-companion"), "/tmp/test-companion") + gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) + + // start an embedded DNS and http server in the VM. Wait a bit for the server to start. + cmd := sshCommand("sudo /tmp/test-companion") + gomega.Expect(cmd.Start()).ShouldNot(gomega.HaveOccurred()) + time.Sleep(5 * time.Second) +}) + +func qemuExecutable() string { + if runtime.GOOS == "darwin" { + return "qemu-system-x86_64" + } + return "qemu-kvm" +} + +func qemuArgs() string { + if runtime.GOOS == "darwin" { + return "-machine q35,accel=hvf:tcg -smp 4 -cpu host" + } + return "-cpu host" +} + +func createSSHKeys() (string, error) { + _ = os.Remove(publicKeyFile) + _ = os.Remove(privateKeyFile) + err := exec.Command("ssh-keygen", "-N", "", "-t", "ed25519", "-f", privateKeyFile).Run() + if err != nil { + return "", errors.Wrap(err, "Could not generate ssh keys") + } + + return readPublicKey() +} + +func readPublicKey() (string, error) { + publicKey, err := os.ReadFile(publicKeyFile) + if err != nil { + return "", nil + } + + return strings.TrimSpace(string(publicKey)), nil +} + +func scp(src, dst string) error { + sshCmd := exec.Command("scp", + "-o", "UserKnownHostsFile=/dev/null", + "-o", "StrictHostKeyChecking=no", + "-i", privateKeyFile, + "-P", strconv.Itoa(sshPort), + src, + fmt.Sprintf("%s@127.0.0.1:%s", ignitionUser, dst)) // #nosec G204 + sshCmd.Stderr = os.Stderr + sshCmd.Stdout = os.Stdout + return sshCmd.Run() +} + +func sshExec(cmd ...string) ([]byte, error) { + return sshCommand(cmd...).Output() +} + +func sshCommand(cmd ...string) *exec.Cmd { + sshCmd := exec.Command("ssh", + "-o", "UserKnownHostsFile=/dev/null", + "-o", "StrictHostKeyChecking=no", + "-i", privateKeyFile, + "-p", strconv.Itoa(sshPort), + fmt.Sprintf("%s@127.0.0.1", ignitionUser), "--", strings.Join(cmd, " ")) // #nosec G204 + return sshCmd +} + +func panicCheck(con string) (bool, error) { + file, err := os.Open(con) + if err != nil { + return false, err + } + + _, _ = file.Seek(-500, io.SeekEnd) + // Ignore seek errors (not enough content yet) + + contents := make([]byte, 500) + _, err = io.ReadAtLeast(file, contents, len(contents)) + if err != nil && err != io.ErrUnexpectedEOF { + return false, err + } + + return strings.Contains(string(contents), "end Kernel panic"), nil +} + +var _ = ginkgo.AfterSuite(func() { + if host != nil { + if err := host.Process.Kill(); err != nil { + log.Error(err) + } + } + if client != nil { + if err := client.Process.Kill(); err != nil { + log.Error(err) + } + } +}) diff --git a/test/wsl.sh b/test/wsl.sh new file mode 100755 index 0000000..a28daf5 --- /dev/null +++ b/test/wsl.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -x + +./bin/gvforwarder \ + -url="stdio:$(pwd)/bin/gvproxy-windows.exe?listen-stdio=accept&debug=true" \ + -iface="eth1" \ + -stop-if-exist="" \ + -debug diff --git a/tools/bin/.gitignore b/tools/bin/.gitignore new file mode 100644 index 0000000..d243069 --- /dev/null +++ b/tools/bin/.gitignore @@ -0,0 +1 @@ +makefat* diff --git a/tools/dummy.go b/tools/dummy.go new file mode 100644 index 0000000..1c7e8ab --- /dev/null +++ b/tools/dummy.go @@ -0,0 +1,9 @@ +// This file is not meant to be built, it's just a placeholder to ensure +// the source for the various tools that we use is properly referenced in go.mod +// and vendored in vendor/ +package buildtools + +import ( + _ "github.com/golangci/golangci-lint/cmd/golangci-lint" + _ "github.com/randall77/makefat" +) diff --git a/tools/go.mod b/tools/go.mod new file mode 100644 index 0000000..7b0b5a9 --- /dev/null +++ b/tools/go.mod @@ -0,0 +1,192 @@ +module github.com/containers/gvisor-tap-vsock/tools + +go 1.20 + +require ( + github.com/golangci/golangci-lint v1.55.2 + github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 +) + +require ( + 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect + 4d63.com/gochecknoglobals v0.2.1 // indirect + github.com/4meepo/tagalign v1.3.3 // indirect + github.com/Abirdcfly/dupword v0.0.13 // indirect + github.com/Antonboom/errname v0.1.12 // indirect + github.com/Antonboom/nilnil v0.1.7 // indirect + github.com/Antonboom/testifylint v0.2.3 // indirect + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect + github.com/GaijinEntertainment/go-exhaustruct/v3 v3.1.0 // indirect + github.com/Masterminds/semver v1.5.0 // indirect + github.com/OpenPeeDeeP/depguard/v2 v2.1.0 // indirect + github.com/alecthomas/go-check-sumtype v0.1.3 // indirect + github.com/alexkohler/nakedret/v2 v2.0.2 // indirect + github.com/alexkohler/prealloc v1.0.0 // indirect + github.com/alingse/asasalint v0.0.11 // indirect + github.com/ashanbrown/forbidigo v1.6.0 // indirect + github.com/ashanbrown/makezero v1.1.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bkielbasa/cyclop v1.2.1 // indirect + github.com/blizzy78/varnamelen v0.8.0 // indirect + github.com/bombsimon/wsl/v3 v3.4.0 // indirect + github.com/breml/bidichk v0.2.7 // indirect + github.com/breml/errchkjson v0.3.6 // indirect + github.com/butuzov/ireturn v0.2.2 // indirect + github.com/butuzov/mirror v1.1.0 // indirect + github.com/catenacyber/perfsprint v0.2.0 // indirect + github.com/ccojocar/zxcvbn-go v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/charithe/durationcheck v0.0.10 // indirect + github.com/chavacava/garif v0.1.0 // indirect + github.com/curioswitch/go-reassign v0.2.0 // indirect + github.com/daixiang0/gci v0.11.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/denis-tingaikin/go-header v0.4.3 // indirect + github.com/esimonov/ifshort v1.0.4 // indirect + github.com/ettle/strcase v0.1.1 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/fatih/structtag v1.2.0 // indirect + github.com/firefart/nonamedreturns v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/fzipp/gocyclo v0.6.0 // indirect + github.com/ghostiam/protogetter v0.2.3 // indirect + github.com/go-critic/go-critic v0.9.0 // indirect + github.com/go-toolsmith/astcast v1.1.0 // indirect + github.com/go-toolsmith/astcopy v1.1.0 // indirect + github.com/go-toolsmith/astequal v1.1.0 // indirect + github.com/go-toolsmith/astfmt v1.1.0 // indirect + github.com/go-toolsmith/astp v1.1.0 // indirect + github.com/go-toolsmith/strparse v1.1.0 // indirect + github.com/go-toolsmith/typep v1.1.0 // indirect + github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/gofrs/flock v0.8.1 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 // indirect + github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect + github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe // indirect + github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e // indirect + github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 // indirect + github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca // indirect + github.com/golangci/misspell v0.4.1 // indirect + github.com/golangci/revgrep v0.5.2 // indirect + github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601 // indirect + github.com/gostaticanalysis/analysisutil v0.7.1 // indirect + github.com/gostaticanalysis/comment v1.4.2 // indirect + github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect + github.com/gostaticanalysis/nilerr v0.1.1 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hexops/gotextdiff v1.0.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jgautheron/goconst v1.6.0 // indirect + github.com/jingyugao/rowserrcheck v1.1.1 // indirect + github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect + github.com/julz/importas v0.1.0 // indirect + github.com/kisielk/errcheck v1.6.3 // indirect + github.com/kisielk/gotool v1.0.0 // indirect + github.com/kkHAIKE/contextcheck v1.1.4 // indirect + github.com/kulti/thelper v0.6.3 // indirect + github.com/kunwardeep/paralleltest v1.0.8 // indirect + github.com/kyoh86/exportloopref v0.1.11 // indirect + github.com/ldez/gomoddirectives v0.2.3 // indirect + github.com/ldez/tagliatelle v0.5.0 // indirect + github.com/leonklingele/grouper v1.1.1 // indirect + github.com/lufeee/execinquery v1.2.1 // indirect + github.com/macabu/inamedparam v0.1.2 // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/maratori/testableexamples v1.0.0 // indirect + github.com/maratori/testpackage v1.1.1 // indirect + github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/mbilski/exhaustivestruct v1.2.0 // indirect + github.com/mgechev/revive v1.3.4 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moricho/tparallel v0.3.1 // indirect + github.com/nakabonne/nestif v0.3.1 // indirect + github.com/nishanths/exhaustive v0.11.0 // indirect + github.com/nishanths/predeclared v0.2.2 // indirect + github.com/nunnatsa/ginkgolinter v0.14.1 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/polyfloyd/go-errorlint v1.4.5 // indirect + github.com/prometheus/client_golang v1.12.1 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.32.1 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/quasilyte/go-ruleguard v0.4.0 // indirect + github.com/quasilyte/gogrep v0.5.0 // indirect + github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect + github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect + github.com/ryancurrah/gomodguard v1.3.0 // indirect + github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect + github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect + github.com/sashamelentyev/interfacebloat v1.1.0 // indirect + github.com/sashamelentyev/usestdlibvars v1.24.0 // indirect + github.com/securego/gosec/v2 v2.18.2 // indirect + github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sivchari/containedctx v1.0.3 // indirect + github.com/sivchari/nosnakecase v1.7.0 // indirect + github.com/sivchari/tenv v1.7.1 // indirect + github.com/sonatard/noctx v0.0.2 // indirect + github.com/sourcegraph/go-diff v0.7.0 // indirect + github.com/spf13/afero v1.8.2 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.12.0 // indirect + github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect + github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/subosito/gotenv v1.4.1 // indirect + github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect + github.com/tdakkota/asciicheck v0.2.0 // indirect + github.com/tetafro/godot v1.4.15 // indirect + github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect + github.com/timonwong/loggercheck v0.9.4 // indirect + github.com/tomarrell/wrapcheck/v2 v2.8.1 // indirect + github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect + github.com/ultraware/funlen v0.1.0 // indirect + github.com/ultraware/whitespace v0.0.5 // indirect + github.com/uudashr/gocognit v1.1.2 // indirect + github.com/xen0n/gosmopolitan v1.2.2 // indirect + github.com/yagipy/maintidx v1.0.0 // indirect + github.com/yeya24/promlinter v0.2.0 // indirect + github.com/ykadowak/zerologlint v0.1.3 // indirect + gitlab.com/bosi/decorder v0.4.1 // indirect + go-simpler.org/sloglint v0.1.2 // indirect + go.tmz.dev/musttag v0.7.2 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect + golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833 // indirect + golang.org/x/mod v0.13.0 // indirect + golang.org/x/sync v0.4.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + golang.org/x/tools v0.14.0 // indirect + google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + honnef.co/go/tools v0.4.6 // indirect + mvdan.cc/gofumpt v0.5.0 // indirect + mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect + mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect + mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d // indirect +) diff --git a/tools/go.sum b/tools/go.sum new file mode 100644 index 0000000..71c4ac8 --- /dev/null +++ b/tools/go.sum @@ -0,0 +1,990 @@ +4d63.com/gocheckcompilerdirectives v1.2.1 h1:AHcMYuw56NPjq/2y615IGg2kYkBdTvOaojYCBcRE7MA= +4d63.com/gocheckcompilerdirectives v1.2.1/go.mod h1:yjDJSxmDTtIHHCqX0ufRYZDL6vQtMG7tJdKVeWwsqvs= +4d63.com/gochecknoglobals v0.2.1 h1:1eiorGsgHOFOuoOiJDy2psSrQbRdIHrlge0IJIkUgDc= +4d63.com/gochecknoglobals v0.2.1/go.mod h1:KRE8wtJB3CXCsb1xy421JfTHIIbmT3U5ruxw2Qu8fSU= +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.44.3/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.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +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/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= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/4meepo/tagalign v1.3.3 h1:ZsOxcwGD/jP4U/aw7qeWu58i7dwYemfy5Y+IF1ACoNw= +github.com/4meepo/tagalign v1.3.3/go.mod h1:Q9c1rYMZJc9dPRkbQPpcBNCLEmY2njbAsXhQOZFE2dE= +github.com/Abirdcfly/dupword v0.0.13 h1:SMS17YXypwP000fA7Lr+kfyBQyW14tTT+nRv9ASwUUo= +github.com/Abirdcfly/dupword v0.0.13/go.mod h1:Ut6Ue2KgF/kCOawpW4LnExT+xZLQviJPE4klBPMK/5Y= +github.com/Antonboom/errname v0.1.12 h1:oh9ak2zUtsLp5oaEd/erjB4GPu9w19NyoIskZClDcQY= +github.com/Antonboom/errname v0.1.12/go.mod h1:bK7todrzvlaZoQagP1orKzWXv59X/x0W0Io2XT1Ssro= +github.com/Antonboom/nilnil v0.1.7 h1:ofgL+BA7vlA1K2wNQOsHzLJ2Pw5B5DpWRLdDAVvvTow= +github.com/Antonboom/nilnil v0.1.7/go.mod h1:TP+ScQWVEq0eSIxqU8CbdT5DFWoHp0MbP+KMUO1BKYQ= +github.com/Antonboom/testifylint v0.2.3 h1:MFq9zyL+rIVpsvLX4vDPLojgN7qODzWsrnftNX2Qh60= +github.com/Antonboom/testifylint v0.2.3/go.mod h1:IYaXaOX9NbfAyO+Y04nfjGI8wDemC1rUyM/cYolz018= +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/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= +github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= +github.com/GaijinEntertainment/go-exhaustruct/v3 v3.1.0 h1:3ZBs7LAezy8gh0uECsA6CGU43FF3zsx5f4eah5FxTMA= +github.com/GaijinEntertainment/go-exhaustruct/v3 v3.1.0/go.mod h1:rZLTje5A9kFBe0pzhpe2TdhRniBF++PRHQuRpR8esVc= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/OpenPeeDeeP/depguard/v2 v2.1.0 h1:aQl70G173h/GZYhWf36aE5H0KaujXfVMnn/f1kSDVYY= +github.com/OpenPeeDeeP/depguard/v2 v2.1.0/go.mod h1:PUBgk35fX4i7JDmwzlJwJ+GMe6NfO1723wmJMgPThNQ= +github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= +github.com/alecthomas/go-check-sumtype v0.1.3 h1:M+tqMxB68hcgccRXBMVCPI4UJ+QUfdSx0xdbypKCqA8= +github.com/alecthomas/go-check-sumtype v0.1.3/go.mod h1:WyYPfhfkdhyrdaligV6svFopZV8Lqdzn5pyVBaV6jhQ= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alexkohler/nakedret/v2 v2.0.2 h1:qnXuZNvv3/AxkAb22q/sEsEpcA99YxLFACDtEw9TPxE= +github.com/alexkohler/nakedret/v2 v2.0.2/go.mod h1:2b8Gkk0GsOrqQv/gPWjNLDSKwG8I5moSXG1K4VIBcTQ= +github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= +github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= +github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= +github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= +github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= +github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= +github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= +github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bkielbasa/cyclop v1.2.1 h1:AeF71HZDob1P2/pRm1so9cd1alZnrpyc4q2uP2l0gJY= +github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM= +github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= +github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= +github.com/bombsimon/wsl/v3 v3.4.0 h1:RkSxjT3tmlptwfgEgTgU+KYKLI35p/tviNXNXiL2aNU= +github.com/bombsimon/wsl/v3 v3.4.0/go.mod h1:KkIB+TXkqy6MvK9BDZVbZxKNYsE1/oLRJbIFtf14qqo= +github.com/breml/bidichk v0.2.7 h1:dAkKQPLl/Qrk7hnP6P+E0xOodrq8Us7+U0o4UBOAlQY= +github.com/breml/bidichk v0.2.7/go.mod h1:YodjipAGI9fGcYM7II6wFvGhdMYsC5pHDlGzqvEW3tQ= +github.com/breml/errchkjson v0.3.6 h1:VLhVkqSBH96AvXEyclMR37rZslRrY2kcyq+31HCsVrA= +github.com/breml/errchkjson v0.3.6/go.mod h1:jhSDoFheAF2RSDOlCfhHO9KqhZgAYLyvHe7bRCX8f/U= +github.com/butuzov/ireturn v0.2.2 h1:jWI36dxXwVrI+RnXDwux2IZOewpmfv930OuIRfaBUJ0= +github.com/butuzov/ireturn v0.2.2/go.mod h1:RfGHUvvAuFFxoHKf4Z8Yxuh6OjlCw1KvR2zM1NFHeBk= +github.com/butuzov/mirror v1.1.0 h1:ZqX54gBVMXu78QLoiqdwpl2mgmoOJTk7s4p4o+0avZI= +github.com/butuzov/mirror v1.1.0/go.mod h1:8Q0BdQU6rC6WILDiBM60DBfvV78OLJmMmixe7GF45AE= +github.com/catenacyber/perfsprint v0.2.0 h1:azOocHLscPjqXVJ7Mf14Zjlkn4uNua0+Hcg1wTR6vUo= +github.com/catenacyber/perfsprint v0.2.0/go.mod h1:/wclWYompEyjUD2FuIIDVKNkqz7IgBIWXIH3V0Zol50= +github.com/ccojocar/zxcvbn-go v1.0.1 h1:+sxrANSCj6CdadkcMnvde/GWU1vZiiXRbqYSCalV4/4= +github.com/ccojocar/zxcvbn-go v1.0.1/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= +github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= +github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= +github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= +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/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo= +github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc= +github.com/daixiang0/gci v0.11.2 h1:Oji+oPsp3bQ6bNNgX30NBAVT18P4uBH4sRZnlOlTj7Y= +github.com/daixiang0/gci v0.11.2/go.mod h1:xtHP9N7AHdNvtRNfcx9gwTDfw7FRJx4bZUsiEfiNNAI= +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/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU= +github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c= +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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/esimonov/ifshort v1.0.4 h1:6SID4yGWfRae/M7hkVDVVyppy8q/v9OuxNdmjLQStBA= +github.com/esimonov/ifshort v1.0.4/go.mod h1:Pe8zjlRrJ80+q2CxHLfEOfTwxCZ4O+MuhcHcfgNWTk0= +github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw= +github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/firefart/nonamedreturns v1.0.4 h1:abzI1p7mAEPYuR4A+VLKn4eNDOycjYo2phmY9sfv40Y= +github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= +github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= +github.com/ghostiam/protogetter v0.2.3 h1:qdv2pzo3BpLqezwqfGDLZ+nHEYmc5bUpIdsMbBVwMjw= +github.com/ghostiam/protogetter v0.2.3/go.mod h1:KmNLOsy1v04hKbvZs8EfGI1fk39AgTdRDxWNYPfXVc4= +github.com/go-critic/go-critic v0.9.0 h1:Pmys9qvU3pSML/3GEQ2Xd9RZ/ip+aXHKILuxczKGV/U= +github.com/go-critic/go-critic v0.9.0/go.mod h1:5P8tdXL7m/6qnyG6oRAlYLORvoXH0WDypYgAEmagT40= +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-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= +github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= +github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= +github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw= +github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4= +github.com/go-toolsmith/astequal v1.1.0 h1:kHKm1AWqClYn15R0K1KKE4RG614D46n+nqUQ06E1dTw= +github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ= +github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco= +github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4= +github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA= +github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA= +github.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk= +github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= +github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw= +github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= +github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= +github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= +github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U= +github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +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/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.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0= +github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= +github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= +github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe h1:6RGUuS7EGotKx6J5HIP8ZtyMdiDscjMLfRBSPuzVVeo= +github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe/go.mod h1:gjqyPShc/m8pEMpk0a3SeagVb0kaqvhscv+i9jI5ZhQ= +github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e h1:ULcKCDV1LOZPFxGZaA6TlQbiM3J2GCPnkx/bGF6sX/g= +github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e/go.mod h1:Pm5KhLPA8gSnQwrQ6ukebRcapGb/BG9iUkdaiCcGHJM= +github.com/golangci/golangci-lint v1.55.2 h1:yllEIsSJ7MtlDBwDJ9IMBkyEUz2fYE0b5B8IUgO1oP8= +github.com/golangci/golangci-lint v1.55.2/go.mod h1:H60CZ0fuqoTwlTvnbyjhpZPWp7KmsjwV2yupIMiMXbM= +github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 h1:MfyDlzVjl1hoaPzPD4Gpb/QgoRfSBR0jdhwGyAWwMSA= +github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= +github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= +github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= +github.com/golangci/misspell v0.4.1 h1:+y73iSicVy2PqyX7kmUefHusENlrP9YwuHZHPLGQj/g= +github.com/golangci/misspell v0.4.1/go.mod h1:9mAN1quEo3DlpbaIKKyEvRxK1pwqR9s/Sea1bJCtlNI= +github.com/golangci/revgrep v0.5.2 h1:EndcWoRhcnfj2NHQ+28hyuXpLMF+dQmCN+YaeeIl4FU= +github.com/golangci/revgrep v0.5.2/go.mod h1:bjAMA+Sh/QUfTDcHzxfyHxr4xKvllVr/0sCv2e7jJHA= +github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys= +github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= +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.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/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +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/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601 h1:mrEEilTAUmaAORhssPPkxj84TsHrPMLBGW2Z4SoTxm8= +github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= +github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= +github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= +github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado= +github.com/gostaticanalysis/comment v1.4.2 h1:hlnx5+S2fY9Zo9ePo4AhgYsYHbM2+eAv8m/s1JiCd6Q= +github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= +github.com/gostaticanalysis/forcetypeassert v0.1.0 h1:6eUflI3DiGusXGK6X7cCcIgVCpZ2CiZ1Q7jl6ZxNV70= +github.com/gostaticanalysis/forcetypeassert v0.1.0/go.mod h1:qZEedyP/sY1lTGV1uJ3VhWZ2mqag3IkWsDHVbplHXak= +github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk= +github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= +github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= +github.com/gostaticanalysis/testutil v0.4.0 h1:nhdCmubdmDF6VEatUNjgUZBJKWRqugoISdUv3PPQgHY= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +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 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +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.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jgautheron/goconst v1.6.0 h1:gbMLWKRMkzAc6kYsQL6/TxaoBUg3Jm9LSF/Ih1ADWGA= +github.com/jgautheron/goconst v1.6.0/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= +github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= +github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= +github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af h1:KA9BjwUk7KlCh6S9EAGWBt1oExIUv9WyNCiRz5amv48= +github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/julz/importas v0.1.0 h1:F78HnrsjY3cR7j0etXy5+TU1Zuy7Xt08X/1aJnH5xXY= +github.com/julz/importas v0.1.0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= +github.com/kisielk/errcheck v1.6.3 h1:dEKh+GLHcWm2oN34nMvDzn1sqI0i0WxPvrgiJA5JuM8= +github.com/kisielk/errcheck v1.6.3/go.mod h1:nXw/i/MfnvRHqXa7XXmQMUB0oNFGuBrNI8d8NLy0LPw= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkHAIKE/contextcheck v1.1.4 h1:B6zAaLhOEEcjvUgIYEqystmnFk1Oemn8bvJhbt0GMb8= +github.com/kkHAIKE/contextcheck v1.1.4/go.mod h1:1+i/gWqokIa+dm31mqGLZhZJ7Uh44DJGZVmr6QRBNJg= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +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/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs= +github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= +github.com/kunwardeep/paralleltest v1.0.8 h1:Ul2KsqtzFxTlSU7IP0JusWlLiNqQaloB9vguyjbE558= +github.com/kunwardeep/paralleltest v1.0.8/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY= +github.com/kyoh86/exportloopref v0.1.11 h1:1Z0bcmTypkL3Q4k+IDHMWTcnCliEZcaPiIe0/ymEyhQ= +github.com/kyoh86/exportloopref v0.1.11/go.mod h1:qkV4UF1zGl6EkF1ox8L5t9SwyeBAZ3qLMd6up458uqA= +github.com/ldez/gomoddirectives v0.2.3 h1:y7MBaisZVDYmKvt9/l1mjNCiSA1BVn34U0ObUcJwlhA= +github.com/ldez/gomoddirectives v0.2.3/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0= +github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo= +github.com/ldez/tagliatelle v0.5.0/go.mod h1:rj1HmWiL1MiKQuOONhd09iySTEkUuE/8+5jtPYz9xa4= +github.com/leonklingele/grouper v1.1.1 h1:suWXRU57D4/Enn6pXR0QVqqWWrnJ9Osrz+5rjt8ivzU= +github.com/leonklingele/grouper v1.1.1/go.mod h1:uk3I3uDfi9B6PeUjsCKi6ndcf63Uy7snXgR4yDYQVDY= +github.com/lufeee/execinquery v1.2.1 h1:hf0Ems4SHcUGBxpGN7Jz78z1ppVkP/837ZlETPCEtOM= +github.com/lufeee/execinquery v1.2.1/go.mod h1:EC7DrEKView09ocscGHC+apXMIaorh4xqSxS/dy8SbM= +github.com/macabu/inamedparam v0.1.2 h1:RR5cnayM6Q7cDhQol32DE2BGAPGMnffJ31LFE+UklaU= +github.com/macabu/inamedparam v0.1.2/go.mod h1:Xg25QvY7IBRl1KLPV9Rbml8JOMZtF/iAkNkmV7eQgjw= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= +github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= +github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04= +github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc= +github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 h1:gWg6ZQ4JhDfJPqlo2srm/LN17lpybq15AryXIRcWYLE= +github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= +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.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mbilski/exhaustivestruct v1.2.0 h1:wCBmUnSYufAHO6J4AVWY6ff+oxWxsVFrwgOdMUQePUo= +github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= +github.com/mgechev/revive v1.3.4 h1:k/tO3XTaWY4DEHal9tWBkkUMJYO/dLDVyMmAQxmIMDc= +github.com/mgechev/revive v1.3.4/go.mod h1:W+pZCMu9qj8Uhfs1iJMQsEFLRozUfvwFwqVvRbSNLVw= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/moricho/tparallel v0.3.1 h1:fQKD4U1wRMAYNngDonW5XupoB/ZGJHdpzrWqgyg9krA= +github.com/moricho/tparallel v0.3.1/go.mod h1:leENX2cUv7Sv2qDgdi0D0fCftN8fRC67Bcn8pqzeYNI= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= +github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/nishanths/exhaustive v0.11.0 h1:T3I8nUGhl/Cwu5Z2hfc92l0e04D2GEW6e0l8pzda2l0= +github.com/nishanths/exhaustive v0.11.0/go.mod h1:RqwDsZ1xY0dNdqHho2z6X+bgzizwbLYOWnZbbl2wLB4= +github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= +github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= +github.com/nunnatsa/ginkgolinter v0.14.1 h1:khx0CqR5U4ghsscjJ+lZVthp3zjIFytRXPTaQ/TMiyA= +github.com/nunnatsa/ginkgolinter v0.14.1/go.mod h1:nY0pafUSst7v7F637e7fymaMlQqI9c0Wka2fGsDkzWg= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/gomega v1.28.1 h1:MijcGUbfYuznzK/5R4CPNoUP/9Xvuo20sXfEm6XxoTA= +github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= +github.com/otiai10/copy v1.11.0 h1:OKBD80J/mLBrwnzXqGtFCzprFSGioo30JcmR4APsNwc= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +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/polyfloyd/go-errorlint v1.4.5 h1:70YWmMy4FgRHehGNOUask3HtSFSOLKgmDn7ryNe7LqI= +github.com/polyfloyd/go-errorlint v1.4.5/go.mod h1:sIZEbFoDOCnTYYZoVkjc4hTnM459tuWA9H/EkdXwsKk= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/quasilyte/go-ruleguard v0.4.0 h1:DyM6r+TKL+xbKB4Nm7Afd1IQh9kEUKQs2pboWGKtvQo= +github.com/quasilyte/go-ruleguard v0.4.0/go.mod h1:Eu76Z/R8IXtViWUIHkE3p8gdH3/PKk1eh3YGfaEof10= +github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= +github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= +github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= +github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= +github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= +github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= +github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 h1:GranzK4hv1/pqTIhMTXt2X8MmMOuH3hMeUR0o9SP5yc= +github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844/go.mod h1:T1TLSfyWVBRXVGzWd0o9BI4kfoO9InEgfQe4NV3mLz8= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryancurrah/gomodguard v1.3.0 h1:q15RT/pd6UggBXVBuLps8BXRvl5GPBcwVA7BJHMLuTw= +github.com/ryancurrah/gomodguard v1.3.0/go.mod h1:ggBxb3luypPEzqVtq33ee7YSN35V28XeGnid8dnni50= +github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= +github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= +github.com/sanposhiho/wastedassign/v2 v2.0.7 h1:J+6nrY4VW+gC9xFzUc+XjPD3g3wF3je/NsJFwFK7Uxc= +github.com/sanposhiho/wastedassign/v2 v2.0.7/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI= +github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= +github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= +github.com/sashamelentyev/usestdlibvars v1.24.0 h1:MKNzmXtGh5N0y74Z/CIaJh4GlB364l0K1RUT08WSWAc= +github.com/sashamelentyev/usestdlibvars v1.24.0/go.mod h1:9cYkq+gYJ+a5W2RPdhfaSCnTVUC1OQP/bSiiBhq3OZE= +github.com/securego/gosec/v2 v2.18.2 h1:DkDt3wCiOtAHf1XkiXZBhQ6m6mK/b9T/wD257R3/c+I= +github.com/securego/gosec/v2 v2.18.2/go.mod h1:xUuqSF6i0So56Y2wwohWAmB07EdBkUN6crbLlHwbyJs= +github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c h1:W65qqJCIOVP4jpqPQ0YvHYKwcMEMVWIzWC5iNQQfBTU= +github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c/go.mod h1:/PevMnwAxekIXwN8qQyfc5gl2NlkB3CQlkizAbOkeBs= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= +github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= +github.com/sivchari/nosnakecase v1.7.0 h1:7QkpWIRMe8x25gckkFd2A5Pi6Ymo0qgr4JrhGt95do8= +github.com/sivchari/nosnakecase v1.7.0/go.mod h1:CwDzrzPea40/GB6uynrNLiorAlgFRvRbFSgJx2Gs+QY= +github.com/sivchari/tenv v1.7.1 h1:PSpuD4bu6fSmtWMxSGWcvqUUgIn7k3yOJhOIzVWn8Ak= +github.com/sivchari/tenv v1.7.1/go.mod h1:64yStXKSOxDfX47NlhVwND4dHwfZDdbp2Lyl018Icvg= +github.com/sonatard/noctx v0.0.2 h1:L7Dz4De2zDQhW8S0t+KUjY0MAQJd6SgVwhzNIc4ok00= +github.com/sonatard/noctx v0.0.2/go.mod h1:kzFz+CzWSjQ2OzIm46uJZoXuBpa2+0y3T36U18dWqIo= +github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= +github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= +github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= +github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= +github.com/stbenjam/no-sprintf-host-port v0.1.1 h1:tYugd/yrm1O0dV+ThCbaKZh195Dfm07ysF0U6JQXczc= +github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8LHsN9N74I+PhRquPsxpL0I= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +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.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.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= +github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c h1:+aPplBwWcHBo6q9xrfWdMrT9o4kltkmmvpemgIjep/8= +github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c/go.mod h1:SbErYREK7xXdsRiigaQiQkI9McGRzYMvlKYaP3Nimdk= +github.com/tdakkota/asciicheck v0.2.0 h1:o8jvnUANo0qXtnslk2d3nMKTFNlOnJjRrNcj0j9qkHM= +github.com/tdakkota/asciicheck v0.2.0/go.mod h1:Qb7Y9EgjCLJGup51gDHFzbI08/gbGhL/UVhYIPWG2rg= +github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= +github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= +github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= +github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= +github.com/tetafro/godot v1.4.15 h1:QzdIs+XB8q+U1WmQEWKHQbKmCw06QuQM7gLx/dky2RM= +github.com/tetafro/godot v1.4.15/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= +github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 h1:quvGphlmUVU+nhpFa4gg4yJyTRJ13reZMDHrKwYw53M= +github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966/go.mod h1:27bSVNWSBOHm+qRp1T9qzaIpsWEP6TbUnei/43HK+PQ= +github.com/timonwong/loggercheck v0.9.4 h1:HKKhqrjcVj8sxL7K77beXh0adEm6DLjV/QOGeMXEVi4= +github.com/timonwong/loggercheck v0.9.4/go.mod h1:caz4zlPcgvpEkXgVnAJGowHAMW2NwHaNlpS8xDbVhTg= +github.com/tomarrell/wrapcheck/v2 v2.8.1 h1:HxSqDSN0sAt0yJYsrcYVoEeyM4aI9yAm3KQpIXDJRhQ= +github.com/tomarrell/wrapcheck/v2 v2.8.1/go.mod h1:/n2Q3NZ4XFT50ho6Hbxg+RV1uyo2Uow/Vdm9NQcl5SE= +github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= +github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= +github.com/ultraware/funlen v0.1.0 h1:BuqclbkY6pO+cvxoq7OsktIXZpgBSkYTQtmwhAK81vI= +github.com/ultraware/funlen v0.1.0/go.mod h1:XJqmOQja6DpxarLj6Jj1U7JuoS8PvL4nEqDaQhy22p4= +github.com/ultraware/whitespace v0.0.5 h1:hh+/cpIcopyMYbZNVov9iSxvJU3OYQg78Sfaqzi/CzI= +github.com/ultraware/whitespace v0.0.5/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA= +github.com/uudashr/gocognit v1.1.2 h1:l6BAEKJqQH2UpKAPKdMfZf5kE4W/2xk8pfU1OVLvniI= +github.com/uudashr/gocognit v1.1.2/go.mod h1:aAVdLURqcanke8h3vg35BC++eseDm66Z7KmchI5et4k= +github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= +github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= +github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= +github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= +github.com/yeya24/promlinter v0.2.0 h1:xFKDQ82orCU5jQujdaD8stOHiv8UN68BSdn2a8u8Y3o= +github.com/yeya24/promlinter v0.2.0/go.mod h1:u54lkmBOZrpEbQQ6gox2zWKKLKu2SGe+2KOiextY+IA= +github.com/ykadowak/zerologlint v0.1.3 h1:TLy1dTW3Nuc+YE3bYRPToG1Q9Ej78b5UUN6bjbGdxPE= +github.com/ykadowak/zerologlint v0.1.3/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= +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.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +gitlab.com/bosi/decorder v0.4.1 h1:VdsdfxhstabyhZovHafFw+9eJ6eU0d2CkFNJcZz/NU4= +gitlab.com/bosi/decorder v0.4.1/go.mod h1:jecSqWUew6Yle1pCr2eLWTensJMmsxHsBwt+PVbkAqA= +go-simpler.org/assert v0.6.0 h1:QxSrXa4oRuo/1eHMXSBFHKvJIpWABayzKldqZyugG7E= +go-simpler.org/sloglint v0.1.2 h1:IjdhF8NPxyn0Ckn2+fuIof7ntSnVUAqBFcQRrnG9AiM= +go-simpler.org/sloglint v0.1.2/go.mod h1:2LL+QImPfTslD5muNPydAEYmpXIj6o/WYcqnJjLi4o4= +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.tmz.dev/musttag v0.7.2 h1:1J6S9ipDbalBSODNT5jCep8dhZyMr4ttnjQagmGYR5s= +go.tmz.dev/musttag v0.7.2/go.mod h1:m6q5NiiSKMnQYokefa2xGoyoXnrswCbJ0AWYzf4Zs28= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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-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-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +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-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/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4= +golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833 h1:jWGQJV4niP+CCmFW9ekjA9Zx8vYORzOUH2/Nl5WPuLQ= +golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +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/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/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/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.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.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-20181114220301-adae6a3d119a/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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-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-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +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-20210514164344-f6687ab2804c/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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-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-20191001151750-bb3f8db39f24/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-20200106162015-b016eb3dc98e/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-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-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-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/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-20210603081109-ebe580a85c40/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-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +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/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-20190321232350-e250d351ecad/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-20190910044552-dd2b5c81c578/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-20191108193012-7d206e10da11/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-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +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-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +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-20200820010801-b793a1359eac/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-20201001104356-43ebab892c4c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +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-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= +golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +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/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-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-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +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.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/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= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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= +honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8= +honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= +mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E= +mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= +mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= +mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= +mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d h1:3rvTIIM22r9pvXk+q3swxUQAQOxksVMGK7sml4nG57w= +mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d/go.mod h1:IeHQjmn6TOD+e4Z3RFiZMMsLVL+A96Nvptar8Fj71is= +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/tools/tools.mk b/tools/tools.mk new file mode 100644 index 0000000..b4cd37a --- /dev/null +++ b/tools/tools.mk @@ -0,0 +1,10 @@ +TOOLS_BINDIR = $(realpath $(TOOLS_DIR)/bin) + +$(TOOLS_BINDIR)/makefat: $(TOOLS_DIR)/go.mod + cd $(TOOLS_DIR) && GOBIN="$(TOOLS_BINDIR)" go install github.com/randall77/makefat + +$(TOOLS_BINDIR)/golangci-lint: $(TOOLS_DIR)/go.mod + cd $(TOOLS_DIR) && GOBIN="$(TOOLS_BINDIR)" go install github.com/golangci/golangci-lint/cmd/golangci-lint + +$(TOOLS_BINDIR)/gomod2rpmdeps: $(TOOLS_DIR)/go.mod + cd $(TOOLS_DIR) && GOBIN="$(TOOLS_BINDIR)" go install github.com/cfergeau/gomod2rpmdeps/cmd/gomod2rpmdeps |