├── .github ├── pull_request_template.md ├── renovate.json └── workflows │ ├── ci.yaml │ ├── release.yaml │ └── renovate-vault.yml ├── .gitignore ├── CODEOWNERS ├── Dockerfile.proxy ├── LICENSE ├── Makefile ├── README.md ├── VERSION.md ├── back_pressure.go ├── buffer_test.go ├── charts └── remotedialer-proxy │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── role.yaml │ ├── rolebinding.yaml │ ├── service.yaml │ └── serviceaccount.yaml │ └── values.yaml ├── client.go ├── client └── main.go ├── client_dialer.go ├── cmd └── proxy │ ├── main.go │ └── proxy.yaml ├── connection.go ├── dialer.go ├── docs ├── remotedialer-flow.drawio ├── remotedialer-flow.png ├── remotedialer.drawio └── remotedialer.png ├── dummy └── main.go ├── examples ├── fakek8s │ ├── Dockerfile │ ├── fakek8s.yaml │ ├── go.mod │ └── main.go └── proxyclient │ └── main.go ├── forward └── forward.go ├── go.mod ├── go.sum ├── message.go ├── metrics └── session_manager.go ├── peer.go ├── proxy ├── config.go └── server.go ├── proxyclient └── client.go ├── readbuffer.go ├── server.go ├── server └── main.go ├── session.go ├── session_manager.go ├── session_serve.go ├── session_serve_test.go ├── session_sync.go ├── session_sync_test.go ├── session_test.go ├── types.go ├── wsconn.go └── wsconn_test.go /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Issue: 2 | 3 | 5 | 6 | ## Problem 7 | 8 | 11 | 12 | ## Solution 13 | 14 | 16 | 17 | ## CheckList 18 | 19 | 24 | - [ ] Test 25 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>rancher/renovate-config#release" 4 | ], 5 | "baseBranches": [ 6 | "main", "release/v0.3" 7 | ], 8 | "prHourlyLimit": 2, 9 | "packageRules": [ 10 | { 11 | "matchPackagePatterns": [ 12 | "k8s.io/*", 13 | "sigs.k8s.io/*", 14 | "github.com/prometheus/*" 15 | ], 16 | "enabled": false 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | ci: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout code 8 | # https://github.com/actions/checkout/releases/tag/v4.1.1 9 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 10 | - name: Install Go 11 | # https://github.com/actions/setup-go/releases/tag/v5.0.0 12 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 13 | with: 14 | go-version-file: 'go.mod' 15 | - run: make test 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | permissions: 9 | contents: write 10 | 11 | env: 12 | REGISTRY: docker.io 13 | REPO: rancher 14 | 15 | jobs: 16 | release: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: write 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | 24 | - name: Install helm 25 | env: 26 | HELM_VERSION: "v3.13.3" 27 | run: | 28 | curl -sL https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz | tar xvzf - -C /usr/local/bin --strip-components=1 29 | 30 | - name: Create helm tarball 31 | env: 32 | HELM_TAG: "${{ github.ref_name }}" 33 | run: | 34 | mkdir -p build dist/artifacts 35 | cp -rf charts build/ 36 | 37 | # Remove prefix `v` because it's not needed 38 | HELM_CHART_VERSION=$(echo "$HELM_TAG" | sed 's|^v||') 39 | 40 | sed -i \ 41 | -e 's/^version:.*/version: '${HELM_CHART_VERSION}'/' \ 42 | -e 's/appVersion:.*/appVersion: '${HELM_CHART_VERSION}'/' \ 43 | build/charts/remotedialer-proxy/Chart.yaml 44 | 45 | sed -i \ 46 | -e 's/tag:.*/tag: '${HELM_TAG}'/' \ 47 | build/charts/remotedialer-proxy/values.yaml 48 | 49 | helm package -d ./dist/artifacts ./build/charts/remotedialer-proxy 50 | 51 | - name: Create release on Github 52 | env: 53 | GH_TOKEN: ${{ github.token }} 54 | run: | 55 | cd dist/artifacts 56 | 57 | if [[ "${{ github.ref_name }}" == *-rc* ]]; then 58 | gh --repo "${{ github.repository }}" release create ${{ github.ref_name }} --verify-tag --generate-notes --prerelease remotedialer-proxy*.tgz 59 | else 60 | gh --repo "${{ github.repository }}" release create ${{ github.ref_name }} --verify-tag --generate-notes remotedialer-proxy*.tgz 61 | fi 62 | image: 63 | runs-on: ubuntu-latest 64 | permissions: 65 | contents: read 66 | id-token: write 67 | strategy: 68 | matrix: 69 | arch: 70 | - amd64 71 | - arm64 72 | name: Build and push proxy image 73 | steps: 74 | - name : Checkout repository 75 | # https://github.com/actions/checkout/releases/tag/v4.1.1 76 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 77 | 78 | - name: "Read vault secrets" 79 | uses: rancher-eio/read-vault-secrets@main 80 | with: 81 | secrets: | 82 | secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME; 83 | secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD 84 | 85 | - name: Set up QEMU 86 | # https://github.com/docker/setup-qemu-action/releases/tag/v3.1.0 87 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 88 | 89 | - name: Set up Docker Buildx 90 | uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 91 | # https://github.com/docker/setup-buildx-action/releases/tag/v3.4.0 92 | 93 | - name: Log in to the Container registry 94 | # https://github.com/docker/login-action/releases/tag/v3.2.0 95 | uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 96 | with: 97 | registry: ${{ env.REGISTRY }} 98 | username: ${{ env.DOCKER_USERNAME }} 99 | password: ${{ env.DOCKER_PASSWORD }} 100 | 101 | - name: Build and push the remotedialer image 102 | id: build 103 | # https://github.com/docker/build-push-action/releases/tag/v6.3.0 104 | uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0 105 | with: 106 | context: . 107 | file: ./Dockerfile.proxy 108 | platforms: "linux/${{ matrix.arch }}" 109 | outputs: type=image,name=${{ env.REPO }}/remotedialer-proxy,push-by-digest=true,name-canonical=true,push=true 110 | 111 | - name: Export digest 112 | run: | 113 | mkdir -p /tmp/digests 114 | digest="${{ steps.build.outputs.digest }}" 115 | touch "/tmp/digests/${digest#sha256:}" 116 | 117 | - name: Upload digest 118 | uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 119 | # https://github.com/actions/upload-artifact/releases/tag/v4.3.3 120 | with: 121 | name: digests-${{ matrix.arch }} 122 | path: /tmp/digests/* 123 | if-no-files-found: error 124 | retention-days: 1 125 | 126 | merge-images: 127 | permissions: 128 | id-token: write 129 | runs-on: ubuntu-latest 130 | needs: image 131 | steps: 132 | - name: Download digests 133 | uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 134 | # https://github.com/actions/download-artifact/releases/tag/v4.1.7 135 | with: 136 | path: /tmp/digests 137 | pattern: digests-* 138 | merge-multiple: true 139 | 140 | - name: Set up Docker Buildx 141 | uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 142 | # https://github.com/docker/setup-buildx-action/releases/tag/v3.4.0 143 | 144 | - name: "Read vault secrets" 145 | uses: rancher-eio/read-vault-secrets@main 146 | with: 147 | secrets: | 148 | secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ; 149 | secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD 150 | 151 | - name: Log in to the Container registry 152 | uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 153 | # https://github.com/docker/login-action/releases/tag/v3.2.0 154 | with: 155 | registry: ${{ env.REGISTRY }} 156 | username: ${{ env.DOCKER_USERNAME }} 157 | password: ${{ env.DOCKER_PASSWORD }} 158 | 159 | # setup tag name 160 | - if: ${{ startsWith(github.ref, 'refs/tags/') }} 161 | run: | 162 | echo TAG_NAME=$(echo $GITHUB_REF | sed -e "s|refs/tags/||") >> $GITHUB_ENV 163 | 164 | - name: Create manifest list and push 165 | working-directory: /tmp/digests 166 | run: | 167 | docker buildx imagetools create -t ${{ env.REGISTRY }}/${{ env.REPO }}/remotedialer-proxy:${{ env.TAG_NAME }} \ 168 | $(printf '${{ env.REPO }}/remotedialer-proxy@sha256:%s ' *) -------------------------------------------------------------------------------- /.github/workflows/renovate-vault.yml: -------------------------------------------------------------------------------- 1 | name: Renovate 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | logLevel: 6 | description: "Override default log level" 7 | required: false 8 | default: "info" 9 | type: string 10 | overrideSchedule: 11 | description: "Override all schedules" 12 | required: false 13 | default: "false" 14 | type: string 15 | # Run twice in the early morning (UTC) for initial and follow up steps (create pull request and merge) 16 | schedule: 17 | - cron: '30 4,6 * * *' 18 | 19 | permissions: 20 | contents: read 21 | id-token: write 22 | 23 | jobs: 24 | call-workflow: 25 | uses: rancher/renovate-config/.github/workflows/renovate-vault.yml@release 26 | with: 27 | logLevel: ${{ inputs.logLevel || 'info' }} 28 | overrideSchedule: ${{ github.event.inputs.overrideSchedule == 'true' && '{''schedule'':null}' || '' }} 29 | secrets: inherit 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /client/client 3 | /dummy/dummy 4 | /server/server 5 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @rancher/rancher-squad-frameworks 2 | -------------------------------------------------------------------------------- /Dockerfile.proxy: -------------------------------------------------------------------------------- 1 | FROM golang:1.23 AS builder 2 | WORKDIR /app 3 | 4 | COPY . . 5 | RUN go mod download 6 | 7 | 8 | RUN CGO_ENABLED=0 go build -o proxy ./cmd/proxy 9 | 10 | FROM scratch 11 | COPY --from=builder /app/proxy . 12 | CMD ["./proxy"] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: client dummy server 2 | 3 | client: 4 | go build -o client/client ./client 5 | 6 | dummy: 7 | go build -o dummy/dummy ./dummy 8 | 9 | server: 10 | go build -o server/server ./server 11 | 12 | test: 13 | go test -cover ./... 14 | 15 | .PHONY: all client dummy server test 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Reverse Tunneling Dialer 2 | ======================== 3 | 4 | remotedialer creates a two-way connection between a server and a client, so 5 | that a `net.Dial` can be performed on the server and actually connects to the 6 | client running services accessible on localhost or behind a NAT or firewall. 7 | 8 | Architecture 9 | ------------ 10 | 11 | ### Abstractions 12 | 13 | remotedialer consists of structs that organize and abstract a TCP connection 14 | between a server and a client. Both client and server use ``Session``s, which 15 | provides a means to make a network connection to a remote endpoint and keep 16 | track of incoming connections. A server uses the ``Server`` object which 17 | contains a ``sessionManager`` which governs one or more ``Session``s, while a 18 | client creates a ``Session`` directly. A ``connection`` implements the 19 | ``io.Reader`` and ``io.WriteCloser`` interfaces, so it can be read from and 20 | written to directly. The ``connection``'s internal ``readBuffer`` monitors the 21 | size of the data it is carrying and uses ``backPressure`` to pause incoming 22 | data transfer until the amount of data is below a threshold. 23 | 24 | ![](./docs/remotedialer.png) 25 | 26 | ### Data flow 27 | 28 | ![](./docs/remotedialer-flow.png) 29 | 30 | A client establishes a session with a server using the server's URL. The server 31 | upgrades the connection to a websocket connection which it uses to create a 32 | ``Session``. The client then also creates a ``Session`` with the websocket 33 | connection. 34 | 35 | The client sits in front of some kind of HTTP server, most often a kubernetes 36 | API server, and acts as a reverse proxy for that HTTP resource. When a user 37 | requests a resource from this remote resource, request first goes to the 38 | remotedialer server. The application containing the server is responsible for 39 | routing the request to the right client connection. 40 | 41 | The request is sent through the websocket connection to the client and read 42 | into the client's connection buffer. A pipe is created between the client and 43 | the HTTP service which continually copies data between each socket. The request 44 | is forwarded through the pipe to the remote service, draining the buffer. The 45 | service's response is copied back to the client's buffer, and then forwarded 46 | back to the server and copied into the server connection's own buffer. As the 47 | user reads the response, the server's buffer is drained. 48 | 49 | The pause/resume mechanism checks the size of the buffer for both the client 50 | and server. If it is greater than the threshold, a ``PAUSE`` message is sent 51 | back to the remote connection, as a suggestion not to send any more data. As 52 | the buffer is drained, either by the pipe to the remote HTTP service or the 53 | user's connection, the size is checked again. When it is lower than the 54 | threshold, a ``RESUME`` message is sent, and the data transfer may continue. 55 | 56 | ### remotedialer in the Rancher ecosystem 57 | 58 | remotedialer is used to connect Rancher to the downstream clusters it manages, 59 | enabling a user agent to access the cluster through an endpoint on the Rancher 60 | server. remotedialer is used in three main ways: 61 | 62 | #### Agent config and tunnel server 63 | 64 | When the agent starts, it initially makes a client connection to the endpoint 65 | `/v3/connect/register`, which runs an authorizer that sets some initial data 66 | about the node. The agent continues to connect to the endpoint `/v3/connect` on 67 | a loop. On each connection, it runs an OnConnect handler which pulls down node 68 | configuration data from `/v3/connect/config`. 69 | 70 | #### Steve Aggregation 71 | 72 | The steve aggregation server on the agent establishes a remotedialer Session 73 | with Rancher, making the steve API on the downstream cluster accessible from 74 | the Rancher server and facilitating resource watches. 75 | 76 | #### Health Check 77 | 78 | The clusterconnected controller in Rancher uses the established tunnel to check 79 | that clusters are still responsive and sets alert conditions on the cluster 80 | object if they are not. 81 | 82 | #### HA operation (peering) 83 | 84 | remotedialer supports a mode where multiple servers can be configured as peers. 85 | In that mode all servers maintain a mapping of all remotedialer client connections 86 | to all other servers, and can route incoming requests appropriately. 87 | 88 | Therefore, http requests referring any of the remotedialer clients can be resolved 89 | by any of the peer servers. This is useful for high availability, and Rancher 90 | leverages that functionality to distribute downstream clusters (running agents 91 | acting as remotedialer clients) among replica pods (acting as remotedialer 92 | server peers). In case one Rancher replica pod breaks down, Rancher will 93 | reassign its downstream clusters to others. 94 | 95 | Peers authenticate to one another via a shared token. 96 | 97 | Running Locally 98 | --------------- 99 | 100 | remotedialer provides an example client and server which can be run in 101 | standalone mode FOR TESTING ONLY. These are found in the `server/` and 102 | `client/` directories.` 103 | 104 | ### Compile 105 | 106 | Compile the server and client: 107 | 108 | ``` 109 | make server 110 | make client 111 | ``` 112 | 113 | ### Run 114 | 115 | Start the server first. 116 | 117 | ``` 118 | ./server/server 119 | ``` 120 | 121 | The server has debug mode off by default. Enable it with `--debug`. 122 | 123 | The client proxies requests from the remotedialer server to a web server, so it 124 | needs to be run somewhere where it can access the web server you want to 125 | target. The remotedialer server also needs to be reachable by the client. 126 | 127 | For testing purposes, a basic HTTP file server is provided. Build the server with: 128 | 129 | ``` 130 | make dummy 131 | ``` 132 | 133 | Create a directory with files to serve, then run the web server from that directory: 134 | 135 | ``` 136 | mkdir www 137 | cd www 138 | echo 'hello' > bigfile 139 | /path/to/dummy 140 | ``` 141 | 142 | Run the client with 143 | 144 | ``` 145 | ./client/client 146 | ``` 147 | 148 | Both server and client can be run with even more verbose logging: 149 | 150 | ``` 151 | CATTLE_TUNNEL_DATA_DEBUG=true ./server/server --debug 152 | CATTLE_TUNNEL_DATA_DEBUG=true ./client/client 153 | ``` 154 | 155 | ### Usage 156 | 157 | If the remotedialer server is running on 192.168.0.42, and the web service that 158 | the client can access is running at address 127.0.0.1:8125, make proxied 159 | requests like this: 160 | 161 | ``` 162 | curl http://192.168.0.42:8123/client/foo/http/127.0.0.1:8125/bigfile 163 | ``` 164 | 165 | where `foo` is the hardcoded client ID for this test server. 166 | 167 | This test server only supports GET requests. 168 | 169 | ### HA Usage 170 | 171 | To test remotedialer in HA mode, first start the dummy server from an appropriate directory, eg.: 172 | 173 | ```shell 174 | cd /tmp 175 | mkdir www 176 | cd www 177 | echo 'hello' > bigfile 178 | /path/to/dummy -listen :8125 179 | ``` 180 | 181 | Then start two peer remotedialer servers with the `-peers id:token:url` flag: 182 | 183 | ```shell 184 | ./server/server -debug -id first -token aaa -listen :8123 -peers second:aaa:ws://localhost:8124/connect & 185 | ./server/server -debug -id second -token aaa -listen :8124 -peers first:aaa:ws://localhost:8123/connect 186 | ``` 187 | 188 | Then connect a client to the first server, eg: 189 | ```shell 190 | ./client/client -id foo -connect ws://localhost:8123/connect 191 | ``` 192 | 193 | Finally, use the second server to make a request to the client via the first server: 194 | ``` 195 | curl http://localhost:8124/client/foo/http/127.0.0.1:8125/ 196 | ``` 197 | 198 | # Versioning 199 | 200 | See [VERSION.md](VERSION.md). 201 | -------------------------------------------------------------------------------- /VERSION.md: -------------------------------------------------------------------------------- 1 | RemoteDialer follows a pre-release (v0.x) strategy of semver. There is limited compatibility between releases, though we do aim to avoid breaking changes on minor version lines. 2 | 3 | The current supported release lines are: 4 | 5 | | RemoteDialer Branch | RemoteDialer Minor version | 6 | |--------------------------|------------------------------------| 7 | | main | v0.4 | 8 | | release/v0.3 | v0.3 | 9 | -------------------------------------------------------------------------------- /back_pressure.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | type backPressure struct { 9 | cond sync.Cond 10 | c *connection 11 | paused bool 12 | closed bool 13 | } 14 | 15 | func newBackPressure(c *connection) *backPressure { 16 | return &backPressure{ 17 | cond: sync.Cond{ 18 | L: &sync.Mutex{}, 19 | }, 20 | c: c, 21 | paused: false, 22 | } 23 | } 24 | 25 | func (b *backPressure) OnPause() { 26 | b.cond.L.Lock() 27 | defer b.cond.L.Unlock() 28 | 29 | b.paused = true 30 | b.cond.Broadcast() 31 | } 32 | 33 | func (b *backPressure) Close() { 34 | b.cond.L.Lock() 35 | defer b.cond.L.Unlock() 36 | 37 | b.closed = true 38 | b.cond.Broadcast() 39 | } 40 | 41 | func (b *backPressure) OnResume() { 42 | b.cond.L.Lock() 43 | defer b.cond.L.Unlock() 44 | 45 | b.paused = false 46 | b.cond.Broadcast() 47 | } 48 | 49 | func (b *backPressure) Pause() { 50 | b.cond.L.Lock() 51 | defer b.cond.L.Unlock() 52 | if b.paused { 53 | return 54 | } 55 | b.c.Pause() 56 | b.paused = true 57 | } 58 | 59 | func (b *backPressure) Resume() { 60 | b.cond.L.Lock() 61 | defer b.cond.L.Unlock() 62 | if !b.paused { 63 | return 64 | } 65 | b.c.Resume() 66 | b.paused = false 67 | } 68 | 69 | func (b *backPressure) Wait(cancel context.CancelFunc) { 70 | b.cond.L.Lock() 71 | defer b.cond.L.Unlock() 72 | 73 | for !b.closed && b.paused { 74 | b.cond.Wait() 75 | cancel() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /buffer_test.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "net" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestExceedBuffer(t *testing.T) { 14 | ctx, cancel := context.WithCancel(context.Background()) 15 | defer cancel() 16 | 17 | producerAddress, err := newTestProducer(ctx) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | serverAddress, server, err := newTestServer(ctx) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | if err := newTestClient(ctx, "ws://"+serverAddress); err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | client := http.Client{ 32 | Transport: &http.Transport{ 33 | DialContext: func(ctx context.Context, proto, address string) (net.Conn, error) { 34 | return server.Dialer("client")(ctx, proto, address) 35 | }, 36 | }, 37 | } 38 | 39 | producerURL := "http://" + producerAddress 40 | 41 | resp, err := client.Get(producerURL) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | defer resp.Body.Close() 46 | 47 | resp2, err := client.Get(producerURL) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | defer resp2.Body.Close() 52 | 53 | resp2Body, err := ioutil.ReadAll(resp2.Body) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | respBody, err := ioutil.ReadAll(resp.Body) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | assert.Equal(t, 4096*4096, len(resp2Body)) 64 | assert.Equal(t, 4096*4096, len(respBody)) 65 | } 66 | 67 | func newTestServer(ctx context.Context) (string, *Server, error) { 68 | auth := func(req *http.Request) (clientKey string, authed bool, err error) { 69 | return "client", true, nil 70 | } 71 | 72 | server := New(auth, DefaultErrorWriter) 73 | address, err := newServer(ctx, server) 74 | return address, server, err 75 | } 76 | 77 | func newTestClient(ctx context.Context, url string) error { 78 | result := make(chan error, 2) 79 | go func() { 80 | err := ConnectToProxy(ctx, url, nil, func(proto, address string) bool { 81 | return true 82 | }, nil, func(ctx context.Context, session *Session) error { 83 | result <- nil 84 | return nil 85 | }) 86 | result <- err 87 | }() 88 | return <-result 89 | } 90 | 91 | func newServer(ctx context.Context, handler http.Handler) (string, error) { 92 | server := http.Server{ 93 | BaseContext: func(_ net.Listener) context.Context { 94 | return ctx 95 | }, 96 | Handler: handler, 97 | } 98 | listener, err := net.Listen("tcp", "localhost:0") 99 | if err != nil { 100 | return "", err 101 | } 102 | go func() { 103 | <-ctx.Done() 104 | listener.Close() 105 | server.Shutdown(context.Background()) 106 | }() 107 | go server.Serve(listener) 108 | return listener.Addr().String(), nil 109 | } 110 | 111 | func newTestProducer(ctx context.Context) (string, error) { 112 | buffer := make([]byte, 4096) 113 | return newServer(ctx, http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 114 | for i := 0; i < 4096; i++ { 115 | if _, err := resp.Write(buffer); err != nil { 116 | panic(err) 117 | } 118 | } 119 | })) 120 | } 121 | -------------------------------------------------------------------------------- /charts/remotedialer-proxy/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/remotedialer-proxy/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: remotedialer-proxy 3 | description: creates a bridge between k8s api-server and imperative api when Rancher is outside of the cluster 4 | type: application 5 | version: 0.0.1 6 | appVersion: 0.0.1 7 | annotations: 8 | catalog.cattle.io/certified: rancher 9 | catalog.cattle.io/hidden: "true" 10 | catalog.cattle.io/namespace: cattle-system 11 | catalog.cattle.io/release-name: remotedialer-proxy 12 | catalog.cattle.io/os: linux 13 | catalog.cattle.io/permits-os: linux,windows 14 | catalog.cattle.io/rancher-version: ">= 2.11.0-0" 15 | catalog.cattle.io/managed: "true" 16 | -------------------------------------------------------------------------------- /charts/remotedialer-proxy/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{- define "system_default_registry" -}} 2 | {{- if .Values.global.cattle.systemDefaultRegistry -}} 3 | {{- printf "%s/" .Values.global.cattle.systemDefaultRegistry -}} 4 | {{- else -}} 5 | {{- "" -}} 6 | {{- end -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | API Extension Name - To be used in other variables 11 | */}} 12 | {{- define "api-extension.name" }} 13 | {{- default "api-extension" .Values.apiExtensionName }} 14 | {{- end}} 15 | 16 | {{/* 17 | Namespace to use 18 | */}} 19 | {{- define "remotedialer-proxy.namespace" -}} 20 | {{- default "cattle-system" .Values.namespaceOverride }} 21 | {{- end }} 22 | 23 | {{/* 24 | Expand the name of the chart. 25 | */}} 26 | {{- define "remotedialer-proxy.name" -}} 27 | {{- default (include "api-extension.name" .) .Values.nameOverride | trunc 63 | trimSuffix "-" }} 28 | {{- end }} 29 | 30 | {{/* 31 | Create chart name and version as used by the chart label. 32 | */}} 33 | {{- define "remotedialer-proxy.chart" -}} 34 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 35 | {{- end }} 36 | 37 | {{/* 38 | Common labels 39 | */}} 40 | {{- define "remotedialer-proxy.labels" -}} 41 | helm.sh/chart: {{ include "remotedialer-proxy.chart" . }} 42 | {{- if .Chart.AppVersion }} 43 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 44 | {{- end }} 45 | app.kubernetes.io/managed-by: {{ .Release.Service }} 46 | {{ include "remotedialer-proxy.selectorLabels" . }} 47 | {{- end }} 48 | 49 | {{/* 50 | Selector labels 51 | */}} 52 | {{- define "remotedialer-proxy.selectorLabels" -}} 53 | app.kubernetes.io/name: {{ include "remotedialer-proxy.name" . }} 54 | app.kubernetes.io/instance: {{ include "api-extension.name" . }} 55 | app: {{ include "api-extension.name" . }} 56 | {{- end }} 57 | 58 | {{/* 59 | Create the name of the service account to use 60 | */}} 61 | {{- define "remotedialer-proxy.serviceAccountName" -}} 62 | {{- default (include "api-extension.name" .) .Values.serviceAccount.name }} 63 | {{- end }} 64 | 65 | {{/* 66 | Role to use 67 | */}} 68 | {{- define "remotedialer-proxy.role" -}} 69 | {{- default (include "api-extension.name" .) .Values.roleOverride }} 70 | {{- end }} 71 | 72 | {{/* 73 | Role Binding to use 74 | */}} 75 | {{- define "remotedialer-proxy.rolebinding" -}} 76 | {{- include "api-extension.name" . }} 77 | {{- end }} 78 | -------------------------------------------------------------------------------- /charts/remotedialer-proxy/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "remotedialer-proxy.name" . }} 5 | namespace: {{ include "remotedialer-proxy.namespace" . }} 6 | spec: 7 | replicas: {{ .Values.replicaCount }} 8 | selector: 9 | matchLabels: 10 | {{- include "remotedialer-proxy.selectorLabels" . | nindent 6 }} 11 | template: 12 | metadata: 13 | {{- with .Values.podAnnotations }} 14 | annotations: 15 | {{- toYaml . | nindent 8 }} 16 | {{- end }} 17 | labels: 18 | {{- include "remotedialer-proxy.labels" . | nindent 8 }} 19 | {{- with .Values.podLabels }} 20 | {{- toYaml . | nindent 8 }} 21 | {{- end }} 22 | spec: 23 | {{- with .Values.imagePullSecrets }} 24 | imagePullSecrets: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | serviceAccountName: {{ include "remotedialer-proxy.serviceAccountName" . }} 28 | containers: 29 | - name: {{ include "remotedialer-proxy.name" . }} 30 | image: {{ template "system_default_registry" $ }}{{ $.Values.image.repository }}:{{default .Chart.AppVersion .Values.image.tag }} 31 | imagePullPolicy: {{ .Values.image.pullPolicy }} 32 | ports: 33 | - name: https 34 | containerPort: {{ .Values.service.httpsPort }} 35 | protocol: TCP 36 | - name: proxy 37 | containerPort: {{ .Values.service.proxyPort }} 38 | protocol: TCP 39 | env: 40 | - name: CERT_CA_NAME 41 | value: {{ .Values.service.certCAName }} 42 | - name: TLS_NAME 43 | value: {{ .Values.service.tlsName}} 44 | - name: CA_NAME 45 | value: {{ .Values.service.caName}} 46 | - name: CERT_CA_NAMESPACE 47 | value: {{ include "remotedialer-proxy.namespace" . }} 48 | - name: SECRET 49 | valueFrom: 50 | secretKeyRef: 51 | name: {{ include "api-extension.name" . }} 52 | key: data 53 | - name: HTTPS_PORT 54 | value: {{ .Values.service.httpsPort | quote }} 55 | - name: PROXY_PORT 56 | value: {{ .Values.service.proxyPort | quote }} 57 | - name: PEER_PORT 58 | value: {{ .Values.service.peerPort | quote }} 59 | -------------------------------------------------------------------------------- /charts/remotedialer-proxy/templates/role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: {{ include "remotedialer-proxy.role" . }} 5 | namespace: {{ include "remotedialer-proxy.namespace" . }} 6 | rules: 7 | - apiGroups: [""] 8 | resources: ["secrets"] 9 | verbs: ["get", "create", "update"] 10 | 11 | -------------------------------------------------------------------------------- /charts/remotedialer-proxy/templates/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: {{ include "remotedialer-proxy.rolebinding" . }} 5 | namespace: {{ include "remotedialer-proxy.namespace" . }} 6 | subjects: 7 | - kind: ServiceAccount 8 | name: {{ include "remotedialer-proxy.serviceAccountName" . }} 9 | namespace: {{ include "remotedialer-proxy.namespace" . }} 10 | roleRef: 11 | kind: Role 12 | name: {{ include "remotedialer-proxy.role" . }} 13 | apiGroup: rbac.authorization.k8s.io 14 | -------------------------------------------------------------------------------- /charts/remotedialer-proxy/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "remotedialer-proxy.name" . }} 5 | labels: 6 | {{- include "remotedialer-proxy.labels" . | nindent 4 }} 7 | namespace: {{ include "remotedialer-proxy.namespace" . }} 8 | spec: 9 | type: {{ .Values.service.type }} 10 | ports: 11 | - port: {{ .Values.service.httpsPort }} 12 | targetPort: https 13 | protocol: TCP 14 | name: https 15 | - port: {{ .Values.service.proxyPort }} 16 | targetPort: proxy 17 | protocol: TCP 18 | name: proxy 19 | selector: 20 | {{- include "remotedialer-proxy.selectorLabels" . | nindent 4 }} 21 | -------------------------------------------------------------------------------- /charts/remotedialer-proxy/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ include "remotedialer-proxy.serviceAccountName" . }} 5 | namespace: {{ include "remotedialer-proxy.namespace" . }} 6 | 7 | -------------------------------------------------------------------------------- /charts/remotedialer-proxy/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for remotedialer-proxy. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: rancher/remotedialer-proxy 9 | pullPolicy: IfNotPresent 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "" 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | namespaceOverride: "" 16 | roleOverride: "" 17 | 18 | # prefix name used in deployment, role, rolebinding and other k8s resources 19 | apiExtensionName: "" 20 | 21 | serviceAccount: 22 | name: "" 23 | 24 | service: 25 | type: ClusterIP 26 | httpsPort: 5555 27 | proxyPort: 6666 28 | peerPort: 6666 29 | caName: "api-extension-ca-name" 30 | certCAName: "api-extension-cert-ca-name" 31 | tlsName: "api-extension-tls-name" 32 | certCAName: "api-extension-ca" 33 | 34 | global: 35 | cattle: 36 | systemDefaultRegistry: "" 37 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/gorilla/websocket" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // ConnectAuthorizer custom for authorization 16 | type ConnectAuthorizer func(proto, address string) bool 17 | 18 | // ClientConnect connect to WS and wait 5 seconds when error 19 | func ClientConnect(ctx context.Context, wsURL string, headers http.Header, dialer *websocket.Dialer, 20 | auth ConnectAuthorizer, onConnect func(context.Context, *Session) error) error { 21 | if err := ConnectToProxy(ctx, wsURL, headers, auth, dialer, onConnect); err != nil { 22 | if !errors.Is(err, context.Canceled) { 23 | logrus.WithError(err).Error("Remotedialer proxy error") 24 | time.Sleep(time.Duration(5) * time.Second) 25 | } 26 | return err 27 | } 28 | return nil 29 | } 30 | 31 | // ConnectToProxy connects to the websocket server. 32 | // Local connections on behalf of the remote host will be dialed using a default net.Dialer. 33 | func ConnectToProxy(rootCtx context.Context, proxyURL string, headers http.Header, auth ConnectAuthorizer, dialer *websocket.Dialer, onConnect func(context.Context, *Session) error) error { 34 | return ConnectToProxyWithDialer(rootCtx, proxyURL, headers, auth, dialer, nil, onConnect) 35 | } 36 | 37 | // ConnectToProxyWithDialer connects to the websocket server. 38 | // Local connections on behalf of the remote host will be dialed using the provided Dialer function. 39 | func ConnectToProxyWithDialer(rootCtx context.Context, proxyURL string, headers http.Header, auth ConnectAuthorizer, dialer *websocket.Dialer, localDialer Dialer, onConnect func(context.Context, *Session) error) error { 40 | logrus.WithField("url", proxyURL).Info("Connecting to proxy") 41 | 42 | if dialer == nil { 43 | dialer = &websocket.Dialer{Proxy: http.ProxyFromEnvironment, HandshakeTimeout: HandshakeTimeOut} 44 | } 45 | ws, resp, err := dialer.DialContext(rootCtx, proxyURL, headers) 46 | if err != nil { 47 | if resp == nil { 48 | if !errors.Is(err, context.Canceled) { 49 | logrus.WithError(err).Errorf("Failed to connect to proxy. Empty dialer response") 50 | } 51 | } else { 52 | rb, err2 := ioutil.ReadAll(resp.Body) 53 | if err2 != nil { 54 | logrus.WithError(err).Errorf("Failed to connect to proxy. Response status: %v - %v. Couldn't read response body (err: %v)", resp.StatusCode, resp.Status, err2) 55 | } else { 56 | logrus.WithError(err).Errorf("Failed to connect to proxy. Response status: %v - %v. Response body: %s", resp.StatusCode, resp.Status, rb) 57 | } 58 | } 59 | return err 60 | } 61 | defer ws.Close() 62 | 63 | result := make(chan error, 2) 64 | 65 | ctx, cancel := context.WithCancel(rootCtx) 66 | defer cancel() 67 | ctx = context.WithValue(ctx, ContextKeyCaller, fmt.Sprintf("ConnectToProxy: url: %s", proxyURL)) 68 | 69 | session := NewClientSessionWithDialer(auth, ws, localDialer) 70 | defer session.Close() 71 | 72 | if onConnect != nil { 73 | go func() { 74 | if err := onConnect(ctx, session); err != nil { 75 | result <- err 76 | } 77 | }() 78 | } 79 | 80 | go func() { 81 | _, err = session.Serve(ctx) 82 | result <- err 83 | }() 84 | 85 | select { 86 | case <-ctx.Done(): 87 | logrus.WithField("url", proxyURL).WithField("err", ctx.Err()).Info("Proxy done") 88 | return nil 89 | case err := <-result: 90 | return err 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "net/http" 7 | 8 | "github.com/rancher/remotedialer" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | var ( 13 | addr string 14 | id string 15 | debug bool 16 | ) 17 | 18 | func main() { 19 | flag.StringVar(&addr, "connect", "ws://localhost:8123/connect", "Address to connect to") 20 | flag.StringVar(&id, "id", "foo", "Client ID") 21 | flag.BoolVar(&debug, "debug", true, "Debug logging") 22 | flag.Parse() 23 | 24 | if debug { 25 | logrus.SetLevel(logrus.DebugLevel) 26 | } 27 | 28 | headers := http.Header{ 29 | "X-Tunnel-ID": []string{id}, 30 | } 31 | 32 | remotedialer.ClientConnect(context.Background(), addr, headers, nil, func(string, string) bool { return true }, nil) 33 | } 34 | -------------------------------------------------------------------------------- /client_dialer.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | func clientDial(ctx context.Context, dialer Dialer, conn *connection, message *message) { 12 | defer conn.Close() 13 | 14 | var ( 15 | netConn net.Conn 16 | err error 17 | ) 18 | 19 | ctx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Minute)) 20 | if dialer == nil { 21 | d := net.Dialer{} 22 | netConn, err = d.DialContext(ctx, message.proto, message.address) 23 | } else { 24 | netConn, err = dialer(ctx, message.proto, message.address) 25 | } 26 | cancel() 27 | 28 | if err != nil { 29 | conn.tunnelClose(err) 30 | return 31 | } 32 | defer netConn.Close() 33 | 34 | pipe(conn, netConn) 35 | } 36 | 37 | func pipe(client *connection, server net.Conn) { 38 | wg := sync.WaitGroup{} 39 | wg.Add(1) 40 | 41 | closePipe := func(err error) error { 42 | if err == nil { 43 | err = io.EOF 44 | } 45 | client.doTunnelClose(err) 46 | server.Close() 47 | return err 48 | } 49 | 50 | go func() { 51 | defer wg.Done() 52 | _, err := io.Copy(server, client) 53 | closePipe(err) 54 | }() 55 | 56 | _, err := io.Copy(client, server) 57 | err = closePipe(err) 58 | wg.Wait() 59 | 60 | // Write tunnel error after no more I/O is happening, just incase messages get out of order 61 | client.writeErr(err) 62 | } 63 | -------------------------------------------------------------------------------- /cmd/proxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "k8s.io/client-go/rest" 6 | 7 | "github.com/rancher/remotedialer/proxy" 8 | ) 9 | 10 | func main() { 11 | logrus.Info("Starting Remote Dialer Proxy") 12 | 13 | cfg, err := proxy.ConfigFromEnvironment() 14 | if err != nil { 15 | logrus.Fatalf("fatal configuration error: %v", err) 16 | } 17 | 18 | restConfig, err := rest.InClusterConfig() 19 | if err != nil { 20 | logrus.Errorf("failed to get in-cluster config: %s", err.Error()) 21 | return 22 | } 23 | 24 | err = proxy.Start(cfg, restConfig) 25 | if err != nil { 26 | logrus.Fatal(err) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cmd/proxy/proxy.yaml: -------------------------------------------------------------------------------- 1 | #FIXME This is temporary file. This should be converted into Helm Charts in the charts repo. 2 | 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: remotedialer-proxy 7 | namespace: cattle-system 8 | labels: 9 | app: remotedialer-proxy 10 | spec: 11 | replicas: 1 12 | selector: 13 | matchLabels: 14 | app: remotedialer-proxy 15 | template: 16 | metadata: 17 | labels: 18 | app: remotedialer-proxy 19 | spec: 20 | containers: 21 | - name: remotedialer-proxy 22 | image: rancher/remotedialer-proxy:latest 23 | imagePullPolicy: IfNotPresent 24 | env: 25 | - name: TLS_NAME 26 | value: "remotedialer-proxy" 27 | - name: CA_NAME 28 | value: "remotedialer-proxy-ca" 29 | - name: CERT_CA_NAMESPACE 30 | value: "cattle-system" 31 | - name: CERT_CA_NAME 32 | value: "remotedialer-proxy-cert" 33 | - name: SECRET 34 | value: "secret" # X-Tunnel-ID header secret 35 | - name: PROXY_PORT 36 | value: "6666" # The proxy TCP port for kube-apiserver traffic 37 | - name: PEER_PORT 38 | value: "8888" # The port used to connect to the special "imperative API" server behind the remotedialer 39 | - name: HTTPS_PORT 40 | value: "8443" # The dynamiclistener HTTPS port for /connect 41 | ports: 42 | - containerPort: 6666 43 | name: proxy 44 | - containerPort: 8443 45 | name: https 46 | - containerPort: 8888 47 | name: peer 48 | 49 | --- 50 | apiVersion: v1 51 | kind: Service 52 | metadata: 53 | name: remotedialer-proxy 54 | namespace: cattle-system 55 | labels: 56 | app: remotedialer-proxy 57 | spec: 58 | type: ClusterIP 59 | selector: 60 | app: remotedialer-proxy 61 | ports: 62 | - name: proxy 63 | port: 6666 64 | targetPort: proxy 65 | - name: https 66 | port: 8443 67 | targetPort: https 68 | 69 | --- 70 | apiVersion: rbac.authorization.k8s.io/v1 71 | kind: Role 72 | metadata: 73 | name: remotedialer-proxy-secret-access 74 | namespace: cattle-system 75 | rules: 76 | - apiGroups: [""] 77 | resources: ["secrets"] 78 | verbs: ["get", "create", "update"] 79 | 80 | --- 81 | apiVersion: rbac.authorization.k8s.io/v1 82 | kind: RoleBinding 83 | metadata: 84 | name: remotedialer-proxy-secret-access-binding 85 | namespace: cattle-system 86 | subjects: 87 | - kind: ServiceAccount 88 | name: default 89 | namespace: cattle-system 90 | roleRef: 91 | kind: Role 92 | name: remotedialer-proxy-secret-access 93 | apiGroup: rbac.authorization.k8s.io 94 | -------------------------------------------------------------------------------- /connection.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net" 7 | "time" 8 | 9 | "github.com/rancher/remotedialer/metrics" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type connection struct { 14 | err error 15 | writeDeadline time.Time 16 | backPressure *backPressure 17 | buffer *readBuffer 18 | addr addr 19 | session *Session 20 | connID int64 21 | } 22 | 23 | func newConnection(connID int64, session *Session, proto, address string) *connection { 24 | c := &connection{ 25 | addr: addr{ 26 | proto: proto, 27 | address: address, 28 | }, 29 | connID: connID, 30 | session: session, 31 | } 32 | c.backPressure = newBackPressure(c) 33 | c.buffer = newReadBuffer(connID, c.backPressure) 34 | metrics.IncSMTotalAddConnectionsForWS(session.clientKey, proto, address) 35 | return c 36 | } 37 | 38 | func (c *connection) tunnelClose(err error) { 39 | c.writeErr(err) 40 | c.doTunnelClose(err) 41 | } 42 | 43 | func (c *connection) doTunnelClose(err error) { 44 | if c.err != nil { 45 | return 46 | } 47 | 48 | metrics.IncSMTotalRemoveConnectionsForWS(c.session.clientKey, c.addr.Network(), c.addr.String()) 49 | c.err = err 50 | if c.err == nil { 51 | c.err = io.ErrClosedPipe 52 | } 53 | 54 | c.buffer.Close(c.err) 55 | } 56 | 57 | func (c *connection) OnData(r io.Reader) error { 58 | if PrintTunnelData { 59 | defer func() { 60 | logrus.Debugf("ONDATA [%d] %s", c.connID, c.buffer.Status()) 61 | }() 62 | } 63 | return c.buffer.Offer(r) 64 | } 65 | 66 | func (c *connection) Close() error { 67 | c.session.closeConnection(c.connID, io.EOF) 68 | c.backPressure.Close() 69 | return nil 70 | } 71 | 72 | func (c *connection) Read(b []byte) (int, error) { 73 | n, err := c.buffer.Read(b) 74 | metrics.AddSMTotalReceiveBytesOnWS(c.session.clientKey, float64(n)) 75 | if PrintTunnelData { 76 | logrus.Debugf("READ [%d] %s %d %v", c.connID, c.buffer.Status(), n, err) 77 | } 78 | return n, err 79 | } 80 | 81 | func (c *connection) Write(b []byte) (int, error) { 82 | if c.err != nil { 83 | return 0, io.ErrClosedPipe 84 | } 85 | ctx, cancel := context.WithCancel(context.Background()) 86 | if !c.writeDeadline.IsZero() { 87 | ctx, cancel = context.WithDeadline(ctx, c.writeDeadline) 88 | go func(ctx context.Context) { 89 | select { 90 | case <-ctx.Done(): 91 | if ctx.Err() == context.DeadlineExceeded { 92 | c.Close() 93 | } 94 | return 95 | } 96 | }(ctx) 97 | } 98 | 99 | c.backPressure.Wait(cancel) 100 | msg := newMessage(c.connID, b) 101 | metrics.AddSMTotalTransmitBytesOnWS(c.session.clientKey, float64(len(msg.Bytes()))) 102 | return c.session.writeMessage(c.writeDeadline, msg) 103 | } 104 | 105 | func (c *connection) OnPause() { 106 | c.backPressure.OnPause() 107 | } 108 | 109 | func (c *connection) OnResume() { 110 | c.backPressure.OnResume() 111 | } 112 | 113 | func (c *connection) Pause() { 114 | msg := newPause(c.connID) 115 | _, _ = c.session.writeMessage(c.writeDeadline, msg) 116 | } 117 | 118 | func (c *connection) Resume() { 119 | msg := newResume(c.connID) 120 | _, _ = c.session.writeMessage(c.writeDeadline, msg) 121 | } 122 | 123 | func (c *connection) writeErr(err error) { 124 | if err != nil { 125 | msg := newErrorMessage(c.connID, err) 126 | metrics.AddSMTotalTransmitErrorBytesOnWS(c.session.clientKey, float64(len(msg.Bytes()))) 127 | deadline := time.Now().Add(SendErrorTimeout) 128 | if _, err2 := c.session.writeMessage(deadline, msg); err2 != nil { 129 | logrus.Warnf("[%d] encountered error %q while writing error %q to close remotedialer", c.connID, err2, err) 130 | } 131 | } 132 | } 133 | 134 | func (c *connection) LocalAddr() net.Addr { 135 | return c.addr 136 | } 137 | 138 | func (c *connection) RemoteAddr() net.Addr { 139 | return c.addr 140 | } 141 | 142 | func (c *connection) SetDeadline(t time.Time) error { 143 | if err := c.SetReadDeadline(t); err != nil { 144 | return err 145 | } 146 | return c.SetWriteDeadline(t) 147 | } 148 | 149 | func (c *connection) SetReadDeadline(t time.Time) error { 150 | c.buffer.deadline = t 151 | return nil 152 | } 153 | 154 | func (c *connection) SetWriteDeadline(t time.Time) error { 155 | c.writeDeadline = t 156 | return nil 157 | } 158 | 159 | type addr struct { 160 | proto string 161 | address string 162 | } 163 | 164 | func (a addr) Network() string { 165 | return a.proto 166 | } 167 | 168 | func (a addr) String() string { 169 | return a.address 170 | } 171 | -------------------------------------------------------------------------------- /dialer.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "context" 5 | "net" 6 | ) 7 | 8 | type Dialer func(ctx context.Context, network, address string) (net.Conn, error) 9 | 10 | func (s *Server) HasSession(clientKey string) bool { 11 | _, err := s.sessions.getDialer(clientKey) 12 | return err == nil 13 | } 14 | 15 | func (s *Server) Dialer(clientKey string) Dialer { 16 | return func(ctx context.Context, network, address string) (net.Conn, error) { 17 | d, err := s.sessions.getDialer(clientKey) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | return d(ctx, network, address) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/remotedialer-flow.drawio: -------------------------------------------------------------------------------- 1 | 7Vvfc5s4EP5rPHP3EAYQYPzoOklzM+01N0mv7VMGg2zTYssn5NjOX3+SkcBIAjsBQjJ39kNg0Q/87Wp3v5UyAJPl7iMO1ovPKILJwDaj3QBcDmzbGjoW/cMk+0zij+xMMMdxxBsVgrv4CXKhyaWbOIJpqSFBKCHxuiwM0WoFQ1KSBRijbbnZDCXlWdfBHCqCuzBIVOm3OCIL/itcs5DfwHi+EDNbJn8yDcJfc4w2Kz7fwAbXh0/2eBmIsXj7dBFEaHs0KbgagAlGiGRXy90EJgxbAVvW77riaf7eGK7IOR0e0ew6/nL7Gd9sH5K///zj22q2u+CjPAbJhuNxB/EjxPyVyV7AdPihkA1lDcCH7SIm8G4dhOzplhoGlS3IMuGP6QAkphCPk3i+ojKCWAP1hcXstDncychQi4NoCQne0ybiKeBgcmOzHH6/LVRnC9UtjtQGPC4MuLnM87ELyOgFR+05CLoKhMJUZQxhRC2P3yJMFmiOVkFyVUg/FCib9K5o8wkxBA/Y/oSE7PkyCjYElZFPgilMaMcI4glKED5MDLzDt04F7NVqFYBhEpD4sbxodGDyrrcoplPkigO+pLihVx6CBHgOCe9VqGSMcbA/arZmDdLqeRxXP8/1y9rTi+wNKnp7J35VijY4hMqvOhhaDu7Lbc9WTG+SxHClWt7bWL0KXP2v3pGC4NeUeT9zPNfhSH34ml1ulsk4JGx55Wh9YivvFqUxiRFDbYoIQctKOI/wRhuSxCs4yQOc2RLcZbRt01XQdjRg+11hbaue8g6mKYOr2l7NPqONbK++aq/WSAOhPeoMQ++dYej47kkMbbMrDL/75oP9z1O4XMM77y/y8PX2J7hwnNrIvEKrVkIxxQzvv7P+BvBcIfhxEJimIwSXOz5Fdrc/vruFOKa/GWIuPCu6S6Fcq78sLtVgBLJ2WVSuw7JhttBIkUBZCyzzopLftunvzdZDhZlrwKy0fNsuW37OeI4s39MYvtOZ3Stw9Y5R7j0FRkONh7U0IOXC1lFSg1TvKOX+UaCki0Ovi5IahtjSS7sIQgG/S+CMtISnJeE56h1PS/VlvZuda0kOrP/FKdKf1yXVcBeT7yIu0+sshrv8rgjg7EbE7xZjseU1DLJ68mpLmZkSeFqi5K5EscU8VZS86r20lPwFTFqfSqgO7XJ8P6aSZTofAHrhJcz7TCk39Obs6ub+/pY+Zr6Zads0DENrmwdeWLYn4dFCagoQaxzfMo6izHRhGj8F08N4bB6ONR3c/TBwL6tTwiv2rXMDvEbFxx7kVcpjA61ZgpVO48I0hpaYhGtQ1CkaGuyFXTakYXkANJulsGmZRZ84mYpm22QHdpkcnCAGr8cBLL4mTpKALHj1xQIcNVJ2QefeIu5+r7iDV8H9aKG8+3Uy6lNflj5nfyt0WUkTNOVKLV12u8LLV/DqHyS5ptA/E7TUEvo7ooIukAB9A1RQwRPDIJpuZjPNtqzYkAj3SUzRxeA0tNNMD5+muSDfyP6S7URweZq5ZMttCWqpmg40uz+6Be52RiaHCtLrmKKpYEww+pUfDrDLaAoFLHdzdj7CmCVoGy4CTIwoxjTBfogCElQopQVQPcl+gaV6TUu0OUa1jR01Paqq21wHm5TCek3ZzGYJGflYwPCXgnO4wY/5HiVcRWN2uoPZdhKkaRyWcS97k5yhG74rePmPQcHeVY5+4RggzyeyrpZTSjDy5EOTYOS96XBykV5buW8z3Tg329AbjtiHURefbudVyBpSN1fai3TlWkPFRrk6kFQwduRdopaKFp5UInTkwzXyew2l9qCmaFH07p672vWbXR1VzFo096bc8syilA06sSNHZJH5PPV2pLYH1XbUWvFL3fOQil/mx6v7AXt15sMzjemsqpda19Bn325qXRXbm0e1rqFTVphwz40dpnbUVv2F9oCBoyZFbyb9bHQQwZYzpb7TT6Am+v8nSmdHjjeS2Ni+bwCz+NhlG/M9w3OLp9Lw5yY9tuMa3qj4+M+apaVIZvtDKSOyTry01B6U2r9KRqT1cCo/4Vs51aEtSeJ1Co/YXpigTXRuxaeR2xrKiYqrIXi2rkDRxjk+veNS62CvuQlrnOe3Xs66ag8vHtOumoZ9FXmBeo7gvNSta+3l0aNUwy9K+s+q4vsT9m1XwSNVv53wkWZ7Lm6t8v47ey7nnk/sdTE69adVOtCWB0bl9WVa4ITO3slB06ZnYCoUydxi/h9YWYZR/JsbuPoX -------------------------------------------------------------------------------- /docs/remotedialer-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rancher/remotedialer/60bd97a03d38ec042a81f34537db64906ff2d8bb/docs/remotedialer-flow.png -------------------------------------------------------------------------------- /docs/remotedialer.drawio: -------------------------------------------------------------------------------- 1 | 5VxLc6M4EP41PsaFxPuYONns1M7UpCqHnZy2ZJBtNhh5QI7t/fUrjMRDwjZ2wMTFKdAIIfX3dau7JWekT5bb5xitFj+Ij8MR1PztSH8cQQhsA7A/qWSXSVzdyATzOPB5o0LwGvyHuVDj0nXg46TSkBIS0mBVFXokirBHKzIUx2RTbTYjYfWrKzTHiuDVQ6Eq/Tvw6SKTOqZWyP/EwXwhvgw0/mSJRGMuSBbIJ5uSSH8a6ZOYEJpdLbcTHKbKE3rJ3vvjwNN8YDGOaJMX7v55QO6vCTGfv/+c7p6tp2+/f96ZWS8fKFzzCY+gFbL+HvzgIx003XFNWL/X6UgfKldz/nf/RrJCUe0rU+S9z2Oyjvw7j4QkHun36biigAYoPNrlVAhecfyBYyFmk5zKTZksG4Ai3s9ESGFlhHA/KpxqCLDHm0VA8esKeenTDSM0ky3oMuSP2QhowKhxHwbziMkoSRsgfhfiWfGJtCneHkQK5Pgzw8FkiWm8Y034C9DilOE2Y4Ax5EBtCg7mTFuU+Oc4Y5s3RZz587z/ghzsgvPjDK7YB7lSgilJAhLV4/QZ1UtKrkeiBdWbzVQPa1QPzA5175zWfZLp/geKmE/KTWUa11jEjYFiaFVQdHesOw1BAXDcFSYA1IAi6RNH/n26CrE7L0QMIK+qwkLfGrvD24D+4k/S67dUzsaf3T1uS80ed+ImYnNJX7rTxprmCkn2qm45QlC8vb+rvP6C44DphLEmE2bTwL6yNEqIsamSdezhI0rSuU4oiueYnvIuKgVKAJs1+ApZjENEg4/qeOsQ5194IQGbScEws8owYGjVLrJ58rfKi6zUkSlR1ZX6ydSg9LPnXz7rT1ASXoeSsCEntSodzRNkbJN3+k3yzmyJd3lwcC3i6V/KF/bHO+g05J3TJ+90Eajk/q6Ic86lngH6dXmHU5g8zNkkE5Ye3m4AZMPLAyCtw6gUWK1b/YUWfNEy1eZyYzY0+4yufdm9JaU3AGgX270t2b2ldtW16TfISHld6KaTUtn8gQUbGj8wFEzaM/66nPRM4z+wyNvu8WWe3cgpi7eOP3Lc2jRsq6lh9xpI2nLdAl4YSDqSVet2s+WcwYx2pWartEFyxBO5Eqer1Ut2kfXYrsNwTzuMGCP/YT2bHao23oLDcOTwzgJ5EHDKZzgS3q35C/GxY8pPK8UvMU6SdYxvV/1A0xs47DpvAIRTbV/77RerPhmqpX6+6vfTrORcv9+iqxfhxGlX7/bp6h3J1UPr8tzNlbw9I+C1gzjBwzbCCG10Sa2gYOyb6O8IfbsKNAyjIft6jTMsWwoPDKW+3jh/kKJa6CpdtRVryBZzjVgDfumKGLxiScyEDZld2hbvgdpAk4qxLBa+2LEW5wBEZ/r1PavRgmctvKQBrYqfHGvAPUGi2s2lemd9olTTldc1m3rdftM7JaJUqqyNva6Ud+laY16e63Zdye3qV3G7alWCb4wnCvlZhE+rdE9oTN7xJDuk8ujjGVrvc5BZEIZCGpEIl5IJj3Eu5baSTiwD30+/VJuVVD27j5KFxO7PHSBxqpoXi3aJrFYNWeWaQXvJh5p3rzCOB4yI2zMiYlu+hIgXBpjb8zAwMWC1StK3lehqxDgwK5ER6d1K1AgqDBKKo0GjAkDfsKhbvt5+f3coiJj2Reu7KH+3D4i6D5wCMiAbkRFx3J4RUbdGs/X9L7wbLipAU09NXBeWg9nJsHGBds+4qAmKH6Aw3QMcLCaGVbdneFVYDDUi3uBpQrx3TMeTQS35tlQAcqyxOHN56jch3cGjhseJ+AHIUHGBwGxqNlCtvLUHjRqRTfmhhqEiA9zmyHSVuRg1YRlzY98ehwsL1Jut/Z1hIkr05V0Rf45f+S2J6YLMSYTCp0Iq6aho852kh0j2uvoXU7rjP2hFa0pG9VvW+QbeW+lJ5+dVjaY/j8gaqmg23tD4HDbqqZXpjuJk/DAwZ+aIhDJ3ZpZiNfZVrUYtIFcPbw0VGQjUNca5KjJqVFY+BD0UXIAmn3X/Auu/qVYuV2idsHkPF5dzIubOcKkpYIZk4Lh0F5ix2+L/XmQ7/8V/D9Gf/gc= -------------------------------------------------------------------------------- /docs/remotedialer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rancher/remotedialer/60bd97a03d38ec042a81f34537db64906ff2d8bb/docs/remotedialer.png -------------------------------------------------------------------------------- /dummy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | "sync/atomic" 8 | "time" 9 | ) 10 | 11 | var ( 12 | counter int64 13 | listen string 14 | ) 15 | 16 | func main() { 17 | flag.StringVar(&listen, "listen", ":8125", "Listen address") 18 | flag.Parse() 19 | 20 | fmt.Println("listening ", listen) 21 | http.ListenAndServe(listen, http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 22 | start := time.Now() 23 | next := atomic.AddInt64(&counter, 1) 24 | http.FileServer(http.Dir("./")).ServeHTTP(rw, req) 25 | fmt.Println("request", next, time.Now().Sub(start)) 26 | })) 27 | } 28 | -------------------------------------------------------------------------------- /examples/fakek8s/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23 as builder 2 | WORKDIR /app 3 | COPY . . 4 | RUN go mod download 5 | RUN go build -o /app/tcp-client main.go 6 | 7 | FROM debian:bookworm-slim 8 | COPY --from=builder /app/tcp-client . 9 | 10 | RUN ls . 11 | 12 | CMD ["./tcp-client"] 13 | -------------------------------------------------------------------------------- /examples/fakek8s/fakek8s.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: fakek8s-deployment 5 | namespace: cattle-system 6 | labels: 7 | app: fakek8s 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: fakek8s 13 | template: 14 | metadata: 15 | labels: 16 | app: fakek8s 17 | spec: 18 | containers: 19 | - name: fakek8s 20 | image: rancher/fakek8s:latest 21 | imagePullPolicy: IfNotPresent 22 | env: 23 | - name: TARGET_HOST 24 | value: "api-extension.cattle-system.svc.cluster.local" 25 | - name: TARGET_PORT 26 | value: "6666" 27 | - name: SEND_INTERVAL 28 | value: "1" 29 | -------------------------------------------------------------------------------- /examples/fakek8s/go.mod: -------------------------------------------------------------------------------- 1 | module dummy/fakek8s 2 | 3 | go 1.23 4 | -------------------------------------------------------------------------------- /examples/fakek8s/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "os" 8 | "os/signal" 9 | "strconv" 10 | "syscall" 11 | "time" 12 | ) 13 | 14 | var ( 15 | targetHost = "api-extension.cattle-system.svc.cluster.local" 16 | targetPort = 6666 17 | retryDelay = 5 * time.Second 18 | ) 19 | 20 | func init() { 21 | if host, ok := os.LookupEnv("TARGET_HOST"); ok { 22 | targetHost = host 23 | } 24 | 25 | if portStr, ok := os.LookupEnv("TARGET_PORT"); ok { 26 | if p, err := strconv.Atoi(portStr); err != nil { 27 | fmt.Printf("Could not parse TARGET_PORT=%q: %v. Using default %d.\n", 28 | portStr, err, targetPort) 29 | } else { 30 | targetPort = p 31 | } 32 | } 33 | 34 | if intervalStr, ok := os.LookupEnv("SEND_INTERVAL"); ok { 35 | if i, err := strconv.Atoi(intervalStr); err != nil { 36 | fmt.Printf("Could not parse SEND_INTERVAL=%q: %v. Using default %v.\n", 37 | intervalStr, err, retryDelay) 38 | } else { 39 | retryDelay = time.Duration(i) * time.Second 40 | } 41 | } 42 | } 43 | 44 | func echoHandler(ctx context.Context, conn net.Conn) { 45 | defer conn.Close() 46 | go func() { 47 | <-ctx.Done() 48 | fmt.Println("echoHandler: context canceled; closing connection.") 49 | _ = conn.Close() 50 | }() 51 | 52 | buffer := make([]byte, 1024) 53 | for { 54 | n, err := conn.Read(buffer) 55 | if err != nil { 56 | fmt.Printf("Connection closed or error occurred: %v\n", err) 57 | return 58 | } 59 | 60 | fmt.Println("Received from Server:", string(buffer[:n])) 61 | 62 | // Echo back the received data 63 | if _, err := conn.Write(buffer[:n]); err != nil { 64 | fmt.Printf("Error sending data back: %v\n", err) 65 | return 66 | } 67 | 68 | fmt.Println("Sent back to Server:", string(buffer[:n])) 69 | } 70 | } 71 | 72 | func main() { 73 | ctx, cancel := context.WithCancel(context.Background()) 74 | defer cancel() 75 | 76 | sigChan := make(chan os.Signal, 1) 77 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 78 | go func() { 79 | <-sigChan 80 | fmt.Println("main: received shutdown signal; canceling context...") 81 | cancel() 82 | }() 83 | 84 | for { 85 | select { 86 | case <-ctx.Done(): 87 | fmt.Println("main: context canceled; exiting dial loop.") 88 | return 89 | default: 90 | } 91 | 92 | fmt.Printf("Attempting to connect to %s:%d...\n", targetHost, targetPort) 93 | 94 | conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", targetHost, targetPort)) 95 | if err != nil { 96 | fmt.Printf("Failed to connect: %v. Retrying in %v...\n", err, retryDelay) 97 | time.Sleep(retryDelay) 98 | continue 99 | } 100 | 101 | fmt.Println("Connected to the server.") 102 | 103 | // Send a welcome message 104 | welcomeMessage := "Hello, server! Client has connected.\nPlease type any word and hit enter:" 105 | if _, err = conn.Write([]byte(welcomeMessage)); err != nil { 106 | fmt.Printf("Error sending welcome message: %v\n", err) 107 | conn.Close() 108 | continue 109 | } 110 | 111 | echoHandler(ctx, conn) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /examples/proxyclient/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "net" 8 | "os" 9 | "os/signal" 10 | "path/filepath" 11 | "strings" 12 | "syscall" 13 | 14 | "github.com/rancher/remotedialer/forward" 15 | proxyclient "github.com/rancher/remotedialer/proxyclient" 16 | "github.com/rancher/wrangler/v3/pkg/generated/controllers/core" 17 | "github.com/sirupsen/logrus" 18 | "k8s.io/client-go/tools/clientcmd" 19 | "k8s.io/client-go/util/homedir" 20 | ) 21 | 22 | var ( 23 | namespace = "cattle-system" 24 | label = "app=api-extension" 25 | certSecretName = "api-extension-ca-name" 26 | certServerName = "api-extension-tls-name" 27 | connectSecret = "api-extension" 28 | ports = []string{"5555:5555"} 29 | fakeImperativeAPIAddr = "0.0.0.0:6666" 30 | ) 31 | 32 | func init() { 33 | if val, ok := os.LookupEnv("NAMESPACE"); ok { 34 | namespace = val 35 | } 36 | if val, ok := os.LookupEnv("LABEL"); ok { 37 | label = val 38 | } 39 | if val, ok := os.LookupEnv("CERT_SECRET_NAME"); ok { 40 | certSecretName = val 41 | } 42 | if val, ok := os.LookupEnv("CERT_SERVER_NAME"); ok { 43 | certServerName = val 44 | } 45 | if val, ok := os.LookupEnv("CONNECT_SECRET"); ok { 46 | connectSecret = val 47 | } 48 | if val, ok := os.LookupEnv("PORTS"); ok { 49 | ports = strings.Split(val, ",") 50 | } 51 | if val, ok := os.LookupEnv("FAKE_IMPERATIVE_API_ADDR"); ok { 52 | fakeImperativeAPIAddr = val 53 | } 54 | } 55 | 56 | func handleConnection(ctx context.Context, conn net.Conn) { 57 | go func() { 58 | <-ctx.Done() 59 | fmt.Println("handleConnection: context canceled; closing connection.") 60 | _ = conn.Close() 61 | }() 62 | 63 | defer fmt.Println("handleConnection: exiting for", conn.RemoteAddr()) 64 | defer conn.Close() 65 | 66 | buffer := make([]byte, 1024) 67 | for { 68 | n, err := conn.Read(buffer) 69 | if err != nil { 70 | fmt.Println("Connection closed or error occurred:", err) 71 | return 72 | } 73 | fmt.Println("Received from Client", string(buffer[:n])) 74 | } 75 | } 76 | 77 | func handleKeyboardInput(ctx context.Context, conn net.Conn) { 78 | go func() { 79 | <-ctx.Done() 80 | fmt.Println("handleKeyboardInput: context canceled; closing connection.") 81 | _ = conn.Close() 82 | }() 83 | 84 | defer fmt.Println("handleKeyboardInput: exiting for", conn.RemoteAddr()) 85 | defer conn.Close() 86 | 87 | reader := bufio.NewReader(os.Stdin) 88 | for { 89 | input, err := reader.ReadByte() 90 | if err != nil { 91 | fmt.Println("Error reading keyboard input:", err) 92 | return 93 | } 94 | 95 | _, err = conn.Write([]byte{input}) 96 | if err != nil { 97 | fmt.Println("Error sending data to client:", err) 98 | return 99 | } 100 | } 101 | } 102 | 103 | func fakeImperativeAPI(ctx context.Context) error { 104 | ln, err := net.Listen("tcp", fakeImperativeAPIAddr) 105 | if err != nil { 106 | return fmt.Errorf("Error starting server on %s: %w", fakeImperativeAPIAddr, err) 107 | } 108 | fmt.Printf("Server listening on %s...\n", fakeImperativeAPIAddr) 109 | 110 | go func() { 111 | <-ctx.Done() 112 | fmt.Println("fakeImperativeAPI: context canceled; closing listener.") 113 | _ = ln.Close() 114 | }() 115 | 116 | for { 117 | conn, acceptErr := ln.Accept() 118 | if acceptErr != nil { 119 | select { 120 | case <-ctx.Done(): 121 | fmt.Println("fakeImperativeAPI: accept loop stopping; context is done.") 122 | return nil 123 | default: 124 | return fmt.Errorf("fakeImperativeAPI: error accepting connection: %w", acceptErr) 125 | } 126 | } 127 | 128 | fmt.Println("Connection established with client:", conn.RemoteAddr()) 129 | 130 | go handleConnection(ctx, conn) 131 | go handleKeyboardInput(ctx, conn) 132 | } 133 | } 134 | 135 | func main() { 136 | ctx, cancel := context.WithCancel(context.Background()) 137 | defer cancel() 138 | 139 | sigChan := make(chan os.Signal, 1) 140 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 141 | 142 | go func() { 143 | if err := fakeImperativeAPI(ctx); err != nil { 144 | logrus.Errorf("fakeImperativeAPI error: %v", err) 145 | cancel() 146 | } 147 | }() 148 | 149 | home := homedir.HomeDir() 150 | kubeConfigPath := filepath.Join(home, ".kube", "config") 151 | cfg, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath) 152 | if err != nil { 153 | panic(err.Error()) 154 | } 155 | 156 | coreFactory, err := core.NewFactoryFromConfigWithOptions(cfg, nil) 157 | if err != nil { 158 | logrus.Fatal(err) 159 | } 160 | 161 | podClient := coreFactory.Core().V1().Pod() 162 | secretContoller := coreFactory.Core().V1().Secret() 163 | 164 | portForwarder, err := forward.New(cfg, podClient, namespace, label, ports) 165 | if err != nil { 166 | logrus.Fatal(err) 167 | } 168 | 169 | proxyClient, err := proxyclient.New( 170 | ctx, 171 | connectSecret, 172 | namespace, 173 | certSecretName, 174 | certServerName, 175 | secretContoller, 176 | portForwarder, 177 | ) 178 | if err != nil { 179 | logrus.Fatal(err) 180 | } 181 | 182 | if err := coreFactory.Start(ctx, 1); err != nil { 183 | logrus.Fatal(err) 184 | } 185 | 186 | proxyClient.Run(ctx) 187 | 188 | logrus.Info("RDP Client Started... Waiting for CTRL+C") 189 | <-sigChan 190 | logrus.Info("Stopping...") 191 | 192 | cancel() 193 | proxyClient.Stop() 194 | } 195 | -------------------------------------------------------------------------------- /forward/forward.go: -------------------------------------------------------------------------------- 1 | package forward 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "math/rand" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | "time" 13 | 14 | v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1" 15 | "github.com/sirupsen/logrus" 16 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 | "k8s.io/client-go/rest" 18 | "k8s.io/client-go/tools/portforward" 19 | "k8s.io/client-go/transport/spdy" 20 | ) 21 | 22 | var ( 23 | podConnectionRetryTimeout = 1 * time.Second 24 | ) 25 | 26 | type PortForward struct { 27 | restConfig *rest.Config 28 | podClient v1.PodController 29 | namespace string 30 | labelSelector string 31 | ports []string 32 | 33 | readyCh chan struct{} 34 | readyErr chan error 35 | cancel context.CancelFunc 36 | } 37 | 38 | func New(restConfig *rest.Config, podClient v1.PodController, namespace string, labelSelector string, ports []string) (*PortForward, error) { 39 | if restConfig == nil { 40 | return nil, fmt.Errorf("restConfig must not be nil") 41 | } 42 | if podClient == nil { 43 | return nil, fmt.Errorf("podClient must not be nil") 44 | } 45 | if labelSelector == "" { 46 | return nil, fmt.Errorf("labelSelector must not be empty") 47 | } 48 | if len(ports) == 0 { 49 | return nil, fmt.Errorf("ports must not be empty") 50 | } 51 | if namespace == "" { 52 | return nil, fmt.Errorf("namespace must not be empty") 53 | } 54 | 55 | for _, p := range ports { 56 | if strings.HasPrefix(p, "0:") { 57 | return nil, fmt.Errorf("cannot bind port zero") 58 | } 59 | } 60 | 61 | return &PortForward{ 62 | restConfig: restConfig, 63 | podClient: podClient, 64 | namespace: namespace, 65 | labelSelector: labelSelector, 66 | ports: ports, 67 | readyCh: make(chan struct{}, 1), 68 | }, nil 69 | } 70 | 71 | func (r *PortForward) Stop() { 72 | r.cancel() 73 | } 74 | 75 | func (r *PortForward) Start() error { 76 | ctx, cancel := context.WithCancel(context.Background()) 77 | 78 | r.cancel = cancel 79 | r.readyCh = make(chan struct{}, 1) 80 | r.readyErr = make(chan error, 1) 81 | 82 | go func() { 83 | for { 84 | select { 85 | case <-ctx.Done(): 86 | logrus.Infoln("Goroutine stopped.") 87 | return 88 | default: 89 | err := r.runForwarder(ctx, r.readyCh, r.ports) 90 | if err != nil { 91 | if errors.Is(err, portforward.ErrLostConnectionToPod) { 92 | logrus.Errorf("Lost connection to pod: %v, retrying in %d secs.", err, podConnectionRetryTimeout/time.Second) 93 | } else { 94 | logrus.Errorf("Non-restartable error: %v", err) 95 | r.readyErr <- err 96 | return 97 | } 98 | } 99 | } 100 | } 101 | }() 102 | 103 | for { 104 | select { 105 | case <-ctx.Done(): 106 | return nil 107 | 108 | case <-r.readyCh: 109 | return nil 110 | 111 | case err := <-r.readyErr: 112 | if err != nil { 113 | return err 114 | } 115 | return nil 116 | } 117 | } 118 | } 119 | 120 | func (r *PortForward) runForwarder(ctx context.Context, readyCh chan struct{}, ports []string) error { 121 | podName, err := lookForPodName(ctx, r.namespace, r.labelSelector, r.podClient) 122 | if err != nil { 123 | return err 124 | } 125 | logrus.Infof("Selected pod %q for label %q", podName, r.labelSelector) 126 | 127 | path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", r.namespace, podName) 128 | hostIP := strings.TrimPrefix(r.restConfig.Host, "https://") 129 | serverURL := url.URL{ 130 | Scheme: "https", 131 | Path: path, 132 | Host: hostIP, 133 | } 134 | 135 | roundTripper, upgrader, err := spdy.RoundTripperFor(r.restConfig) 136 | if err != nil { 137 | return err 138 | } 139 | dialer := spdy.NewDialer(upgrader, &http.Client{ 140 | Transport: roundTripper, 141 | }, http.MethodPost, &serverURL) 142 | 143 | stdout, stderr := new(bytes.Buffer), new(bytes.Buffer) 144 | forwarder, err := portforward.New(dialer, ports, ctx.Done(), readyCh, stdout, stderr) 145 | if err != nil { 146 | return err 147 | } 148 | 149 | return forwarder.ForwardPorts() 150 | } 151 | 152 | func lookForPodName(ctx context.Context, namespace, labelSelector string, podClient v1.PodClient) (string, error) { 153 | for { 154 | select { 155 | case <-ctx.Done(): 156 | return "", ctx.Err() 157 | default: 158 | pods, err := podClient.List(namespace, metav1.ListOptions{ 159 | LabelSelector: labelSelector, 160 | }) 161 | if err != nil { 162 | return "", err 163 | } 164 | if len(pods.Items) < 1 { 165 | logrus.Debugf("no pod found with label selector %q, retrying in 1s", labelSelector) 166 | time.Sleep(time.Second) 167 | continue 168 | } 169 | i := rand.Intn(len(pods.Items)) 170 | return pods.Items[i].Name, nil 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rancher/remotedialer 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/gorilla/mux v1.8.1 7 | github.com/gorilla/websocket v1.5.3 8 | github.com/pkg/errors v0.9.1 9 | github.com/prometheus/client_golang v1.19.1 10 | github.com/rancher/dynamiclistener v0.6.1 11 | github.com/rancher/wrangler/v3 v3.0.1-rc.2 12 | github.com/sirupsen/logrus v1.9.3 13 | github.com/stretchr/testify v1.10.0 14 | k8s.io/api v0.31.1 15 | k8s.io/apimachinery v0.31.1 16 | k8s.io/client-go v0.31.1 17 | ) 18 | 19 | require ( 20 | github.com/beorn7/perks v1.0.1 // indirect 21 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 22 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 23 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 24 | github.com/evanphx/json-patch v5.9.0+incompatible // indirect 25 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 26 | github.com/go-logr/logr v1.4.2 // indirect 27 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 28 | github.com/go-openapi/jsonreference v0.20.2 // indirect 29 | github.com/go-openapi/swag v0.22.4 // indirect 30 | github.com/gogo/protobuf v1.3.2 // indirect 31 | github.com/golang/protobuf v1.5.4 // indirect 32 | github.com/google/gnostic-models v0.6.8 // indirect 33 | github.com/google/go-cmp v0.6.0 // indirect 34 | github.com/google/gofuzz v1.2.0 // indirect 35 | github.com/google/uuid v1.6.0 // indirect 36 | github.com/imdario/mergo v0.3.13 // indirect 37 | github.com/josharian/intern v1.0.0 // indirect 38 | github.com/json-iterator/go v1.1.12 // indirect 39 | github.com/mailru/easyjson v0.7.7 // indirect 40 | github.com/moby/spdystream v0.4.0 // indirect 41 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 42 | github.com/modern-go/reflect2 v1.0.2 // indirect 43 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 44 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 45 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 46 | github.com/prometheus/client_model v0.6.1 // indirect 47 | github.com/prometheus/common v0.55.0 // indirect 48 | github.com/prometheus/procfs v0.15.1 // indirect 49 | github.com/rancher/lasso v0.0.0-20240924233157-8f384efc8813 // indirect 50 | github.com/spf13/pflag v1.0.5 // indirect 51 | github.com/x448/float16 v0.8.4 // indirect 52 | golang.org/x/crypto v0.26.0 // indirect 53 | golang.org/x/net v0.28.0 // indirect 54 | golang.org/x/oauth2 v0.21.0 // indirect 55 | golang.org/x/sync v0.8.0 // indirect 56 | golang.org/x/sys v0.23.0 // indirect 57 | golang.org/x/term v0.23.0 // indirect 58 | golang.org/x/text v0.17.0 // indirect 59 | golang.org/x/time v0.3.0 // indirect 60 | google.golang.org/protobuf v1.34.2 // indirect 61 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 62 | gopkg.in/inf.v0 v0.9.1 // indirect 63 | gopkg.in/yaml.v2 v2.4.0 // indirect 64 | gopkg.in/yaml.v3 v3.0.1 // indirect 65 | k8s.io/klog/v2 v2.130.1 // indirect 66 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect 67 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect 68 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 69 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 70 | sigs.k8s.io/yaml v1.4.0 // indirect 71 | ) 72 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 2 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 3 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 11 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 13 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 14 | github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= 15 | github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 16 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 17 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 18 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 19 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 20 | github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= 21 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 22 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 23 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 24 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 25 | github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= 26 | github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 27 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 28 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 29 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 30 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 31 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 32 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 33 | github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= 34 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 35 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 36 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 37 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 38 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 39 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 40 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 41 | github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= 42 | github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= 43 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 44 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 45 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 46 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 47 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 48 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 49 | github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= 50 | github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= 51 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 52 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 53 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 54 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 55 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 56 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 57 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 58 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 59 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 60 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 61 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 62 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 63 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 64 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 65 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 66 | github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8= 67 | github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= 68 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 69 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 70 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 71 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 72 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 73 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 74 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 75 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= 76 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 77 | github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= 78 | github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= 79 | github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= 80 | github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= 81 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 82 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 83 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 84 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 85 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 86 | github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= 87 | github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= 88 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 89 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 90 | github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= 91 | github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= 92 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 93 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 94 | github.com/rancher/dynamiclistener v0.6.1 h1:sw4fxjutSedm7uIPD4I/hhAS2zIJIk3wOZLEZEElcYI= 95 | github.com/rancher/dynamiclistener v0.6.1/go.mod h1:0KhUMHy3VcGMGavTY3i1/Mr8rVM02wFqNlUzjc+Cplg= 96 | github.com/rancher/lasso v0.0.0-20240924233157-8f384efc8813 h1:V/LY8pUHZG9Kc+xEDWDOryOnCU6/Q+Lsr9QQEQnshpU= 97 | github.com/rancher/lasso v0.0.0-20240924233157-8f384efc8813/go.mod h1:IxgTBO55lziYhTEETyVKiT8/B5Rg92qYiRmcIIYoPgI= 98 | github.com/rancher/wrangler/v3 v3.0.1-rc.2 h1:sHrZTPNco7SCNw372sv51DMK9a53ra/YboL4sQJjEQM= 99 | github.com/rancher/wrangler/v3 v3.0.1-rc.2/go.mod h1:eXqcPIuGWblud9Wd1Auh7AWRHd6gs2H24asMMPuUR/s= 100 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 101 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 102 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 103 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 104 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 105 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 106 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 107 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 108 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 109 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 110 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 111 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 112 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 113 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 114 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 115 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 116 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 117 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 118 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 119 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 120 | go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= 121 | go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 122 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 123 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 124 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 125 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 126 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 127 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 128 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 129 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 130 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 131 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 132 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 133 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 134 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 135 | golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= 136 | golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 137 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 138 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 139 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 140 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 141 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 142 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 143 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 144 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 145 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 146 | golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= 147 | golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 148 | golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= 149 | golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= 150 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 151 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 152 | golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= 153 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 154 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 155 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 156 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 157 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 158 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 159 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 160 | golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= 161 | golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= 162 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 163 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 164 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 165 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 166 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 167 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 168 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 169 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 170 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 171 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 172 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 173 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 174 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 175 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 176 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 177 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 178 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 179 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 180 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 181 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 182 | k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= 183 | k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= 184 | k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= 185 | k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= 186 | k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= 187 | k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= 188 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 189 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 190 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= 191 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= 192 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= 193 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 194 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 195 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 196 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 197 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 198 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 199 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 200 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "math/rand" 11 | "strings" 12 | "sync/atomic" 13 | "time" 14 | 15 | "github.com/gorilla/websocket" 16 | ) 17 | 18 | const ( 19 | // Data is the main message type, used to transport application data 20 | Data messageType = iota + 1 21 | // Connect is a control message type, used to request opening a new connection 22 | Connect 23 | // Error is a message type used to send an error during the communication. 24 | // Any receiver of an Error message can assume the connection can be closed. 25 | // io.EOF is used for graceful termination of connections. 26 | Error 27 | // AddClient is a message type used to open a new client to the peering session 28 | AddClient 29 | // RemoveClient is a message type used to remove an existing client from a peering session 30 | RemoveClient 31 | // Pause is a message type used to temporarily stop a given connection 32 | Pause 33 | // Resume is a message type used to resume a paused connection 34 | Resume 35 | // SyncConnections is a message type used to communicate active connection IDs. 36 | // The receiver can consider any ID not present in this message as stale and free any associated resource. 37 | SyncConnections 38 | ) 39 | 40 | var ( 41 | idCounter int64 42 | legacyDeadline = (15 * time.Second).Milliseconds() 43 | ) 44 | 45 | func init() { 46 | r := rand.New(rand.NewSource(int64(time.Now().Nanosecond()))) 47 | idCounter = r.Int63() 48 | } 49 | 50 | type messageType int64 51 | 52 | type message struct { 53 | id int64 54 | err error 55 | connID int64 56 | messageType messageType 57 | bytes []byte 58 | body io.Reader 59 | proto string 60 | address string 61 | } 62 | 63 | func nextid() int64 { 64 | return atomic.AddInt64(&idCounter, 1) 65 | } 66 | 67 | func newMessage(connID int64, bytes []byte) *message { 68 | return &message{ 69 | id: nextid(), 70 | connID: connID, 71 | messageType: Data, 72 | bytes: bytes, 73 | } 74 | } 75 | 76 | func newPause(connID int64) *message { 77 | return &message{ 78 | id: nextid(), 79 | connID: connID, 80 | messageType: Pause, 81 | } 82 | } 83 | 84 | func newResume(connID int64) *message { 85 | return &message{ 86 | id: nextid(), 87 | connID: connID, 88 | messageType: Resume, 89 | } 90 | } 91 | 92 | func newConnect(connID int64, proto, address string) *message { 93 | return &message{ 94 | id: nextid(), 95 | connID: connID, 96 | messageType: Connect, 97 | bytes: []byte(fmt.Sprintf("%s/%s", proto, address)), 98 | proto: proto, 99 | address: address, 100 | } 101 | } 102 | 103 | func newErrorMessage(connID int64, err error) *message { 104 | return &message{ 105 | id: nextid(), 106 | err: err, 107 | connID: connID, 108 | messageType: Error, 109 | bytes: []byte(err.Error()), 110 | } 111 | } 112 | 113 | func newAddClient(client string) *message { 114 | return &message{ 115 | id: nextid(), 116 | messageType: AddClient, 117 | address: client, 118 | bytes: []byte(client), 119 | } 120 | } 121 | 122 | func newRemoveClient(client string) *message { 123 | return &message{ 124 | id: nextid(), 125 | messageType: RemoveClient, 126 | address: client, 127 | bytes: []byte(client), 128 | } 129 | } 130 | 131 | func newServerMessage(reader io.Reader) (*message, error) { 132 | buf := bufio.NewReader(reader) 133 | 134 | id, err := binary.ReadVarint(buf) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | connID, err := binary.ReadVarint(buf) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | mType, err := binary.ReadVarint(buf) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | m := &message{ 150 | id: id, 151 | messageType: messageType(mType), 152 | connID: connID, 153 | body: buf, 154 | } 155 | 156 | if m.messageType == Data || m.messageType == Connect { 157 | // no longer used, this is the deadline field 158 | _, err := binary.ReadVarint(buf) 159 | if err != nil { 160 | return nil, err 161 | } 162 | } 163 | 164 | if m.messageType == Connect { 165 | bytes, err := ioutil.ReadAll(io.LimitReader(buf, 100)) 166 | if err != nil { 167 | return nil, err 168 | } 169 | parts := strings.SplitN(string(bytes), "/", 2) 170 | if len(parts) != 2 { 171 | return nil, fmt.Errorf("failed to parse connect address") 172 | } 173 | m.proto = parts[0] 174 | m.address = parts[1] 175 | m.bytes = bytes 176 | } else if m.messageType == AddClient || m.messageType == RemoveClient { 177 | bytes, err := ioutil.ReadAll(io.LimitReader(buf, 100)) 178 | if err != nil { 179 | return nil, err 180 | } 181 | m.address = string(bytes) 182 | m.bytes = bytes 183 | } 184 | 185 | return m, nil 186 | } 187 | 188 | func (m *message) Err() error { 189 | if m.err != nil { 190 | return m.err 191 | } 192 | bytes, err := ioutil.ReadAll(io.LimitReader(m.body, 100)) 193 | if err != nil { 194 | return err 195 | } 196 | 197 | str := string(bytes) 198 | if str == "EOF" { 199 | m.err = io.EOF 200 | } else { 201 | m.err = errors.New(str) 202 | } 203 | return m.err 204 | } 205 | 206 | func (m *message) Bytes() []byte { 207 | return append(m.header(len(m.bytes)), m.bytes...) 208 | } 209 | 210 | func (m *message) header(space int) []byte { 211 | buf := make([]byte, 24+space) 212 | offset := 0 213 | offset += binary.PutVarint(buf[offset:], m.id) 214 | offset += binary.PutVarint(buf[offset:], m.connID) 215 | offset += binary.PutVarint(buf[offset:], int64(m.messageType)) 216 | if m.messageType == Data || m.messageType == Connect { 217 | offset += binary.PutVarint(buf[offset:], legacyDeadline) 218 | } 219 | return buf[:offset] 220 | } 221 | 222 | func (m *message) Read(p []byte) (int, error) { 223 | return m.body.Read(p) 224 | } 225 | 226 | func (m *message) WriteTo(deadline time.Time, wsConn wsConn) (int, error) { 227 | err := wsConn.WriteMessage(websocket.BinaryMessage, deadline, m.Bytes()) 228 | return len(m.bytes), err 229 | } 230 | 231 | func (m *message) String() string { 232 | switch m.messageType { 233 | case Data: 234 | if m.body == nil { 235 | return fmt.Sprintf("%d DATA [%d]: %d bytes: %s", m.id, m.connID, len(m.bytes), string(m.bytes)) 236 | } 237 | return fmt.Sprintf("%d DATA [%d]: buffered", m.id, m.connID) 238 | case Error: 239 | return fmt.Sprintf("%d ERROR [%d]: %s", m.id, m.connID, m.Err()) 240 | case Connect: 241 | return fmt.Sprintf("%d CONNECT [%d]: %s/%s", m.id, m.connID, m.proto, m.address) 242 | case AddClient: 243 | return fmt.Sprintf("%d ADDCLIENT [%s]", m.id, m.address) 244 | case RemoveClient: 245 | return fmt.Sprintf("%d REMOVECLIENT [%s]", m.id, m.address) 246 | case Pause: 247 | return fmt.Sprintf("%d PAUSE [%d]", m.id, m.connID) 248 | case Resume: 249 | return fmt.Sprintf("%d RESUME [%d]", m.id, m.connID) 250 | case SyncConnections: 251 | return fmt.Sprintf("%d SYNCCONNS [%d]", m.id, m.connID) 252 | } 253 | return fmt.Sprintf("%d UNKNOWN[%d]: %d", m.id, m.connID, m.messageType) 254 | } 255 | -------------------------------------------------------------------------------- /metrics/session_manager.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | const metricsEnv = "CATTLE_PROMETHEUS_METRICS" 10 | 11 | var prometheusMetrics = false 12 | 13 | var ( 14 | TotalAddWS = prometheus.NewCounterVec( 15 | prometheus.CounterOpts{ 16 | Subsystem: "session_server", 17 | Name: "total_add_websocket_session", 18 | Help: "Total count of added websocket sessions", 19 | }, 20 | []string{"clientkey", "peer"}) 21 | 22 | TotalRemoveWS = prometheus.NewCounterVec( 23 | prometheus.CounterOpts{ 24 | Subsystem: "session_server", 25 | Name: "total_remove_websocket_session", 26 | Help: "Total count of removed websocket sessions", 27 | }, 28 | []string{"clientkey", "peer"}) 29 | 30 | TotalAddConnectionsForWS = prometheus.NewCounterVec( 31 | prometheus.CounterOpts{ 32 | Subsystem: "session_server", 33 | Name: "total_add_connections", 34 | Help: "Total count of added connections", 35 | }, 36 | []string{"clientkey", "proto", "addr"}, 37 | ) 38 | 39 | TotalRemoveConnectionsForWS = prometheus.NewCounterVec( 40 | prometheus.CounterOpts{ 41 | Subsystem: "session_server", 42 | Name: "total_remove_connections", 43 | Help: "Total count of removed connections", 44 | }, 45 | []string{"clientkey", "proto", "addr"}, 46 | ) 47 | 48 | TotalTransmitBytesOnWS = prometheus.NewCounterVec( 49 | prometheus.CounterOpts{ 50 | Subsystem: "session_server", 51 | Name: "total_transmit_bytes", 52 | Help: "Total bytes transmitted", 53 | }, 54 | []string{"clientkey"}, 55 | ) 56 | 57 | TotalTransmitErrorBytesOnWS = prometheus.NewCounterVec( 58 | prometheus.CounterOpts{ 59 | Subsystem: "session_server", 60 | Name: "total_transmit_error_bytes", 61 | Help: "Total error bytes transmitted", 62 | }, 63 | []string{"clientkey"}, 64 | ) 65 | 66 | TotalReceiveBytesOnWS = prometheus.NewCounterVec( 67 | prometheus.CounterOpts{ 68 | Subsystem: "session_server", 69 | Name: "total_receive_bytes", 70 | Help: "Total bytes received", 71 | }, 72 | []string{"clientkey"}, 73 | ) 74 | 75 | TotalAddPeerAttempt = prometheus.NewCounterVec( 76 | prometheus.CounterOpts{ 77 | Subsystem: "session_server", 78 | Name: "total_peer_ws_attempt", 79 | Help: "Total count of attempts to establish websocket session to other rancher-server", 80 | }, 81 | []string{"peer"}, 82 | ) 83 | TotalPeerConnected = prometheus.NewCounterVec( 84 | prometheus.CounterOpts{ 85 | Subsystem: "session_server", 86 | Name: "total_peer_ws_connected", 87 | Help: "Total count of connected websocket sessions to other rancher-server", 88 | }, 89 | []string{"peer"}, 90 | ) 91 | TotalPeerDisConnected = prometheus.NewCounterVec( 92 | prometheus.CounterOpts{ 93 | Subsystem: "session_server", 94 | Name: "total_peer_ws_disconnected", 95 | Help: "Total count of disconnected websocket sessions from other rancher-server", 96 | }, 97 | []string{"peer"}, 98 | ) 99 | ) 100 | 101 | // Register registers a series of session 102 | // metrics for Prometheus. 103 | func Register() { 104 | 105 | prometheusMetrics = true 106 | 107 | // Session metrics 108 | prometheus.MustRegister(TotalAddWS) 109 | prometheus.MustRegister(TotalRemoveWS) 110 | prometheus.MustRegister(TotalAddConnectionsForWS) 111 | prometheus.MustRegister(TotalRemoveConnectionsForWS) 112 | prometheus.MustRegister(TotalTransmitBytesOnWS) 113 | prometheus.MustRegister(TotalTransmitErrorBytesOnWS) 114 | prometheus.MustRegister(TotalReceiveBytesOnWS) 115 | prometheus.MustRegister(TotalAddPeerAttempt) 116 | prometheus.MustRegister(TotalPeerConnected) 117 | prometheus.MustRegister(TotalPeerDisConnected) 118 | } 119 | 120 | func init() { 121 | if os.Getenv(metricsEnv) == "true" { 122 | Register() 123 | } 124 | } 125 | 126 | func IncSMTotalAddWS(clientKey string, peer bool) { 127 | var peerStr string 128 | if peer { 129 | peerStr = "true" 130 | } else { 131 | peerStr = "false" 132 | } 133 | if prometheusMetrics { 134 | TotalAddWS.With( 135 | prometheus.Labels{ 136 | "clientkey": clientKey, 137 | "peer": peerStr, 138 | }).Inc() 139 | } 140 | } 141 | 142 | func IncSMTotalRemoveWS(clientKey string, peer bool) { 143 | var peerStr string 144 | if prometheusMetrics { 145 | if peer { 146 | peerStr = "true" 147 | } else { 148 | peerStr = "false" 149 | } 150 | TotalRemoveWS.With( 151 | prometheus.Labels{ 152 | "clientkey": clientKey, 153 | "peer": peerStr, 154 | }).Inc() 155 | } 156 | } 157 | 158 | func AddSMTotalTransmitErrorBytesOnWS(clientKey string, size float64) { 159 | if prometheusMetrics { 160 | TotalTransmitErrorBytesOnWS.With( 161 | prometheus.Labels{ 162 | "clientkey": clientKey, 163 | }).Add(size) 164 | } 165 | } 166 | 167 | func AddSMTotalTransmitBytesOnWS(clientKey string, size float64) { 168 | if prometheusMetrics { 169 | TotalTransmitBytesOnWS.With( 170 | prometheus.Labels{ 171 | "clientkey": clientKey, 172 | }).Add(size) 173 | } 174 | } 175 | 176 | func AddSMTotalReceiveBytesOnWS(clientKey string, size float64) { 177 | if prometheusMetrics { 178 | TotalReceiveBytesOnWS.With( 179 | prometheus.Labels{ 180 | "clientkey": clientKey, 181 | }).Add(size) 182 | } 183 | } 184 | 185 | func IncSMTotalAddConnectionsForWS(clientKey, proto, addr string) { 186 | if prometheusMetrics { 187 | TotalAddConnectionsForWS.With( 188 | prometheus.Labels{ 189 | "clientkey": clientKey, 190 | "proto": proto, 191 | "addr": addr, 192 | }).Inc() 193 | } 194 | } 195 | 196 | func IncSMTotalRemoveConnectionsForWS(clientKey, proto, addr string) { 197 | if prometheusMetrics { 198 | TotalRemoveConnectionsForWS.With( 199 | prometheus.Labels{ 200 | "clientkey": clientKey, 201 | "proto": proto, 202 | "addr": addr, 203 | }).Inc() 204 | } 205 | } 206 | 207 | func IncSMTotalAddPeerAttempt(peer string) { 208 | if prometheusMetrics { 209 | TotalAddPeerAttempt.With( 210 | prometheus.Labels{ 211 | "peer": peer, 212 | }).Inc() 213 | } 214 | } 215 | 216 | func IncSMTotalPeerConnected(peer string) { 217 | if prometheusMetrics { 218 | TotalPeerConnected.With( 219 | prometheus.Labels{ 220 | "peer": peer, 221 | }).Inc() 222 | } 223 | } 224 | 225 | func IncSMTotalPeerDisConnected(peer string) { 226 | if prometheusMetrics { 227 | TotalPeerDisConnected.With( 228 | prometheus.Labels{ 229 | "peer": peer, 230 | }).Inc() 231 | 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /peer.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "strings" 10 | "time" 11 | 12 | "github.com/gorilla/websocket" 13 | "github.com/rancher/remotedialer/metrics" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var ( 18 | Token = "X-API-Tunnel-Token" 19 | ID = "X-API-Tunnel-ID" 20 | ) 21 | 22 | func (s *Server) AddPeer(url, id, token string) { 23 | if s.PeerID == "" || s.PeerToken == "" { 24 | return 25 | } 26 | 27 | ctx, cancel := context.WithCancel(context.Background()) 28 | peer := peer{ 29 | url: url, 30 | id: id, 31 | token: token, 32 | cancel: cancel, 33 | } 34 | 35 | logrus.Infof("Adding peer %s, %s", url, id) 36 | 37 | s.peerLock.Lock() 38 | defer s.peerLock.Unlock() 39 | 40 | if p, ok := s.peers[id]; ok { 41 | if p.equals(peer) { 42 | return 43 | } 44 | p.cancel() 45 | } 46 | 47 | s.peers[id] = peer 48 | go peer.start(ctx, s) 49 | } 50 | 51 | func (s *Server) RemovePeer(id string) { 52 | s.peerLock.Lock() 53 | defer s.peerLock.Unlock() 54 | 55 | if p, ok := s.peers[id]; ok { 56 | logrus.Infof("Removing peer %s", id) 57 | p.cancel() 58 | } 59 | delete(s.peers, id) 60 | } 61 | 62 | type peer struct { 63 | url, id, token string 64 | cancel func() 65 | } 66 | 67 | func (p peer) equals(other peer) bool { 68 | return p.url == other.url && 69 | p.id == other.id && 70 | p.token == other.token 71 | } 72 | 73 | func (p *peer) start(ctx context.Context, s *Server) { 74 | headers := http.Header{ 75 | ID: {s.PeerID}, 76 | Token: {s.PeerToken}, 77 | } 78 | 79 | dialer := &websocket.Dialer{ 80 | TLSClientConfig: &tls.Config{ 81 | InsecureSkipVerify: true, 82 | }, 83 | HandshakeTimeout: HandshakeTimeOut, 84 | } 85 | ctx = context.WithValue(ctx, ContextKeyCaller, fmt.Sprintf("Peer url:%s, id:%s", p.url, p.id)) 86 | 87 | outer: 88 | for { 89 | select { 90 | case <-ctx.Done(): 91 | break outer 92 | default: 93 | } 94 | 95 | metrics.IncSMTotalAddPeerAttempt(p.id) 96 | ws, _, err := dialer.Dial(p.url, headers) 97 | if err != nil { 98 | logrus.Errorf("Failed to connect to peer %s [local ID=%s]: %v", p.url, s.PeerID, err) 99 | time.Sleep(5 * time.Second) 100 | continue 101 | } 102 | metrics.IncSMTotalPeerConnected(p.id) 103 | 104 | session := NewClientSession(func(string, string) bool { return true }, ws) 105 | session.dialer = func(ctx context.Context, network, address string) (net.Conn, error) { 106 | parts := strings.SplitN(network, "::", 2) 107 | if len(parts) != 2 { 108 | return nil, fmt.Errorf("invalid clientKey/proto: %s", network) 109 | } 110 | d := s.Dialer(parts[0]) 111 | return d(ctx, parts[1], address) 112 | } 113 | 114 | s.sessions.addListener(session) 115 | _, err = session.Serve(ctx) 116 | s.sessions.removeListener(session) 117 | session.Close() 118 | 119 | if err != nil { 120 | logrus.Errorf("Failed to serve peer connection %s: %v", p.id, err) 121 | } 122 | 123 | ws.Close() 124 | time.Sleep(5 * time.Second) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /proxy/config.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | type Config struct { 10 | TLSName string // certificate client name (SAN) 11 | CAName string // certificate authority secret name 12 | CertCANamespace string // certificate secret namespace 13 | CertCAName string // certificate secret name 14 | Secret string // remotedialer secret 15 | ProxyPort int // tcp remotedialer-proxy port 16 | PeerPort int // cluster-external service port 17 | HTTPSPort int // https remotedialer-proxy port 18 | } 19 | 20 | func requiredString(key string) (string, error) { 21 | value := os.Getenv(key) 22 | if value == "" { 23 | return "", fmt.Errorf("%s cannot be empty", key) 24 | } 25 | return value, nil 26 | } 27 | 28 | func requiredPort(key string) (int, error) { 29 | valueStr := os.Getenv(key) 30 | port, err := strconv.Atoi(valueStr) 31 | if err != nil { 32 | return 0, fmt.Errorf("failed to read %s: %w", key, err) 33 | } 34 | if port <= 0 { 35 | return 0, fmt.Errorf("%s should be greater than 0", key) 36 | } 37 | return port, nil 38 | } 39 | 40 | func ConfigFromEnvironment() (*Config, error) { 41 | var err error 42 | var config Config 43 | 44 | if config.TLSName, err = requiredString("TLS_NAME"); err != nil { 45 | return nil, err 46 | } 47 | if config.CAName, err = requiredString("CA_NAME"); err != nil { 48 | return nil, err 49 | } 50 | if config.CertCANamespace, err = requiredString("CERT_CA_NAMESPACE"); err != nil { 51 | return nil, err 52 | } 53 | if config.CertCAName, err = requiredString("CERT_CA_NAME"); err != nil { 54 | return nil, err 55 | } 56 | if config.Secret, err = requiredString("SECRET"); err != nil { 57 | return nil, err 58 | } 59 | if config.ProxyPort, err = requiredPort("PROXY_PORT"); err != nil { 60 | return nil, err 61 | } 62 | if config.PeerPort, err = requiredPort("PEER_PORT"); err != nil { 63 | return nil, err 64 | } 65 | if config.HTTPSPort, err = requiredPort("HTTPS_PORT"); err != nil { 66 | return nil, err 67 | } 68 | 69 | return &config, nil 70 | } 71 | -------------------------------------------------------------------------------- /proxy/server.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "math/rand" 8 | "net" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/gorilla/mux" 13 | "github.com/rancher/dynamiclistener" 14 | "github.com/rancher/dynamiclistener/server" 15 | 16 | "github.com/rancher/wrangler/v3/pkg/generated/controllers/core" 17 | "github.com/sirupsen/logrus" 18 | "k8s.io/client-go/rest" 19 | 20 | "github.com/rancher/remotedialer" 21 | ) 22 | 23 | const ( 24 | listClientsRetryCount = 10 25 | listClientSleepTime = 1 * time.Second 26 | ) 27 | 28 | func runProxyListener(ctx context.Context, cfg *Config, server *remotedialer.Server) error { 29 | l, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", cfg.ProxyPort)) //this RDP app starts only once and always running 30 | if err != nil { 31 | return err 32 | } 33 | defer l.Close() 34 | 35 | for { 36 | conn, err := l.Accept() // the client of 6666 is kube-apiserver, according to the APIService object spec, just to this TCP 6666 37 | if err != nil { 38 | logrus.Errorf("proxy TCP connection accept failed: %v", err) 39 | continue 40 | } 41 | 42 | go func() { 43 | var retryTimes = 0 44 | for { 45 | clients := server.ListClients() 46 | if len(clients) == 0 { 47 | retryTimes++ 48 | if retryTimes > listClientsRetryCount { 49 | conn.Close() 50 | return 51 | } 52 | 53 | logrus.Info("proxy TCP connection failed: no clients, retrying in a sec") 54 | time.Sleep(listClientSleepTime) 55 | } else { 56 | client := clients[rand.Intn(len(clients))] 57 | peerAddr := fmt.Sprintf(":%d", cfg.PeerPort) // rancher's special https server for imperative API 58 | clientConn, err := server.Dialer(client)(ctx, "tcp", peerAddr) 59 | if err != nil { 60 | logrus.Errorf("proxy dialing %s failed: %v", peerAddr, err) 61 | conn.Close() 62 | return 63 | } 64 | 65 | go pipe(conn, clientConn) 66 | go pipe(clientConn, conn) 67 | break 68 | } 69 | } 70 | }() 71 | } 72 | } 73 | 74 | func pipe(a, b net.Conn) { 75 | defer func(a net.Conn) { 76 | if err := a.Close(); err != nil { 77 | logrus.Errorf("proxy TCP connection close failed: %v", err) 78 | } 79 | }(a) 80 | defer func(b net.Conn) { 81 | if err := b.Close(); err != nil { 82 | logrus.Errorf("proxy TCP connection close failed: %v", err) 83 | } 84 | }(b) 85 | n, err := io.Copy(a, b) 86 | if err != nil { 87 | logrus.Errorf("proxy copy failed: %v", err) 88 | return 89 | } 90 | logrus.Debugf("proxy copied %d bytes to %v from %v", n, a.LocalAddr(), b.LocalAddr()) 91 | } 92 | 93 | func Start(cfg *Config, restConfig *rest.Config) error { 94 | logrus.SetLevel(logrus.DebugLevel) 95 | ctx := context.Background() 96 | 97 | // Setting Up Default Authorizer 98 | authorizer := func(req *http.Request) (string, bool, error) { 99 | id := req.Header.Get("X-API-Tunnel-Secret") 100 | if id != cfg.Secret { 101 | return "", false, fmt.Errorf("X-API-Tunnel-Secret not specified in request header") 102 | } 103 | return id, true, nil 104 | } 105 | 106 | // Initializing Remote Dialer Server 107 | remoteDialerServer := remotedialer.New(authorizer, remotedialer.DefaultErrorWriter) 108 | 109 | router := mux.NewRouter() 110 | router.Handle("/connect", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 111 | logrus.Info("got a connection") 112 | remoteDialerServer.ServeHTTP(w, req) 113 | })) 114 | 115 | go func() { 116 | if err := runProxyListener(ctx, cfg, remoteDialerServer); err != nil { 117 | logrus.Errorf("proxy listener failed to start in the background: %v", err) 118 | } 119 | }() 120 | 121 | // Setting Up Secret Controller 122 | core, err := core.NewFactoryFromConfigWithOptions(restConfig, nil) 123 | if err != nil { 124 | return fmt.Errorf("build secret controller failed w/ err: %w", err) 125 | } 126 | 127 | if err := core.Start(ctx, 1); err != nil { 128 | return fmt.Errorf("secretController factory start failed: %w", err) 129 | } 130 | 131 | secretController := core.Core().V1().Secret() 132 | 133 | // Setting Up Remote Dialer HTTPS Server 134 | if err := server.ListenAndServe(ctx, cfg.HTTPSPort, 0, router, &server.ListenOpts{ 135 | Secrets: secretController, 136 | CAName: cfg.CAName, 137 | CertName: cfg.CertCAName, 138 | CertNamespace: cfg.CertCANamespace, 139 | TLSListenerConfig: dynamiclistener.Config{ 140 | SANs: []string{cfg.TLSName}, 141 | FilterCN: func(cns ...string) []string { 142 | return []string{cfg.TLSName} 143 | }, 144 | RegenerateCerts: func() bool { 145 | return true 146 | }, 147 | ExpirationDaysCheck: 10, 148 | }, 149 | }); err != nil { 150 | return fmt.Errorf("extension server exited with an error: %w", err) 151 | } 152 | <-ctx.Done() 153 | return nil 154 | } 155 | -------------------------------------------------------------------------------- /proxyclient/client.go: -------------------------------------------------------------------------------- 1 | package proxyclient 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "fmt" 8 | "net/http" 9 | "sync" 10 | "time" 11 | 12 | "github.com/gorilla/websocket" 13 | "github.com/rancher/remotedialer" 14 | v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1" 15 | 16 | "github.com/sirupsen/logrus" 17 | corev1 "k8s.io/api/core/v1" 18 | ) 19 | 20 | const ( 21 | defaultServerAddr = "wss://127.0.0.1" 22 | defaultServerPort = 5555 23 | defaultServerPath = "/connect" 24 | retryTimeout = 1 * time.Second 25 | certificateWatchInterval = 10 * time.Second 26 | getSecretRetryTimeout = 5 * time.Second 27 | ) 28 | 29 | type PortForwarder interface { 30 | Start() error 31 | Stop() 32 | } 33 | 34 | type ProxyClientOpt func(*ProxyClient) 35 | 36 | type ProxyClient struct { 37 | forwarder PortForwarder 38 | serverUrl string 39 | serverConnectSecret string 40 | 41 | dialer *websocket.Dialer 42 | dialerMtx sync.Mutex 43 | 44 | secretController v1.SecretController 45 | namespace string 46 | certSecretName string 47 | certServerName string 48 | 49 | onConnect func(ctx context.Context, session *remotedialer.Session) error 50 | } 51 | 52 | func New(ctx context.Context, serverSharedSecret, namespace, certSecretName, certServerName string, secretController v1.SecretController, forwarder PortForwarder, opts ...ProxyClientOpt) (*ProxyClient, error) { 53 | if secretController == nil { 54 | return nil, fmt.Errorf("SecretController required") 55 | } 56 | 57 | if forwarder == nil { 58 | return nil, fmt.Errorf("a PortForwarder must be provided") 59 | } 60 | 61 | if namespace == "" { 62 | return nil, fmt.Errorf("namespace required") 63 | } 64 | 65 | if certSecretName == "" { 66 | return nil, fmt.Errorf("certSecretName required") 67 | } 68 | 69 | if serverSharedSecret == "" { 70 | return nil, fmt.Errorf("server shared secret must be provided") 71 | } 72 | 73 | serverUrl := fmt.Sprintf("%s:%d%s", defaultServerAddr, defaultServerPort, defaultServerPath) 74 | 75 | client := &ProxyClient{ 76 | serverUrl: serverUrl, 77 | forwarder: forwarder, 78 | serverConnectSecret: serverSharedSecret, 79 | certSecretName: certSecretName, 80 | certServerName: certServerName, 81 | namespace: namespace, 82 | } 83 | 84 | client.setUpBuildDialerCallback(ctx, certSecretName, secretController) 85 | 86 | for _, opt := range opts { 87 | opt(client) 88 | } 89 | 90 | return client, nil 91 | } 92 | 93 | func (c *ProxyClient) setUpBuildDialerCallback(ctx context.Context, certSecretName string, secretController v1.SecretController) { 94 | secretController.OnChange(ctx, certSecretName, func(_ string, newSecret *corev1.Secret) (*corev1.Secret, error) { 95 | if newSecret == nil { 96 | return nil, nil 97 | } 98 | 99 | if newSecret.Name == c.certSecretName && newSecret.Namespace == c.namespace { 100 | rootCAs, err := buildCertFromSecret(c.namespace, c.certSecretName, newSecret) 101 | if err != nil { 102 | logrus.Errorf("RDPClient: build certificate failed: %s", err.Error()) 103 | return nil, err 104 | } 105 | 106 | c.dialerMtx.Lock() 107 | c.dialer = &websocket.Dialer{ 108 | TLSClientConfig: &tls.Config{ 109 | RootCAs: rootCAs, 110 | ServerName: c.certServerName, 111 | }, 112 | } 113 | c.dialerMtx.Unlock() 114 | logrus.Infof("RDPClient: certificate updated successfully") 115 | } 116 | 117 | return newSecret, nil 118 | }) 119 | } 120 | 121 | func buildCertFromSecret(namespace, certSecretName string, secret *corev1.Secret) (*x509.CertPool, error) { 122 | crtData, exists := secret.Data["tls.crt"] 123 | if !exists { 124 | return nil, fmt.Errorf("secret %s/%s missing tls.crt field", namespace, certSecretName) 125 | } 126 | 127 | rootCAs := x509.NewCertPool() 128 | if ok := rootCAs.AppendCertsFromPEM(crtData); !ok { 129 | return nil, fmt.Errorf("failed to parse tls.crt from secret into a CA pool") 130 | } 131 | 132 | return rootCAs, nil 133 | } 134 | 135 | func (c *ProxyClient) Run(ctx context.Context) { 136 | go func() { 137 | LookForDialer: 138 | for { 139 | select { 140 | case <-ctx.Done(): 141 | logrus.Infof("RDPClient: Received stop signal.") 142 | return 143 | 144 | default: 145 | logrus.Info("RDPClient: Checking if dialer is built...") 146 | 147 | c.dialerMtx.Lock() 148 | dialer := c.dialer 149 | c.dialerMtx.Unlock() 150 | 151 | if dialer != nil { 152 | logrus.Info("RDPClient: Dialer is built. Ready to start.") 153 | break LookForDialer 154 | } 155 | 156 | logrus.Infof("RDPClient: Dialer is not built yet, waiting %d secs to re-check.", getSecretRetryTimeout/time.Second) 157 | time.Sleep(getSecretRetryTimeout) 158 | } 159 | } 160 | 161 | for { 162 | select { 163 | case <-ctx.Done(): 164 | logrus.Infof("RDPClient: Received signal to stop.") 165 | return 166 | 167 | default: 168 | if err := c.forwarder.Start(); err != nil { 169 | logrus.Errorf("RDPClient: %s ", err) 170 | time.Sleep(retryTimeout) 171 | continue 172 | } 173 | 174 | logrus.Infof("RDPClient: connecting to %s", c.serverUrl) 175 | 176 | headers := http.Header{} 177 | headers.Set("X-API-Tunnel-Secret", c.serverConnectSecret) 178 | 179 | onConnectAuth := func(proto, address string) bool { return true } 180 | onConnect := func(sessionCtx context.Context, session *remotedialer.Session) error { 181 | logrus.Infoln("RDPClient: remotedialer session connected!") 182 | if c.onConnect != nil { 183 | return c.onConnect(sessionCtx, session) 184 | } 185 | return nil 186 | } 187 | 188 | c.dialerMtx.Lock() 189 | dialer := c.dialer 190 | c.dialerMtx.Unlock() 191 | 192 | if err := remotedialer.ClientConnect(ctx, c.serverUrl, headers, dialer, onConnectAuth, onConnect); err != nil { 193 | logrus.Errorf("RDPClient: remotedialer.ClientConnect error: %s", err.Error()) 194 | c.forwarder.Stop() 195 | time.Sleep(retryTimeout) 196 | } 197 | } 198 | } 199 | }() 200 | } 201 | 202 | func (c *ProxyClient) Stop() { 203 | if c.forwarder != nil { 204 | c.forwarder.Stop() 205 | logrus.Infoln("RDPClient: port-forward stopped.") 206 | } 207 | } 208 | 209 | func WithServerURL(serverUrl string) ProxyClientOpt { 210 | return func(pc *ProxyClient) { 211 | pc.serverUrl = serverUrl 212 | } 213 | } 214 | 215 | func WithOnConnectCallback(onConnect func(ctx context.Context, session *remotedialer.Session) error) ProxyClientOpt { 216 | return func(pc *ProxyClient) { 217 | pc.onConnect = onConnect 218 | } 219 | } 220 | 221 | func WithCustomDialer(dialer *websocket.Dialer) ProxyClientOpt { 222 | return func(pc *ProxyClient) { 223 | pc.dialer = dialer 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /readbuffer.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "sync" 9 | "time" 10 | 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | const ( 15 | MaxBuffer = 1 << 21 16 | ) 17 | 18 | type readBuffer struct { 19 | id, readCount, offerCount int64 20 | cond sync.Cond 21 | deadline time.Time 22 | buf bytes.Buffer 23 | err error 24 | backPressure *backPressure 25 | } 26 | 27 | func newReadBuffer(id int64, backPressure *backPressure) *readBuffer { 28 | return &readBuffer{ 29 | id: id, 30 | backPressure: backPressure, 31 | cond: sync.Cond{ 32 | L: &sync.Mutex{}, 33 | }, 34 | } 35 | } 36 | 37 | func (r *readBuffer) Status() string { 38 | r.cond.L.Lock() 39 | defer r.cond.L.Unlock() 40 | return fmt.Sprintf("%d/%d", r.readCount, r.offerCount) 41 | } 42 | 43 | func (r *readBuffer) Offer(reader io.Reader) error { 44 | r.cond.L.Lock() 45 | defer r.cond.L.Unlock() 46 | 47 | if r.err != nil { 48 | return r.err 49 | } 50 | 51 | if n, err := io.Copy(&r.buf, reader); err != nil { 52 | r.offerCount += n 53 | return err 54 | } else if n > 0 { 55 | r.offerCount += n 56 | r.cond.Broadcast() 57 | } 58 | 59 | if r.buf.Len() > MaxBuffer { 60 | r.backPressure.Pause() 61 | } 62 | 63 | if r.buf.Len() > MaxBuffer*2 { 64 | logrus.Debugf("remotedialer buffer exceeded id=%d, length: %d", r.id, r.buf.Len()) 65 | } 66 | 67 | return nil 68 | } 69 | 70 | func (r *readBuffer) Read(b []byte) (int, error) { 71 | r.cond.L.Lock() 72 | defer r.cond.L.Unlock() 73 | 74 | for { 75 | if r.buf.Len() > 0 { 76 | n, err := r.buf.Read(b) 77 | if err != nil { 78 | // The definition of bytes.Buffer is that this will always return nil because 79 | // we first checked that bytes.Buffer.Len() > 0. We assume that fact so just assert 80 | // that here. 81 | panic("bytes.Buffer returned err=\"" + err.Error() + "\" when buffer length was > 0") 82 | } 83 | r.readCount += int64(n) 84 | r.cond.Broadcast() 85 | if r.buf.Len() < MaxBuffer/8 { 86 | r.backPressure.Resume() 87 | } 88 | return n, nil 89 | } 90 | 91 | if r.buf.Cap() > MaxBuffer/8 { 92 | logrus.Debugf("resetting remotedialer buffer id=%d to zero, old cap %d", r.id, r.buf.Cap()) 93 | r.buf = bytes.Buffer{} 94 | } 95 | 96 | if r.err != nil { 97 | return 0, r.err 98 | } 99 | 100 | now := time.Now() 101 | if !r.deadline.IsZero() { 102 | if now.After(r.deadline) { 103 | return 0, errors.New("deadline exceeded") 104 | } 105 | } 106 | 107 | var t *time.Timer 108 | if !r.deadline.IsZero() { 109 | t = time.AfterFunc(r.deadline.Sub(now), func() { r.cond.Broadcast() }) 110 | } 111 | r.cond.Wait() 112 | if t != nil { 113 | t.Stop() 114 | } 115 | } 116 | } 117 | 118 | func (r *readBuffer) Close(err error) error { 119 | r.cond.L.Lock() 120 | defer r.cond.L.Unlock() 121 | if r.err == nil { 122 | r.err = err 123 | } 124 | r.cond.Broadcast() 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | "time" 7 | 8 | "github.com/gorilla/websocket" 9 | "github.com/pkg/errors" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var ( 14 | errFailedAuth = errors.New("failed authentication") 15 | errWrongMessageType = errors.New("wrong websocket message type") 16 | ) 17 | 18 | type Authorizer func(req *http.Request) (clientKey string, authed bool, err error) 19 | type ErrorWriter func(rw http.ResponseWriter, req *http.Request, code int, err error) 20 | 21 | func DefaultErrorWriter(rw http.ResponseWriter, req *http.Request, code int, err error) { 22 | rw.WriteHeader(code) 23 | rw.Write([]byte(err.Error())) 24 | } 25 | 26 | type Server struct { 27 | PeerID string 28 | PeerToken string 29 | ClientConnectAuthorizer ConnectAuthorizer 30 | authorizer Authorizer 31 | errorWriter ErrorWriter 32 | sessions *sessionManager 33 | peers map[string]peer 34 | peerLock sync.Mutex 35 | } 36 | 37 | func New(auth Authorizer, errorWriter ErrorWriter) *Server { 38 | return &Server{ 39 | peers: map[string]peer{}, 40 | authorizer: auth, 41 | errorWriter: errorWriter, 42 | sessions: newSessionManager(), 43 | } 44 | } 45 | 46 | func (s *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 47 | clientKey, authed, peer, err := s.auth(req) 48 | if err != nil { 49 | s.errorWriter(rw, req, 400, err) 50 | return 51 | } 52 | if !authed { 53 | s.errorWriter(rw, req, 401, errFailedAuth) 54 | return 55 | } 56 | 57 | logrus.Infof("Handling backend connection request [%s]", clientKey) 58 | 59 | upgrader := websocket.Upgrader{ 60 | HandshakeTimeout: 5 * time.Second, 61 | CheckOrigin: func(r *http.Request) bool { return true }, 62 | Error: s.errorWriter, 63 | } 64 | 65 | wsConn, err := upgrader.Upgrade(rw, req, nil) 66 | if err != nil { 67 | s.errorWriter(rw, req, 400, errors.Wrapf(err, "Error during upgrade for host [%v]", clientKey)) 68 | return 69 | } 70 | 71 | session := s.sessions.add(clientKey, wsConn, peer) 72 | session.auth = s.ClientConnectAuthorizer 73 | defer s.sessions.remove(session) 74 | 75 | code, err := session.Serve(req.Context()) 76 | if err != nil { 77 | // Hijacked so we can't write to the client 78 | logrus.Infof("error in remotedialer server [%d]: %v", code, err) 79 | } 80 | } 81 | 82 | func (s *Server) ListClients() []string { 83 | return s.sessions.listClients() 84 | } 85 | 86 | func (s *Server) auth(req *http.Request) (clientKey string, authed, peer bool, err error) { 87 | id := req.Header.Get(ID) 88 | token := req.Header.Get(Token) 89 | if id != "" && token != "" { 90 | // peer authentication 91 | s.peerLock.Lock() 92 | p, ok := s.peers[id] 93 | s.peerLock.Unlock() 94 | 95 | if ok && p.token == token { 96 | return id, true, true, nil 97 | } 98 | } 99 | 100 | id, authed, err = s.authorizer(req) 101 | return id, authed, false, err 102 | } 103 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | "sync/atomic" 12 | "time" 13 | 14 | "github.com/gorilla/mux" 15 | "github.com/rancher/remotedialer" 16 | "github.com/sirupsen/logrus" 17 | ) 18 | 19 | var ( 20 | clients = map[string]*http.Client{} 21 | l sync.Mutex 22 | counter int64 23 | ) 24 | 25 | func authorizer(req *http.Request) (string, bool, error) { 26 | id := req.Header.Get("x-tunnel-id") 27 | return id, id != "", nil 28 | } 29 | 30 | func Client(server *remotedialer.Server, rw http.ResponseWriter, req *http.Request) { 31 | timeout := req.URL.Query().Get("timeout") 32 | if timeout == "" { 33 | timeout = "15" 34 | } 35 | 36 | vars := mux.Vars(req) 37 | clientKey := vars["id"] 38 | url := fmt.Sprintf("%s://%s%s", vars["scheme"], vars["host"], vars["path"]) 39 | client := getClient(server, clientKey, timeout) 40 | 41 | id := atomic.AddInt64(&counter, 1) 42 | logrus.Infof("[%03d] REQ t=%s %s", id, timeout, url) 43 | 44 | resp, err := client.Get(url) 45 | if err != nil { 46 | logrus.Errorf("[%03d] REQ ERR t=%s %s: %v", id, timeout, url, err) 47 | remotedialer.DefaultErrorWriter(rw, req, 500, err) 48 | return 49 | } 50 | defer resp.Body.Close() 51 | 52 | logrus.Infof("[%03d] REQ OK t=%s %s", id, timeout, url) 53 | for k, v := range resp.Header { 54 | for _, h := range v { 55 | if rw.Header().Get(k) == "" { 56 | rw.Header().Set(k, h) 57 | } else { 58 | rw.Header().Add(k, h) 59 | } 60 | } 61 | } 62 | rw.WriteHeader(resp.StatusCode) 63 | io.Copy(rw, resp.Body) 64 | logrus.Infof("[%03d] REQ DONE t=%s %s", id, timeout, url) 65 | } 66 | 67 | func getClient(server *remotedialer.Server, clientKey, timeout string) *http.Client { 68 | l.Lock() 69 | defer l.Unlock() 70 | 71 | key := fmt.Sprintf("%s/%s", clientKey, timeout) 72 | client := clients[key] 73 | if client != nil { 74 | return client 75 | } 76 | 77 | dialer := server.Dialer(clientKey) 78 | client = &http.Client{ 79 | Transport: &http.Transport{ 80 | DialContext: dialer, 81 | }, 82 | } 83 | if timeout != "" { 84 | t, err := strconv.Atoi(timeout) 85 | if err == nil { 86 | client.Timeout = time.Duration(t) * time.Second 87 | } 88 | } 89 | 90 | clients[key] = client 91 | return client 92 | } 93 | 94 | func main() { 95 | var ( 96 | addr string 97 | peerID string 98 | peerToken string 99 | peers string 100 | debug bool 101 | ) 102 | flag.StringVar(&addr, "listen", ":8123", "Listen address") 103 | flag.StringVar(&peerID, "id", "", "Peer ID") 104 | flag.StringVar(&peerToken, "token", "", "Peer Token") 105 | flag.StringVar(&peers, "peers", "", "Peers format id:token:url,id:token:url") 106 | flag.BoolVar(&debug, "debug", false, "Enable debug logging") 107 | flag.Parse() 108 | 109 | if debug { 110 | logrus.SetLevel(logrus.DebugLevel) 111 | remotedialer.PrintTunnelData = true 112 | } 113 | 114 | handler := remotedialer.New(authorizer, remotedialer.DefaultErrorWriter) 115 | handler.PeerToken = peerToken 116 | handler.PeerID = peerID 117 | 118 | for _, peer := range strings.Split(peers, ",") { 119 | parts := strings.SplitN(strings.TrimSpace(peer), ":", 3) 120 | if len(parts) != 3 { 121 | continue 122 | } 123 | handler.AddPeer(parts[2], parts[0], parts[1]) 124 | } 125 | 126 | router := mux.NewRouter() 127 | router.Handle("/connect", handler) 128 | router.HandleFunc("/client/{id}/{scheme}/{host}{path:.*}", func(rw http.ResponseWriter, req *http.Request) { 129 | Client(handler, rw, req) 130 | }) 131 | 132 | fmt.Println("Listening on ", addr) 133 | http.ListenAndServe(addr, router) 134 | } 135 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "os" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "sync/atomic" 14 | "time" 15 | 16 | "github.com/gorilla/websocket" 17 | "github.com/sirupsen/logrus" 18 | ) 19 | 20 | type Session struct { 21 | sync.RWMutex 22 | 23 | nextConnID int64 24 | clientKey string 25 | sessionKey int64 26 | conn wsConn 27 | conns map[int64]*connection 28 | remoteClientKeys map[string]map[int]bool 29 | auth ConnectAuthorizer 30 | pingCancel context.CancelFunc 31 | pingWait sync.WaitGroup 32 | dialer Dialer 33 | client bool 34 | } 35 | 36 | // Use this defined type so we can share context between remotedialer and its clients 37 | type ContextKey struct{} 38 | 39 | var ContextKeyCaller = ContextKey{} 40 | 41 | func ValueFromContext(ctx context.Context) string { 42 | v := ctx.Value(ContextKeyCaller) 43 | if v == nil { 44 | return "" 45 | } 46 | if s, ok := v.(string); ok { 47 | return s 48 | } 49 | return "" 50 | } 51 | 52 | // PrintTunnelData No tunnel logging by default 53 | var PrintTunnelData bool 54 | 55 | func init() { 56 | if os.Getenv("CATTLE_TUNNEL_DATA_DEBUG") == "true" { 57 | PrintTunnelData = true 58 | } 59 | } 60 | 61 | func NewClientSession(auth ConnectAuthorizer, conn *websocket.Conn) *Session { 62 | return NewClientSessionWithDialer(auth, conn, nil) 63 | } 64 | 65 | func NewClientSessionWithDialer(auth ConnectAuthorizer, conn *websocket.Conn, dialer Dialer) *Session { 66 | return &Session{ 67 | clientKey: "client", 68 | conn: newWSConn(conn), 69 | conns: map[int64]*connection{}, 70 | auth: auth, 71 | client: true, 72 | dialer: dialer, 73 | } 74 | } 75 | 76 | func newSession(sessionKey int64, clientKey string, conn wsConn) *Session { 77 | return &Session{ 78 | nextConnID: 1, 79 | clientKey: clientKey, 80 | sessionKey: sessionKey, 81 | conn: conn, 82 | conns: map[int64]*connection{}, 83 | remoteClientKeys: map[string]map[int]bool{}, 84 | } 85 | } 86 | 87 | // addConnection safely registers a new connection in the connections map 88 | func (s *Session) addConnection(connID int64, conn *connection) { 89 | s.Lock() 90 | defer s.Unlock() 91 | 92 | s.conns[connID] = conn 93 | if PrintTunnelData { 94 | logrus.Debugf("CONNECTIONS %d %d", s.sessionKey, len(s.conns)) 95 | } 96 | } 97 | 98 | // removeConnection safely removes a connection by ID, returning the connection object 99 | func (s *Session) removeConnection(connID int64) *connection { 100 | s.Lock() 101 | defer s.Unlock() 102 | 103 | conn := s.removeConnectionLocked(connID) 104 | if PrintTunnelData { 105 | defer logrus.Debugf("CONNECTIONS %d %d", s.sessionKey, len(s.conns)) 106 | } 107 | return conn 108 | } 109 | 110 | // removeConnectionLocked removes a given connection from the session. 111 | // The session lock must be held by the caller when calling this method 112 | func (s *Session) removeConnectionLocked(connID int64) *connection { 113 | conn := s.conns[connID] 114 | delete(s.conns, connID) 115 | return conn 116 | } 117 | 118 | // getConnection retrieves a connection by ID 119 | func (s *Session) getConnection(connID int64) *connection { 120 | s.RLock() 121 | defer s.RUnlock() 122 | 123 | return s.conns[connID] 124 | } 125 | 126 | // activeConnectionIDs returns an ordered list of IDs for the currently active connections 127 | func (s *Session) activeConnectionIDs() []int64 { 128 | s.RLock() 129 | defer s.RUnlock() 130 | 131 | res := make([]int64, 0, len(s.conns)) 132 | for id := range s.conns { 133 | res = append(res, id) 134 | } 135 | sort.Slice(res, func(i, j int) bool { return res[i] < res[j] }) 136 | return res 137 | } 138 | 139 | // addSessionKey registers a new session key for a given client key 140 | func (s *Session) addSessionKey(clientKey string, sessionKey int) { 141 | s.Lock() 142 | defer s.Unlock() 143 | 144 | keys := s.remoteClientKeys[clientKey] 145 | if keys == nil { 146 | keys = map[int]bool{} 147 | s.remoteClientKeys[clientKey] = keys 148 | } 149 | keys[sessionKey] = true 150 | } 151 | 152 | // removeSessionKey removes a specific session key for a client key 153 | func (s *Session) removeSessionKey(clientKey string, sessionKey int) { 154 | s.Lock() 155 | defer s.Unlock() 156 | 157 | keys := s.remoteClientKeys[clientKey] 158 | delete(keys, sessionKey) 159 | if len(keys) == 0 { 160 | delete(s.remoteClientKeys, clientKey) 161 | } 162 | } 163 | 164 | // getSessionKeys retrieves all session keys for a given client key 165 | func (s *Session) getSessionKeys(clientKey string) map[int]bool { 166 | s.RLock() 167 | defer s.RUnlock() 168 | return s.remoteClientKeys[clientKey] 169 | } 170 | 171 | func (s *Session) startPings(rootCtx context.Context) { 172 | ctx, cancel := context.WithCancel(rootCtx) 173 | s.pingCancel = cancel 174 | s.pingWait.Add(1) 175 | 176 | go func() { 177 | defer s.pingWait.Done() 178 | 179 | t := time.NewTicker(PingWriteInterval) 180 | defer t.Stop() 181 | 182 | syncConnections := time.NewTicker(SyncConnectionsInterval) 183 | defer syncConnections.Stop() 184 | 185 | for { 186 | select { 187 | case <-ctx.Done(): 188 | return 189 | case <-syncConnections.C: 190 | if err := s.sendSyncConnections(); err != nil { 191 | logrus.WithError(err).Error("Error syncing connections") 192 | } 193 | case <-t.C: 194 | if err := s.sendPing(); err != nil { 195 | logrus.WithError(err).Error("Error writing ping") 196 | } 197 | s := ValueFromContext(ctx) 198 | if s == "" { 199 | s = "" 200 | } 201 | logrus.Tracef("[%s] Wrote ping", s) 202 | } 203 | } 204 | }() 205 | } 206 | 207 | // sendPing sends a Ping control message to the peer 208 | func (s *Session) sendPing() error { 209 | return s.conn.WriteControl(websocket.PingMessage, time.Now().Add(PingWaitDuration), []byte("")) 210 | } 211 | 212 | func (s *Session) stopPings() { 213 | if s.pingCancel == nil { 214 | return 215 | } 216 | 217 | s.pingCancel() 218 | s.pingWait.Wait() 219 | } 220 | 221 | func (s *Session) Serve(ctx context.Context) (int, error) { 222 | if s.client { 223 | s.startPings(ctx) 224 | } 225 | 226 | for { 227 | msType, reader, err := s.conn.NextReader() 228 | if err != nil { 229 | return 400, err 230 | } 231 | 232 | if msType != websocket.BinaryMessage { 233 | return 400, errWrongMessageType 234 | } 235 | 236 | if err := s.serveMessage(ctx, reader); err != nil { 237 | return 500, err 238 | } 239 | } 240 | } 241 | 242 | func defaultDeadline() time.Time { 243 | return time.Now().Add(time.Minute) 244 | } 245 | 246 | func parseAddress(address string) (string, int, error) { 247 | parts := strings.SplitN(address, "/", 2) 248 | if len(parts) != 2 { 249 | return "", 0, errors.New("not / separated") 250 | } 251 | v, err := strconv.Atoi(parts[1]) 252 | return parts[0], v, err 253 | } 254 | 255 | type connResult struct { 256 | conn net.Conn 257 | err error 258 | } 259 | 260 | func (s *Session) Dial(ctx context.Context, proto, address string) (net.Conn, error) { 261 | return s.serverConnectContext(ctx, proto, address) 262 | } 263 | 264 | func (s *Session) serverConnectContext(ctx context.Context, proto, address string) (net.Conn, error) { 265 | deadline, ok := ctx.Deadline() 266 | if ok { 267 | return s.serverConnect(deadline, proto, address) 268 | } 269 | 270 | result := make(chan connResult, 1) 271 | go func() { 272 | c, err := s.serverConnect(defaultDeadline(), proto, address) 273 | result <- connResult{conn: c, err: err} 274 | }() 275 | 276 | select { 277 | case <-ctx.Done(): 278 | // We don't want to orphan an open connection so we wait for the result and immediately close it 279 | go func() { 280 | r := <-result 281 | if r.err == nil { 282 | r.conn.Close() 283 | } 284 | }() 285 | return nil, ctx.Err() 286 | case r := <-result: 287 | return r.conn, r.err 288 | } 289 | } 290 | 291 | func (s *Session) serverConnect(deadline time.Time, proto, address string) (net.Conn, error) { 292 | connID := atomic.AddInt64(&s.nextConnID, 1) 293 | conn := newConnection(connID, s, proto, address) 294 | 295 | s.addConnection(connID, conn) 296 | 297 | _, err := s.writeMessage(deadline, newConnect(connID, proto, address)) 298 | if err != nil { 299 | s.closeConnection(connID, err) 300 | return nil, err 301 | } 302 | 303 | return conn, err 304 | } 305 | 306 | func (s *Session) writeMessage(deadline time.Time, message *message) (int, error) { 307 | if PrintTunnelData { 308 | logrus.Debug("WRITE ", message) 309 | } 310 | return message.WriteTo(deadline, s.conn) 311 | } 312 | 313 | func (s *Session) Close() { 314 | s.Lock() 315 | defer s.Unlock() 316 | 317 | s.stopPings() 318 | 319 | for _, connection := range s.conns { 320 | connection.tunnelClose(errors.New("tunnel disconnect")) 321 | } 322 | 323 | s.conns = map[int64]*connection{} 324 | } 325 | 326 | func (s *Session) sessionAdded(clientKey string, sessionKey int64) { 327 | client := fmt.Sprintf("%s/%d", clientKey, sessionKey) 328 | _, err := s.writeMessage(time.Time{}, newAddClient(client)) 329 | if err != nil { 330 | s.conn.Close() 331 | } 332 | } 333 | 334 | func (s *Session) sessionRemoved(clientKey string, sessionKey int64) { 335 | client := fmt.Sprintf("%s/%d", clientKey, sessionKey) 336 | _, err := s.writeMessage(time.Time{}, newRemoveClient(client)) 337 | if err != nil { 338 | s.conn.Close() 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /session_manager.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "net" 8 | "sync" 9 | 10 | "github.com/gorilla/websocket" 11 | 12 | "github.com/rancher/remotedialer/metrics" 13 | ) 14 | 15 | type sessionListener interface { 16 | sessionAdded(clientKey string, sessionKey int64) 17 | sessionRemoved(clientKey string, sessionKey int64) 18 | } 19 | 20 | type sessionManager struct { 21 | sync.Mutex 22 | clients map[string][]*Session 23 | peers map[string][]*Session 24 | listeners map[sessionListener]bool 25 | } 26 | 27 | func newSessionManager() *sessionManager { 28 | return &sessionManager{ 29 | clients: map[string][]*Session{}, 30 | peers: map[string][]*Session{}, 31 | listeners: map[sessionListener]bool{}, 32 | } 33 | } 34 | 35 | func toDialer(s *Session, prefix string) Dialer { 36 | return func(ctx context.Context, proto, address string) (net.Conn, error) { 37 | if prefix == "" { 38 | return s.serverConnectContext(ctx, proto, address) 39 | } 40 | return s.serverConnectContext(ctx, prefix+"::"+proto, address) 41 | } 42 | } 43 | 44 | func (sm *sessionManager) removeListener(listener sessionListener) { 45 | sm.Lock() 46 | defer sm.Unlock() 47 | 48 | delete(sm.listeners, listener) 49 | } 50 | 51 | func (sm *sessionManager) addListener(listener sessionListener) { 52 | sm.Lock() 53 | defer sm.Unlock() 54 | 55 | sm.listeners[listener] = true 56 | 57 | for k, sessions := range sm.clients { 58 | for _, session := range sessions { 59 | listener.sessionAdded(k, session.sessionKey) 60 | } 61 | } 62 | 63 | for k, sessions := range sm.peers { 64 | for _, session := range sessions { 65 | listener.sessionAdded(k, session.sessionKey) 66 | } 67 | } 68 | } 69 | 70 | func (sm *sessionManager) listClients() []string { 71 | sm.Lock() 72 | defer sm.Unlock() 73 | clients := make([]string, 0, len(sm.clients)) 74 | for c := range sm.clients { 75 | clients = append(clients, c) 76 | } 77 | return clients 78 | } 79 | 80 | func (sm *sessionManager) getDialer(clientKey string) (Dialer, error) { 81 | sm.Lock() 82 | defer sm.Unlock() 83 | 84 | sessions := sm.clients[clientKey] 85 | if len(sessions) > 0 { 86 | return toDialer(sessions[0], ""), nil 87 | } 88 | 89 | for _, sessions := range sm.peers { 90 | for _, session := range sessions { 91 | keys := session.getSessionKeys(clientKey) 92 | if len(keys) > 0 { 93 | return toDialer(session, clientKey), nil 94 | } 95 | } 96 | } 97 | 98 | return nil, fmt.Errorf("failed to find Session for client %s", clientKey) 99 | } 100 | 101 | func (sm *sessionManager) add(clientKey string, conn *websocket.Conn, peer bool) *Session { 102 | sessionKey := rand.Int63() 103 | session := newSession(sessionKey, clientKey, newWSConn(conn)) 104 | 105 | sm.Lock() 106 | defer sm.Unlock() 107 | 108 | if peer { 109 | sm.peers[clientKey] = append(sm.peers[clientKey], session) 110 | } else { 111 | sm.clients[clientKey] = append(sm.clients[clientKey], session) 112 | } 113 | metrics.IncSMTotalAddWS(clientKey, peer) 114 | 115 | for l := range sm.listeners { 116 | l.sessionAdded(clientKey, session.sessionKey) 117 | } 118 | 119 | return session 120 | } 121 | 122 | func (sm *sessionManager) remove(s *Session) { 123 | var isPeer bool 124 | sm.Lock() 125 | defer sm.Unlock() 126 | 127 | for i, store := range []map[string][]*Session{sm.clients, sm.peers} { 128 | var newSessions []*Session 129 | 130 | for _, v := range store[s.clientKey] { 131 | if v.sessionKey == s.sessionKey { 132 | if i == 0 { 133 | isPeer = false 134 | } else { 135 | isPeer = true 136 | } 137 | metrics.IncSMTotalRemoveWS(s.clientKey, isPeer) 138 | continue 139 | } 140 | newSessions = append(newSessions, v) 141 | } 142 | 143 | if len(newSessions) == 0 { 144 | delete(store, s.clientKey) 145 | } else { 146 | store[s.clientKey] = newSessions 147 | } 148 | } 149 | 150 | for l := range sm.listeners { 151 | l.sessionRemoved(s.clientKey, s.sessionKey) 152 | } 153 | 154 | s.Close() 155 | } 156 | -------------------------------------------------------------------------------- /session_serve.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // serveMessage accepts an incoming message from the underlying websocket connection and processes the request based on its messageType 13 | func (s *Session) serveMessage(ctx context.Context, reader io.Reader) error { 14 | message, err := newServerMessage(reader) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | if PrintTunnelData { 20 | logrus.Debug("REQUEST ", message) 21 | } 22 | 23 | switch message.messageType { 24 | case Connect: 25 | return s.clientConnect(ctx, message) 26 | case AddClient: 27 | return s.addRemoteClient(message.address) 28 | case RemoveClient: 29 | return s.removeRemoteClient(message.address) 30 | case SyncConnections: 31 | return s.syncConnections(message.body) 32 | case Data: 33 | s.connectionData(message.connID, message.body) 34 | case Pause: 35 | s.pauseConnection(message.connID) 36 | case Resume: 37 | s.resumeConnection(message.connID) 38 | case Error: 39 | s.closeConnection(message.connID, message.Err()) 40 | } 41 | return nil 42 | } 43 | 44 | // clientConnect accepts a new connection request, dialing back to establish the connection 45 | func (s *Session) clientConnect(ctx context.Context, message *message) error { 46 | if s.auth == nil || !s.auth(message.proto, message.address) { 47 | return errors.New("connect not allowed") 48 | } 49 | 50 | conn := newConnection(message.connID, s, message.proto, message.address) 51 | s.addConnection(message.connID, conn) 52 | 53 | go clientDial(ctx, s.dialer, conn, message) 54 | 55 | return nil 56 | } 57 | 58 | // / addRemoteClient registers a new remote client, making it accessible for requests 59 | func (s *Session) addRemoteClient(address string) error { 60 | if s.remoteClientKeys == nil { 61 | return nil 62 | } 63 | 64 | clientKey, sessionKey, err := parseAddress(address) 65 | if err != nil { 66 | return fmt.Errorf("invalid remote Session %s: %v", address, err) 67 | } 68 | s.addSessionKey(clientKey, sessionKey) 69 | 70 | if PrintTunnelData { 71 | logrus.Debugf("ADD REMOTE CLIENT %s, SESSION %d", address, s.sessionKey) 72 | } 73 | 74 | return nil 75 | } 76 | 77 | // / addRemoteClient removes a given client from a session 78 | func (s *Session) removeRemoteClient(address string) error { 79 | clientKey, sessionKey, err := parseAddress(address) 80 | if err != nil { 81 | return fmt.Errorf("invalid remote Session %s: %v", address, err) 82 | } 83 | s.removeSessionKey(clientKey, sessionKey) 84 | 85 | if PrintTunnelData { 86 | logrus.Debugf("REMOVE REMOTE CLIENT %s, SESSION %d", address, s.sessionKey) 87 | } 88 | 89 | return nil 90 | } 91 | 92 | // syncConnections closes any session connection that is not present in the IDs received from the client 93 | func (s *Session) syncConnections(r io.Reader) error { 94 | payload, err := io.ReadAll(r) 95 | if err != nil { 96 | return fmt.Errorf("reading message body: %w", err) 97 | } 98 | clientActiveConnections, err := decodeConnectionIDs(payload) 99 | if err != nil { 100 | return fmt.Errorf("decoding sync connections payload: %w", err) 101 | } 102 | 103 | s.compareAndCloseStaleConnections(clientActiveConnections) 104 | return nil 105 | } 106 | 107 | // closeConnection removes a connection for a given ID from the session, sending an error message to communicate the closing to the other end. 108 | // If an error is not provided, io.EOF will be used instead. 109 | func (s *Session) closeConnection(connID int64, err error) { 110 | if conn := s.removeConnection(connID); conn != nil { 111 | conn.tunnelClose(err) 112 | } 113 | } 114 | 115 | // connectionData process incoming data from connection by reading the body into an internal readBuffer 116 | func (s *Session) connectionData(connID int64, body io.Reader) { 117 | conn := s.getConnection(connID) 118 | if conn == nil { 119 | errMsg := newErrorMessage(connID, fmt.Errorf("connection not found %s/%d/%d", s.clientKey, s.sessionKey, connID)) 120 | _, _ = errMsg.WriteTo(defaultDeadline(), s.conn) 121 | return 122 | } 123 | 124 | if err := conn.OnData(body); err != nil { 125 | s.closeConnection(connID, err) 126 | } 127 | } 128 | 129 | // pauseConnection activates backPressure for a given connection ID 130 | func (s *Session) pauseConnection(connID int64) { 131 | if conn := s.getConnection(connID); conn != nil { 132 | conn.OnPause() 133 | } 134 | } 135 | 136 | // resumeConnection deactivates backPressure for a given connection ID 137 | func (s *Session) resumeConnection(connID int64) { 138 | if conn := s.getConnection(connID); conn != nil { 139 | conn.OnResume() 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /session_serve_test.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "math/rand" 9 | "net" 10 | "reflect" 11 | "strings" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func TestSession_clientConnect(t *testing.T) { 17 | ctx, cancel := context.WithCancel(context.Background()) 18 | defer cancel() 19 | 20 | msgProto, msgAddr := "testproto", "testaddr" 21 | s := setupDummySession(t, 0) 22 | s.auth = func(proto, address string) bool { return proto == msgProto && address == msgAddr } 23 | 24 | dialerC := make(chan struct{}) 25 | s.dialer = func(ctx context.Context, network, address string) (net.Conn, error) { 26 | close(dialerC) 27 | clientConn, _ := net.Pipe() 28 | return clientConn, nil 29 | } 30 | 31 | connID := getDummyConnectionID() 32 | if err := s.clientConnect(ctx, newConnect(connID, msgProto, msgAddr)); err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | select { 37 | case <-dialerC: 38 | case <-time.After(1 * time.Second): 39 | t.Errorf("timed out waiting for dialer") 40 | } 41 | 42 | if conn := s.getConnection(connID); conn == nil { 43 | t.Errorf("Connection not found in session for ID %d", connID) 44 | } 45 | } 46 | 47 | func TestSession_addRemoveRemoteClient(t *testing.T) { 48 | s := setupDummySession(t, 0) 49 | clientKey, sessionKey := "test", rand.Int() 50 | 51 | msgAddress := fmt.Sprintf("%s/%d", clientKey, sessionKey) 52 | if err := s.addRemoteClient(msgAddress); err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | if got, want := s.getSessionKeys(clientKey), map[int]bool{sessionKey: true}; !reflect.DeepEqual(got, want) { 57 | t.Errorf("remote client session was not added correctly, got %v, want %v", got, want) 58 | } 59 | 60 | if err := s.removeRemoteClient(msgAddress); err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | if got, want := s.getSessionKeys(clientKey), 0; len(got) != want { 65 | t.Errorf("remote client session was not removed correctly, got %v, want len(%d)", got, want) 66 | } 67 | } 68 | 69 | func TestSession_connectionData(t *testing.T) { 70 | s := setupDummySession(t, 0) 71 | connID := getDummyConnectionID() 72 | conn := newConnection(connID, s, "test", "test") 73 | s.addConnection(connID, conn) 74 | 75 | data := "testing!" 76 | s.connectionData(connID, strings.NewReader(data)) 77 | 78 | if got, want := conn.buffer.offerCount, int64(len(data)); got != want { 79 | t.Errorf("incorrect data length, got %d, want %d", got, want) 80 | } 81 | 82 | buf := make([]byte, conn.buffer.offerCount) 83 | if _, err := conn.buffer.Read(buf); err != nil { 84 | t.Fatal(err) 85 | } 86 | if got, want := string(buf), data; got != want { 87 | t.Errorf("incorrect data, got %q, want %q", got, want) 88 | } 89 | } 90 | 91 | func TestSession_pauseResumeConnection(t *testing.T) { 92 | s := setupDummySession(t, 0) 93 | connID := getDummyConnectionID() 94 | conn := newConnection(connID, s, "test", "test") 95 | s.addConnection(connID, conn) 96 | 97 | s.pauseConnection(connID) 98 | if !conn.backPressure.paused { 99 | t.Errorf("connection was not paused correctly") 100 | } 101 | 102 | s.resumeConnection(connID) 103 | if conn.backPressure.paused { 104 | t.Errorf("connection was not resumed correctly") 105 | } 106 | } 107 | 108 | func TestSession_closeConnection(t *testing.T) { 109 | s := setupDummySession(t, 0) 110 | var msg *message 111 | s.conn = &fakeWSConn{ 112 | writeMessageCallback: func(msgType int, deadline time.Time, data []byte) (err error) { 113 | if !deadline.IsZero() && deadline.Before(time.Now()) { 114 | return errors.New("deadline exceeded") 115 | } 116 | msg, err = newServerMessage(bytes.NewReader(data)) 117 | return 118 | }, 119 | } 120 | connID := getDummyConnectionID() 121 | conn := newConnection(connID, s, "test", "test") 122 | s.addConnection(connID, conn) 123 | 124 | // Ensure Error message is sent regardless of the WriteDeadline value, see https://github.com/rancher/remotedialer/pull/79 125 | _ = conn.SetWriteDeadline(time.Now()) 126 | 127 | expectedErr := errors.New("connection closed") 128 | s.closeConnection(connID, expectedErr) 129 | 130 | if s.getConnection(connID) != nil { 131 | t.Errorf("connection was not closed correctly") 132 | } 133 | if conn.err == nil || msg == nil { 134 | t.Fatal("message not sent on closed connection") 135 | } else if msg.messageType != Error { 136 | t.Errorf("incorrect message type sent") 137 | } else if got, want := msg.Err().Error(), expectedErr.Error(); got != want { 138 | t.Errorf("wrong error, got %v, want %v", got, want) 139 | } else if got, want := conn.err, expectedErr; !errors.Is(got, want) { 140 | t.Errorf("wrong error, got %v, want %v", got, want) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /session_sync.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | var errCloseSyncConnections = errors.New("sync from client") 11 | 12 | // encodeConnectionIDs serializes a slice of connection IDs 13 | func encodeConnectionIDs(ids []int64) []byte { 14 | payload := make([]byte, 0, 8*len(ids)) 15 | for _, id := range ids { 16 | payload = binary.LittleEndian.AppendUint64(payload, uint64(id)) 17 | } 18 | return payload 19 | } 20 | 21 | // decodeConnectionIDs deserializes a slice of connection IDs 22 | func decodeConnectionIDs(payload []byte) ([]int64, error) { 23 | if len(payload)%8 != 0 { 24 | return nil, fmt.Errorf("incorrect data format") 25 | } 26 | result := make([]int64, 0, len(payload)/8) 27 | for x := 0; x < len(payload); x += 8 { 28 | id := binary.LittleEndian.Uint64(payload[x : x+8]) 29 | result = append(result, int64(id)) 30 | } 31 | return result, nil 32 | } 33 | 34 | func newSyncConnectionsMessage(connectionIDs []int64) *message { 35 | return &message{ 36 | id: nextid(), 37 | messageType: SyncConnections, 38 | bytes: encodeConnectionIDs(connectionIDs), 39 | } 40 | } 41 | 42 | // sendSyncConnections sends a binary message of type SyncConnections, whose payload is a list of the active connection IDs for this session 43 | func (s *Session) sendSyncConnections() error { 44 | _, err := s.writeMessage(time.Now().Add(SyncConnectionsTimeout), newSyncConnectionsMessage(s.activeConnectionIDs())) 45 | return err 46 | } 47 | 48 | // compareAndCloseStaleConnections compares the Session's activeConnectionIDs with the provided list from the client, then closing every connection not present in it 49 | func (s *Session) compareAndCloseStaleConnections(clientIDs []int64) { 50 | serverIDs := s.activeConnectionIDs() 51 | toClose := diffSortedSetsGetRemoved(serverIDs, clientIDs) 52 | if len(toClose) == 0 { 53 | return 54 | } 55 | 56 | s.Lock() 57 | defer s.Unlock() 58 | for _, id := range toClose { 59 | // Connection no longer active in the client, close it server-side 60 | conn := s.removeConnectionLocked(id) 61 | if conn != nil { 62 | // Using doTunnelClose directly instead of tunnelClose, omitting unnecessarily sending an Error message 63 | conn.doTunnelClose(errCloseSyncConnections) 64 | } 65 | } 66 | } 67 | 68 | // diffSortedSetsGetRemoved compares two sorted slices and returns those items present in a that are not present in b 69 | // similar to coreutil's "comm -23" 70 | func diffSortedSetsGetRemoved(a, b []int64) []int64 { 71 | var res []int64 72 | var i, j int 73 | for i < len(a) && j < len(b) { 74 | if a[i] < b[j] { // present in "a", not in "b" 75 | res = append(res, a[i]) 76 | i++ 77 | } else if a[i] > b[j] { // present in "b", not in "a" 78 | j++ 79 | } else { // present in both 80 | i++ 81 | j++ 82 | } 83 | } 84 | res = append(res, a[i:]...) // any remainders in "a" are also removed from "b" 85 | return res 86 | } 87 | -------------------------------------------------------------------------------- /session_sync_test.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "math/rand" 9 | "net/http" 10 | "net/http/httptest" 11 | "reflect" 12 | "sort" 13 | "testing" 14 | 15 | "github.com/gorilla/websocket" 16 | ) 17 | 18 | func Test_encodeConnectionIDs(t *testing.T) { 19 | t.Parallel() 20 | tests := []struct { 21 | size int 22 | }{ 23 | {0}, 24 | {1}, 25 | {2}, 26 | {10}, 27 | {100}, 28 | {1000}, 29 | } 30 | for x := range tests { 31 | tt := tests[x] 32 | t.Run(fmt.Sprintf("%d_ids", tt.size), func(t *testing.T) { 33 | t.Parallel() 34 | ids := generateIDs(tt.size) 35 | encoded := encodeConnectionIDs(ids) 36 | decoded, err := decodeConnectionIDs(encoded) 37 | if err != nil { 38 | t.Error(err) 39 | } 40 | if got, want := decoded, ids; !reflect.DeepEqual(got, want) { 41 | t.Errorf("encoding and decoding differs from original data, got: %v, want: %v", got, want) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func Test_diffSortedSetsGetRemoved(t *testing.T) { 48 | t.Parallel() 49 | tests := []struct { 50 | server, client, expected []int64 51 | }{ 52 | { 53 | // same ids 54 | server: []int64{2, 4, 6}, 55 | client: []int64{2, 4, 6}, 56 | expected: nil, 57 | }, 58 | { 59 | // Client keeps all ids from the server, additional ids are okay 60 | server: []int64{1, 2, 3}, 61 | client: []int64{1, 2, 3, 4, 5}, 62 | expected: nil, 63 | }, 64 | { 65 | // Client closed some ids kept by the server 66 | server: []int64{1, 2, 3, 4, 5}, 67 | client: []int64{1, 2, 3}, 68 | expected: []int64{4, 5}, 69 | }, 70 | { 71 | // Combined case 72 | server: []int64{1, 2, 3, 4, 5}, 73 | client: []int64{3, 6}, 74 | expected: []int64{1, 2, 4, 5}, 75 | }, 76 | } 77 | for x := range tests { 78 | tt := tests[x] 79 | t.Run(fmt.Sprintf("case_%d", x), func(t *testing.T) { 80 | t.Parallel() 81 | if got, want := diffSortedSetsGetRemoved(tt.server, tt.client), tt.expected; !reflect.DeepEqual(got, want) { 82 | t.Errorf("unexpected result, got: %v, want: %v", got, want) 83 | } 84 | }) 85 | } 86 | } 87 | 88 | func TestSession_sendSyncConnections(t *testing.T) { 89 | t.Parallel() 90 | 91 | data := make(chan []byte) 92 | conn := testServerWS(t, data) 93 | session := newSession(rand.Int63(), "sync-test", newWSConn(conn)) 94 | 95 | for _, n := range []int{0, 5, 20} { 96 | ids := generateIDs(n) 97 | for _, id := range ids { 98 | session.conns[id] = nil 99 | } 100 | sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) 101 | 102 | if err := session.sendSyncConnections(); err != nil { 103 | t.Fatal(err) 104 | } 105 | message, err := newServerMessage(bytes.NewBuffer(<-data)) 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | payload, err := io.ReadAll(message.body) 110 | if err != nil { 111 | t.Fatal(err) 112 | } 113 | 114 | if got, want := message.messageType, SyncConnections; got != want { 115 | t.Errorf("incorrect message type, got: %v, want: %v", got, want) 116 | } 117 | if decoded, err := decodeConnectionIDs(payload); err != nil { 118 | t.Fatal(err) 119 | } else if got, want := decoded, session.activeConnectionIDs(); !reflect.DeepEqual(got, want) { 120 | t.Errorf("incorrect connections IDs, got: %v, want: %v", got, want) 121 | } 122 | } 123 | } 124 | 125 | func generateIDs(n int) []int64 { 126 | ids := make([]int64, n) 127 | for x := range ids { 128 | ids[x] = rand.Int63() 129 | } 130 | return ids 131 | } 132 | 133 | func testServerWS(t *testing.T, data chan<- []byte) *websocket.Conn { 134 | t.Helper() 135 | 136 | var upgrader websocket.Upgrader 137 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 138 | c, err := upgrader.Upgrade(w, r, nil) 139 | if err != nil { 140 | return 141 | } 142 | defer c.Close() 143 | for { 144 | _, message, err := c.ReadMessage() 145 | if err != nil { 146 | return 147 | } 148 | if data != nil { 149 | data <- message 150 | } 151 | } 152 | })) 153 | t.Cleanup(server.Close) 154 | 155 | ctx, cancel := context.WithCancel(context.Background()) 156 | t.Cleanup(cancel) 157 | 158 | url := "ws" + server.URL[4:] // http:// -> ws:// 159 | conn, _, err := websocket.DefaultDialer.DialContext(ctx, url, nil) 160 | if err != nil { 161 | t.Fatal(err) 162 | } 163 | t.Cleanup(func() { _ = conn.Close() }) 164 | return conn 165 | } 166 | -------------------------------------------------------------------------------- /session_test.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "math/rand" 5 | "reflect" 6 | "sync" 7 | "sync/atomic" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | var dummyConnectionsNextID int64 = 1 13 | 14 | func getDummyConnectionID() int64 { 15 | return atomic.AddInt64(&dummyConnectionsNextID, 1) 16 | } 17 | 18 | func setupDummySession(t *testing.T, nConnections int) *Session { 19 | t.Helper() 20 | 21 | s := newSession(rand.Int63(), "", nil) 22 | 23 | var wg sync.WaitGroup 24 | ready := make(chan struct{}) 25 | for i := 0; i < nConnections; i++ { 26 | connID := getDummyConnectionID() 27 | wg.Add(1) 28 | go func() { 29 | defer wg.Done() 30 | <-ready 31 | s.addConnection(connID, &connection{}) 32 | }() 33 | } 34 | close(ready) 35 | wg.Wait() 36 | 37 | if got, want := len(s.conns), nConnections; got != want { 38 | t.Fatalf("incorrect number of connections, got: %d, want %d", got, want) 39 | } 40 | 41 | return s 42 | } 43 | 44 | func TestSession_connections(t *testing.T) { 45 | t.Parallel() 46 | 47 | const n = 10 48 | s := setupDummySession(t, n) 49 | 50 | connID, conn := getDummyConnectionID(), &connection{} 51 | s.addConnection(connID, conn) 52 | if got, want := len(s.conns), n+1; got != want { 53 | t.Errorf("incorrect number of connections, got: %d, want %d", got, want) 54 | } 55 | if got, want := s.getConnection(connID), conn; got != want { 56 | t.Errorf("incorrect result from getConnection, got: %v, want %v", got, want) 57 | } 58 | if got, want := s.removeConnection(connID), conn; got != want { 59 | t.Errorf("incorrect result from removeConnection, got: %v, want %v", got, want) 60 | } 61 | } 62 | 63 | func TestSession_sessionKeys(t *testing.T) { 64 | t.Parallel() 65 | 66 | s := setupDummySession(t, 0) 67 | 68 | clientKey, sessionKey := "testkey", rand.Int() 69 | s.addSessionKey(clientKey, sessionKey) 70 | if got, want := len(s.remoteClientKeys), 1; got != want { 71 | t.Errorf("incorrect number of remote client keys, got: %d, want %d", got, want) 72 | } 73 | 74 | if got, want := s.getSessionKeys(clientKey), map[int]bool{sessionKey: true}; !reflect.DeepEqual(got, want) { 75 | t.Errorf("incorrect result from getSessionKeys, got: %v, want %v", got, want) 76 | } 77 | 78 | s.removeSessionKey(clientKey, sessionKey) 79 | if got, want := len(s.remoteClientKeys), 0; got != want { 80 | t.Errorf("incorrect number of remote client keys after removal, got: %d, want %d", got, want) 81 | } 82 | } 83 | 84 | func TestSession_activeConnectionIDs(t *testing.T) { 85 | t.Parallel() 86 | tests := []struct { 87 | name string 88 | conns map[int64]*connection 89 | expected []int64 90 | }{ 91 | { 92 | name: "no connections", 93 | conns: map[int64]*connection{}, 94 | expected: []int64{}, 95 | }, 96 | { 97 | name: "single", 98 | conns: map[int64]*connection{ 99 | 1234: nil, 100 | }, 101 | expected: []int64{1234}, 102 | }, 103 | { 104 | name: "multiple connections", 105 | conns: map[int64]*connection{ 106 | 5: nil, 107 | 20: nil, 108 | 3: nil, 109 | }, 110 | expected: []int64{3, 5, 20}, 111 | }, 112 | } 113 | for x := range tests { 114 | tt := tests[x] 115 | t.Run(tt.name, func(t *testing.T) { 116 | t.Parallel() 117 | session := Session{conns: tt.conns} 118 | if got, want := session.activeConnectionIDs(), tt.expected; !reflect.DeepEqual(got, want) { 119 | t.Errorf("incorrect result, got: %v, want: %v", got, want) 120 | } 121 | }) 122 | } 123 | } 124 | 125 | func TestSession_sendPings(t *testing.T) { 126 | t.Parallel() 127 | 128 | conn := testServerWS(t, nil) 129 | session := newSession(rand.Int63(), "pings-test", newWSConn(conn)) 130 | 131 | pongHandler := conn.PongHandler() 132 | 133 | pongs := make(chan struct{}) 134 | conn.SetPongHandler(func(appData string) error { 135 | pongs <- struct{}{} 136 | return pongHandler(appData) 137 | }) 138 | go func() { 139 | // Read channel must be consumed (even if discarded) for control messages to work: 140 | // https://pkg.go.dev/github.com/gorilla/websocket#hdr-Control_Messages 141 | for { 142 | if _, _, err := conn.NextReader(); err != nil { 143 | return 144 | } 145 | } 146 | }() 147 | 148 | for i := 1; i <= 4; i++ { 149 | if err := session.sendPing(); err != nil { 150 | t.Fatal(err) 151 | } 152 | select { 153 | // pong received, ping was successful 154 | case <-pongs: 155 | // High timeout on purpose to avoid flakiness 156 | case <-time.After(5 * time.Second): 157 | t.Errorf("ping %d not received in time", i) 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import "time" 4 | 5 | const ( 6 | PingWaitDuration = 60 * time.Second 7 | PingWriteInterval = 5 * time.Second 8 | // SyncConnectionsInterval is the time after which the client will send the list of active connection IDs 9 | SyncConnectionsInterval = 60 * time.Second 10 | // SyncConnectionsTimeout sets the maximum duration for a SyncConnections operation 11 | SyncConnectionsTimeout = 60 * time.Second 12 | MaxRead = 8192 13 | HandshakeTimeOut = 10 * time.Second 14 | // SendErrorTimeout sets the maximum duration for sending an error message to close a single connection 15 | SendErrorTimeout = 5 * time.Second 16 | ) 17 | -------------------------------------------------------------------------------- /wsconn.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "sync" 8 | "time" 9 | 10 | "github.com/gorilla/websocket" 11 | ) 12 | 13 | type wsWrapper struct { 14 | // Mutex is used to protect from concurrent usage of the websocket connection 15 | sync.Mutex 16 | // conn is the underlying websocket connection 17 | conn *websocket.Conn 18 | } 19 | 20 | func newWSConn(conn *websocket.Conn) *wsWrapper { 21 | w := &wsWrapper{ 22 | conn: conn, 23 | } 24 | w.setupDeadline() 25 | return w 26 | } 27 | 28 | type wsConn interface { 29 | // Close will indicate the underlying websocket connection 30 | Close() error 31 | // NextReader gets a new reader from the underlying websocket connection 32 | NextReader() (int, io.Reader, error) 33 | // WriteControl writes a new websocket control frame, see https://datatracker.ietf.org/doc/html/rfc6455#section-5.5 34 | WriteControl(messageType int, deadline time.Time, data []byte) error 35 | // WriteMessage writes a new websocket data frame, see https://datatracker.ietf.org/doc/html/rfc6455#section-6 36 | WriteMessage(messageType int, deadline time.Time, data []byte) error 37 | } 38 | 39 | func (w *wsWrapper) WriteControl(messageType int, deadline time.Time, data []byte) error { 40 | w.Lock() 41 | defer w.Unlock() 42 | 43 | return w.conn.WriteControl(messageType, data, deadline) 44 | } 45 | 46 | func (w *wsWrapper) WriteMessage(messageType int, deadline time.Time, data []byte) error { 47 | if deadline.IsZero() { 48 | w.Lock() 49 | defer w.Unlock() 50 | return w.conn.WriteMessage(messageType, data) 51 | } 52 | 53 | ctx, cancel := context.WithDeadline(context.Background(), deadline) 54 | defer cancel() 55 | 56 | done := make(chan error, 1) 57 | go func() { 58 | w.Lock() 59 | defer w.Unlock() 60 | done <- w.conn.WriteMessage(messageType, data) 61 | }() 62 | 63 | select { 64 | case <-ctx.Done(): 65 | return fmt.Errorf("i/o timeout") 66 | case err := <-done: 67 | return err 68 | } 69 | } 70 | 71 | func (w *wsWrapper) NextReader() (int, io.Reader, error) { 72 | return w.conn.NextReader() 73 | } 74 | 75 | func (w *wsWrapper) Close() error { 76 | return w.conn.Close() 77 | } 78 | 79 | func (w *wsWrapper) setupDeadline() { 80 | w.conn.SetReadDeadline(time.Now().Add(PingWaitDuration)) 81 | w.conn.SetPingHandler(func(string) error { 82 | w.Lock() 83 | err := w.conn.WriteControl(websocket.PongMessage, []byte(""), time.Now().Add(PingWaitDuration)) 84 | w.Unlock() 85 | if err != nil { 86 | return err 87 | } 88 | if err := w.conn.SetReadDeadline(time.Now().Add(PingWaitDuration)); err != nil { 89 | return err 90 | } 91 | return w.conn.SetWriteDeadline(time.Now().Add(PingWaitDuration)) 92 | }) 93 | w.conn.SetPongHandler(func(string) error { 94 | if err := w.conn.SetReadDeadline(time.Now().Add(PingWaitDuration)); err != nil { 95 | return err 96 | } 97 | return w.conn.SetWriteDeadline(time.Now().Add(PingWaitDuration)) 98 | }) 99 | 100 | } 101 | -------------------------------------------------------------------------------- /wsconn_test.go: -------------------------------------------------------------------------------- 1 | package remotedialer 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "time" 7 | ) 8 | 9 | type fakeWSConn struct { 10 | writeMessageCallback func(int, time.Time, []byte) error 11 | } 12 | 13 | func (f fakeWSConn) Close() error { 14 | return nil 15 | } 16 | 17 | func (f fakeWSConn) NextReader() (int, io.Reader, error) { 18 | return 0, nil, errors.New("not implemented") 19 | } 20 | 21 | func (f fakeWSConn) WriteMessage(messageType int, deadline time.Time, data []byte) error { 22 | if cb := f.writeMessageCallback; cb != nil { 23 | return cb(messageType, deadline, data) 24 | } 25 | return errors.New("callback not provided") 26 | } 27 | 28 | func (f fakeWSConn) WriteControl(int, time.Time, []byte) error { 29 | return errors.New("not implemented") 30 | } 31 | --------------------------------------------------------------------------------