summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 16:16:56 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 16:16:56 +0000
commit62e212dec2415aa605363d616c481c2a56e70737 (patch)
treeb5ace7061598be728afaf95a9a40054daf05ce19
parentInitial commit. (diff)
downloadgolang-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>
-rw-r--r--.dockerignore4
-rw-r--r--.gitattributes1
-rw-r--r--.github/dependabot.yml17
-rw-r--r--.github/workflows/go.yml83
-rw-r--r--.github/workflows/golangci-lint.yml21
-rw-r--r--.github/workflows/release.yml37
-rw-r--r--.gitignore4
-rw-r--r--.golangci.yml13
-rw-r--r--.packit.yaml62
-rw-r--r--CODE-OF-CONDUCT.md3
-rw-r--r--LICENSE202
-rw-r--r--Makefile69
-rw-r--r--OWNERS9
-rw-r--r--README.md205
-rw-r--r--SECURITY.md3
-rw-r--r--cmd/gvproxy/main.go567
-rw-r--r--cmd/qemu-wrapper/README.md6
-rw-r--r--cmd/qemu-wrapper/main.go27
-rw-r--r--cmd/ssh-over-vsock/client.go66
-rw-r--r--cmd/ssh-over-vsock/main.go64
-rw-r--r--cmd/test-companion/main.go53
-rw-r--r--cmd/vm/main_linux.go222
-rw-r--r--cmd/win-sshproxy/event-hook.go73
-rw-r--r--cmd/win-sshproxy/main.go196
-rw-r--r--contrib/networkmanager/vsock0.nmconnection15
-rw-r--r--contrib/systemd/gv-user-network@.service13
-rw-r--r--doc/curl.pngbin0 -> 13363 bytes
-rw-r--r--doc/http.pngbin0 -> 12966 bytes
-rw-r--r--go.mod49
-rw-r--r--go.sum202
-rw-r--r--images/Dockerfile10
-rw-r--r--images/udhcpc.sh68
-rw-r--r--pkg/client/client.go119
-rw-r--r--pkg/fs/umask_unix.go10
-rw-r--r--pkg/fs/umask_windows.go8
-rw-r--r--pkg/net/stdio/dial.go51
-rw-r--r--pkg/net/stdio/ioaddr.go12
-rw-r--r--pkg/net/stdio/ioconn.go50
-rw-r--r--pkg/services/dhcp/dhcp.go127
-rw-r--r--pkg/services/dns/dns.go269
-rw-r--r--pkg/services/dns/dns_test.go194
-rw-r--r--pkg/services/forwarder/ports.go392
-rw-r--r--pkg/services/forwarder/tcp.go62
-rw-r--r--pkg/services/forwarder/udp.go43
-rw-r--r--pkg/services/forwarder/udp_proxy.go192
-rw-r--r--pkg/sshclient/bastion.go199
-rw-r--r--pkg/sshclient/npipe_unsupported.go14
-rw-r--r--pkg/sshclient/npipe_windows.go43
-rw-r--r--pkg/sshclient/ssh_forwarder.go279
-rw-r--r--pkg/tap/connection.go10
-rw-r--r--pkg/tap/ip_pool.go86
-rw-r--r--pkg/tap/ip_pool_test.go41
-rw-r--r--pkg/tap/link.go137
-rw-r--r--pkg/tap/protocols.go68
-rw-r--r--pkg/tap/switch.go296
-rw-r--r--pkg/transport/dial_darwin.go11
-rw-r--r--pkg/transport/dial_linux.go46
-rw-r--r--pkg/transport/listen.go26
-rw-r--r--pkg/transport/listen_darwin.go47
-rw-r--r--pkg/transport/listen_generic.go13
-rw-r--r--pkg/transport/listen_linux.go35
-rw-r--r--pkg/transport/listen_windows.go26
-rw-r--r--pkg/transport/tunnel.go28
-rw-r--r--pkg/transport/unixgram_darwin.go67
-rw-r--r--pkg/transport/unixgram_nondarwin.go17
-rw-r--r--pkg/types/configuration.go80
-rw-r--r--pkg/types/gvproxy_command.go197
-rw-r--r--pkg/types/handshake.go21
-rw-r--r--pkg/types/paths.go3
-rw-r--r--pkg/types/version.go74
-rw-r--r--pkg/virtualnetwork/bess.go12
-rw-r--r--pkg/virtualnetwork/conn.go69
-rw-r--r--pkg/virtualnetwork/mux.go100
-rw-r--r--pkg/virtualnetwork/qemu.go12
-rw-r--r--pkg/virtualnetwork/services.go123
-rw-r--r--pkg/virtualnetwork/stats.go31
-rw-r--r--pkg/virtualnetwork/stdio.go12
-rw-r--r--pkg/virtualnetwork/vfkit.go12
-rw-r--r--pkg/virtualnetwork/virtualnetwork.go144
-rw-r--r--pkg/virtualnetwork/vpnkit.go82
-rw-r--r--rpm/gvisor-tap-vsock.spec127
-rw-r--r--test-win-sshproxy/basic_test.go88
-rw-r--r--test-win-sshproxy/mock_sshserver.go130
-rw-r--r--test-win-sshproxy/suite_test.go123
-rw-r--r--test/basic_test.go222
-rw-r--r--test/fcos.go80
-rw-r--r--test/fcos_amd64.go64
-rw-r--r--test/ignition.go162
-rw-r--r--test/ignition_schema.go248
-rwxr-xr-xtest/performance.sh54
-rw-r--r--test/port_forwarding_test.go269
-rw-r--r--test/pull.go81
-rw-r--r--test/suite_test.go242
-rwxr-xr-xtest/wsl.sh9
-rw-r--r--tools/bin/.gitignore1
-rw-r--r--tools/dummy.go9
-rw-r--r--tools/go.mod192
-rw-r--r--tools/go.sum990
-rw-r--r--tools/tools.mk10
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).
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -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 ./...
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..558fe5a
--- /dev/null
+++ b/OWNERS
@@ -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
new file mode 100644
index 0000000..3423b75
--- /dev/null
+++ b/doc/curl.png
Binary files differ
diff --git a/doc/http.png b/doc/http.png
new file mode 100644
index 0000000..5e2c59d
--- /dev/null
+++ b/doc/http.png
Binary files differ
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..543aa2d
--- /dev/null
+++ b/go.mod
@@ -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
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..7063248
--- /dev/null
+++ b/go.sum
@@ -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