├── .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 | 
25 |
26 | ### Data flow
27 |
28 | 
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 |
--------------------------------------------------------------------------------