Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: rancher/remotedialer
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.4.1
Choose a base ref
...
head repository: rancher/remotedialer
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v0.4.2
Choose a head ref
  • 9 commits
  • 22 files changed
  • 7 contributors

Commits on Jan 30, 2025

  1. [main] Implement versioning ADR (#91)

    * Add release/v0.3 branch in renovate
    
    Signed-off-by: Vatsal Parekh <vatsalparekh@outlook.com>
    
    * Add release GHA
    
    Signed-off-by: Vatsal Parekh <vatsalparekh@outlook.com>
    
    * Add Versioning doc
    
    Signed-off-by: Vatsal Parekh <vatsalparekh@outlook.com>
    
    ---------
    
    Signed-off-by: Vatsal Parekh <vatsalparekh@outlook.com>
    vatsalparekh authored Jan 30, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    ff1af93 View commit details

Commits on Feb 17, 2025

  1. #47037 - Create Remote Dialer Proxy (#90)

    * [WIP] Imperative API: Proxy and port-forward (#85)
    
    * Add a port-forward type
    
    * Add support for automatic reconnects when pods are terminated
    
    * List clients
    
    * Add a Dockerfile for the proxy
    
    * Add proxy
    
    * Update go.mod
    
    * Update package name
    
    * Add config parsing
    
    * Immediately close connection when there are no clients
    
    * added proxy client and server working impl, also some examples
    
    * fixed formatting issue
    
    * added proxyclient retry
    
    * fix format err
    
    * added retry timeout
    
    * stopping forwarder when remote dialer is closed
    
    * addressing comments from josh
    
    * fixing ci
    
    * addressed comments from @tomleb + other fixes
    
    * updated to use helm chart values
    
    * adjusted naming to be according RFC
    
    * Update proxyclient/client.go
    
    Co-authored-by: Tom Lebreux <tom.lebreux@suse.com>
    
    * remove unused start
    
    * addressing more comments from @tomleb
    
    * fixed imports
    
    ---------
    
    Co-authored-by: Max Sokolovsky <genexpr@protonmail.com>
    Co-authored-by: Tom Lebreux <tom.lebreux@suse.com>
    3 people authored Feb 17, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    69cb968 View commit details

Commits on Feb 19, 2025

  1. Update module github.com/gorilla/websocket to v1.5.3 (#93)

    Co-authored-by: renovate-rancher[bot] <119870437+renovate-rancher[bot]@users.noreply.github.com>
    renovate-rancher[bot] authored Feb 19, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    09d0781 View commit details
  2. Update actions/checkout action to v4.2.2 (#94)

    Co-authored-by: renovate-rancher[bot] <119870437+renovate-rancher[bot]@users.noreply.github.com>
    renovate-rancher[bot] authored Feb 19, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    7418df3 View commit details
  3. Update actions/setup-go action to v5.3.0 (#95)

    Co-authored-by: renovate-rancher[bot] <119870437+renovate-rancher[bot]@users.noreply.github.com>
    renovate-rancher[bot] authored Feb 19, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    817e94f View commit details
  4. Update module github.com/stretchr/testify to v1.10.0 (#97)

    Co-authored-by: renovate-rancher[bot] <119870437+renovate-rancher[bot]@users.noreply.github.com>
    renovate-rancher[bot] authored Feb 19, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    09633c2 View commit details
  5. Update module github.com/rancher/dynamiclistener to v0.6.1 (#103)

    Co-authored-by: renovate-rancher[bot] <119870437+renovate-rancher[bot]@users.noreply.github.com>
    renovate-rancher[bot] authored Feb 19, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    a870e58 View commit details
  6. Add initial Renovate configuration (#89)

    Co-authored-by: renovate-rancher[bot] <119870437+renovate-rancher[bot]@users.noreply.github.com>
    renovate-rancher[bot] authored Feb 19, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    2e32f62 View commit details

Commits on Feb 21, 2025

  1. [main] Add image build on tag workflow (#105)

    * add initial image build workflow
    
    * add arm64 image build
    
    ---------
    
    Co-authored-by: joshmeranda <joshua.meranda@gmail.com>
    joshmeranda and joshmeranda authored Feb 21, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    64d0093 View commit details
2 changes: 1 addition & 1 deletion .github/renovate.json
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
"github>rancher/renovate-config#release"
],
"baseBranches": [
"master"
"main", "release/v0.3"
],
"prHourlyLimit": 2
}
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -6,10 +6,10 @@ jobs:
steps:
- name: Checkout code
# https://github.com/actions/checkout/releases/tag/v4.1.1
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Go
# https://github.com/actions/setup-go/releases/tag/v5.0.0
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version-file: 'go.mod'
- run: make test
139 changes: 139 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
name: Release

on:
push:
tags:
- v*

permissions:
contents: write

env:
REGISTRY: docker.io
REPO: rancher

jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name : Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Create release on Github
env:
GH_TOKEN: ${{ github.token }}
run: |
if [[ "${{ github.ref_name }}" == *-rc* ]]; then
gh --repo "${{ github.repository }}" release create ${{ github.ref_name }} --verify-tag --generate-notes --prerelease
else
gh --repo "${{ github.repository }}" release create ${{ github.ref_name }} --verify-tag --generate-notes
fi
image:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
strategy:
matrix:
arch:
- amd64
- arm64
name: Build and push proxy image
steps:
- name : Checkout repository
# https://github.com/actions/checkout/releases/tag/v4.1.1
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: "Read vault secrets"
uses: rancher-eio/read-vault-secrets@main
with:
secrets: |
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME;
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD
- name: Set up QEMU
# https://github.com/docker/setup-qemu-action/releases/tag/v3.1.0
uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 # v3.4.0

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
# https://github.com/docker/setup-buildx-action/releases/tag/v3.4.0

- name: Log in to the Container registry
# https://github.com/docker/login-action/releases/tag/v3.2.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.DOCKER_USERNAME }}
password: ${{ env.DOCKER_PASSWORD }}

- name: Build and push the remotedialer image
id: build
# https://github.com/docker/build-push-action/releases/tag/v6.3.0
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
with:
context: .
file: ./Dockerfile.proxy
platforms: "linux/${{ matrix.arch }}"
outputs: type=image,name=${{ env.REPO }}/remotedialer-proxy,push-by-digest=true,name-canonical=true,push=true

- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
# https://github.com/actions/upload-artifact/releases/tag/v4.3.3
with:
name: digests-${{ matrix.arch }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1

merge-images:
permissions:
id-token: write
runs-on: ubuntu-latest
needs: image
steps:
- name: Download digests
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
# https://github.com/actions/download-artifact/releases/tag/v4.1.7
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4fd812986e6c8c2a69e18311145f9371337f27d4 # v3.4.0
# https://github.com/docker/setup-buildx-action/releases/tag/v3.4.0

- name: "Read vault secrets"
uses: rancher-eio/read-vault-secrets@main
with:
secrets: |
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials username | DOCKER_USERNAME ;
secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD
- name: Log in to the Container registry
uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
# https://github.com/docker/login-action/releases/tag/v3.2.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ env.DOCKER_USERNAME }}
password: ${{ env.DOCKER_PASSWORD }}

# setup tag name
- if: ${{ startsWith(github.ref, 'refs/tags/') }}
run: |
echo TAG_NAME=$(echo $GITHUB_REF | sed -e "s|refs/tags/||") >> $GITHUB_ENV
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create -t ${{ env.REGISTRY }}/${{ env.REPO }}/remotedialer-proxy:${{ env.TAG_NAME }} \
$(printf '${{ env.REPO }}/remotedialer-proxy@sha256:%s ' *)
Original file line number Diff line number Diff line change
@@ -16,9 +16,13 @@ on:
schedule:
- cron: '30 4,6 * * *'

permissions:
contents: read
id-token: write

jobs:
call-workflow:
uses: rancher/renovate-config/.github/workflows/renovate.yml@release
uses: rancher/renovate-config/.github/workflows/renovate-vault.yml@release
with:
logLevel: ${{ inputs.logLevel || 'info' }}
overrideSchedule: ${{ github.event.inputs.overrideSchedule == 'true' && '{''schedule'':null}' || '' }}
12 changes: 12 additions & 0 deletions Dockerfile.proxy
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM golang:1.23 AS builder
WORKDIR /app

COPY . .
RUN go mod download


RUN CGO_ENABLED=0 go build -o proxy ./cmd/proxy

FROM scratch
COPY --from=builder /app/proxy .
CMD ["./proxy"]
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -194,3 +194,7 @@ Finally, use the second server to make a request to the client via the first ser
```
curl http://localhost:8124/client/foo/http/127.0.0.1:8125/
```

# Versioning

See [VERSION.md](VERSION.md).
8 changes: 8 additions & 0 deletions VERSION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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.

The current supported release lines are:

| RemoteDialer Branch | RemoteDialer Minor version |
|--------------------------|------------------------------------|
| main | v0.4 |
| release/v0.3 | v0.3 |
28 changes: 28 additions & 0 deletions cmd/proxy/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package main

import (
"github.com/sirupsen/logrus"
"k8s.io/client-go/rest"

"github.com/rancher/remotedialer/proxy"
)

func main() {
logrus.Info("Starting Remote Dialer Proxy")

cfg, err := proxy.ConfigFromEnvironment()
if err != nil {
logrus.Fatalf("fatal configuration error: %v", err)
}

restConfig, err := rest.InClusterConfig()
if err != nil {
logrus.Errorf("failed to get in-cluster config: %s", err.Error())
return
}

err = proxy.Start(cfg, restConfig)
if err != nil {
logrus.Fatal(err)
}
}
93 changes: 93 additions & 0 deletions cmd/proxy/proxy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#FIXME This is temporary file. This should be converted into Helm Charts in the charts repo.

apiVersion: apps/v1
kind: Deployment
metadata:
name: remotedialer-proxy
namespace: cattle-system
labels:
app: remotedialer-proxy
spec:
replicas: 1
selector:
matchLabels:
app: remotedialer-proxy
template:
metadata:
labels:
app: remotedialer-proxy
spec:
containers:
- name: remotedialer-proxy
image: rancher/remotedialer-proxy:latest
imagePullPolicy: IfNotPresent
env:
- name: TLS_NAME
value: "remotedialer-proxy"
- name: CA_NAME
value: "remotedialer-proxy-ca"
- name: CERT_CA_NAMESPACE
value: "cattle-system"
- name: CERT_CA_NAME
value: "remotedialer-proxy-cert"
- name: SECRET
value: "secret" # X-Tunnel-ID header secret
- name: PROXY_PORT
value: "6666" # The proxy TCP port for kube-apiserver traffic
- name: PEER_PORT
value: "8888" # The port used to connect to the special "imperative API" server behind the remotedialer
- name: HTTPS_PORT
value: "8443" # The dynamiclistener HTTPS port for /connect
ports:
- containerPort: 6666
name: proxy
- containerPort: 8443
name: https
- containerPort: 8888
name: peer

---
apiVersion: v1
kind: Service
metadata:
name: remotedialer-proxy
namespace: cattle-system
labels:
app: remotedialer-proxy
spec:
type: ClusterIP
selector:
app: remotedialer-proxy
ports:
- name: proxy
port: 6666
targetPort: proxy
- name: https
port: 8443
targetPort: https

---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: remotedialer-proxy-secret-access
namespace: cattle-system
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "create", "update"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: remotedialer-proxy-secret-access-binding
namespace: cattle-system
subjects:
- kind: ServiceAccount
name: default
namespace: cattle-system
roleRef:
kind: Role
name: remotedialer-proxy-secret-access
apiGroup: rbac.authorization.k8s.io
12 changes: 12 additions & 0 deletions examples/fakek8s/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM golang:1.23 as builder
WORKDIR /app
COPY . .
RUN go mod download
RUN go build -o /app/tcp-client main.go

FROM debian:bookworm-slim
COPY --from=builder /app/tcp-client .

RUN ls .

CMD ["./tcp-client"]
28 changes: 28 additions & 0 deletions examples/fakek8s/fakek8s.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: fakek8s-deployment
namespace: cattle-system
labels:
app: fakek8s
spec:
replicas: 1
selector:
matchLabels:
app: fakek8s
template:
metadata:
labels:
app: fakek8s
spec:
containers:
- name: fakek8s
image: rancher/fakek8s:latest
imagePullPolicy: IfNotPresent
env:
- name: TARGET_HOST
value: "api-extension.cattle-system.svc.cluster.local"
- name: TARGET_PORT
value: "6666"
- name: SEND_INTERVAL
value: "1"
3 changes: 3 additions & 0 deletions examples/fakek8s/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module dummy/fakek8s

go 1.23
113 changes: 113 additions & 0 deletions examples/fakek8s/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package main

import (
"context"
"fmt"
"net"
"os"
"os/signal"
"strconv"
"syscall"
"time"
)

var (
targetHost = "api-extension.cattle-system.svc.cluster.local"
targetPort = 6666
retryDelay = 5 * time.Second
)

func init() {
if host, ok := os.LookupEnv("TARGET_HOST"); ok {
targetHost = host
}

if portStr, ok := os.LookupEnv("TARGET_PORT"); ok {
if p, err := strconv.Atoi(portStr); err != nil {
fmt.Printf("Could not parse TARGET_PORT=%q: %v. Using default %d.\n",
portStr, err, targetPort)
} else {
targetPort = p
}
}

if intervalStr, ok := os.LookupEnv("SEND_INTERVAL"); ok {
if i, err := strconv.Atoi(intervalStr); err != nil {
fmt.Printf("Could not parse SEND_INTERVAL=%q: %v. Using default %v.\n",
intervalStr, err, retryDelay)
} else {
retryDelay = time.Duration(i) * time.Second
}
}
}

func echoHandler(ctx context.Context, conn net.Conn) {
defer conn.Close()
go func() {
<-ctx.Done()
fmt.Println("echoHandler: context canceled; closing connection.")
_ = conn.Close()
}()

buffer := make([]byte, 1024)
for {
n, err := conn.Read(buffer)
if err != nil {
fmt.Printf("Connection closed or error occurred: %v\n", err)
return
}

fmt.Println("Received from Server:", string(buffer[:n]))

// Echo back the received data
if _, err := conn.Write(buffer[:n]); err != nil {
fmt.Printf("Error sending data back: %v\n", err)
return
}

fmt.Println("Sent back to Server:", string(buffer[:n]))
}
}

func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
fmt.Println("main: received shutdown signal; canceling context...")
cancel()
}()

for {
select {
case <-ctx.Done():
fmt.Println("main: context canceled; exiting dial loop.")
return
default:
}

fmt.Printf("Attempting to connect to %s:%d...\n", targetHost, targetPort)

conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", targetHost, targetPort))
if err != nil {
fmt.Printf("Failed to connect: %v. Retrying in %v...\n", err, retryDelay)
time.Sleep(retryDelay)
continue
}

fmt.Println("Connected to the server.")

// Send a welcome message
welcomeMessage := "Hello, server! Client has connected.\nPlease type any word and hit enter:"
if _, err = conn.Write([]byte(welcomeMessage)); err != nil {
fmt.Printf("Error sending welcome message: %v\n", err)
conn.Close()
continue
}

echoHandler(ctx, conn)
}
}
196 changes: 196 additions & 0 deletions examples/proxyclient/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package main

import (
"bufio"
"context"
"fmt"
"net"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"

"github.com/rancher/remotedialer/forward"
proxyclient "github.com/rancher/remotedialer/proxyclient"
"github.com/rancher/wrangler/v3/pkg/generated/controllers/core"
"github.com/sirupsen/logrus"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
)

var (
namespace = "cattle-system"
label = "app=api-extension"
certSecretName = "api-extension-ca-name"
certServerName = "api-extension-tls-name"
connectSecret = "api-extension"
ports = []string{"5555:8443"}
fakeImperativeAPIAddr = "0.0.0.0:8888"
)

func init() {
if val, ok := os.LookupEnv("NAMESPACE"); ok {
namespace = val
}
if val, ok := os.LookupEnv("LABEL"); ok {
label = val
}
if val, ok := os.LookupEnv("CERT_SECRET_NAME"); ok {
certSecretName = val
}
if val, ok := os.LookupEnv("CERT_SERVER_NAME"); ok {
certServerName = val
}
if val, ok := os.LookupEnv("CONNECT_SECRET"); ok {
connectSecret = val
}
if val, ok := os.LookupEnv("PORTS"); ok {
ports = strings.Split(val, ",")
}
if val, ok := os.LookupEnv("FAKE_IMPERATIVE_API_ADDR"); ok {
fakeImperativeAPIAddr = val
}
}

func handleConnection(ctx context.Context, conn net.Conn) {
go func() {
<-ctx.Done()
fmt.Println("handleConnection: context canceled; closing connection.")
_ = conn.Close()
}()

defer fmt.Println("handleConnection: exiting for", conn.RemoteAddr())
defer conn.Close()

buffer := make([]byte, 1024)
for {
n, err := conn.Read(buffer)
if err != nil {
fmt.Println("Connection closed or error occurred:", err)
return
}
fmt.Println("Received from Client", string(buffer[:n]))
}
}

func handleKeyboardInput(ctx context.Context, conn net.Conn) {
go func() {
<-ctx.Done()
fmt.Println("handleKeyboardInput: context canceled; closing connection.")
_ = conn.Close()
}()

defer fmt.Println("handleKeyboardInput: exiting for", conn.RemoteAddr())
defer conn.Close()

reader := bufio.NewReader(os.Stdin)
for {
input, err := reader.ReadByte()
if err != nil {
fmt.Println("Error reading keyboard input:", err)
return
}

_, err = conn.Write([]byte{input})
if err != nil {
fmt.Println("Error sending data to client:", err)
return
}
}
}

func fakeImperativeAPI(ctx context.Context) error {
ln, err := net.Listen("tcp", fakeImperativeAPIAddr)
if err != nil {
return fmt.Errorf("Error starting server on %s: %w", fakeImperativeAPIAddr, err)
}
fmt.Printf("Server listening on %s...\n", fakeImperativeAPIAddr)

go func() {
<-ctx.Done()
fmt.Println("fakeImperativeAPI: context canceled; closing listener.")
_ = ln.Close()
}()

for {
conn, acceptErr := ln.Accept()
if acceptErr != nil {
select {
case <-ctx.Done():
fmt.Println("fakeImperativeAPI: accept loop stopping; context is done.")
return nil
default:
return fmt.Errorf("fakeImperativeAPI: error accepting connection: %w", acceptErr)
}
}

fmt.Println("Connection established with client:", conn.RemoteAddr())

go handleConnection(ctx, conn)
go handleKeyboardInput(ctx, conn)
}
}

func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

go func() {
if err := fakeImperativeAPI(ctx); err != nil {
logrus.Errorf("fakeImperativeAPI error: %v", err)
cancel()
}
}()

home := homedir.HomeDir()
kubeConfigPath := filepath.Join(home, ".kube", "config")
cfg, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
if err != nil {
panic(err.Error())
}

coreFactory, err := core.NewFactoryFromConfigWithOptions(cfg, nil)
if err != nil {
logrus.Fatal(err)
}

if err := coreFactory.Start(ctx, 1); err != nil {
logrus.Fatal(err)
}

podClient := coreFactory.Core().V1().Pod()
secretContoller := coreFactory.Core().V1().Secret()

portForwarder, err := forward.New(cfg, podClient, namespace, label, ports)
if err != nil {
logrus.Fatal(err)
}

proxyClient, err := proxyclient.New(
ctx,
connectSecret,
namespace,
certSecretName,
certServerName,
secretContoller,
portForwarder,
)
if err != nil {
logrus.Fatal(err)
}

go func() {
logrus.Info("RDP Client Started... Waiting for CTRL+C")
<-sigChan
logrus.Info("Stopping...")

cancel()
proxyClient.Stop()
}()

proxyClient.Run(ctx)
}
173 changes: 173 additions & 0 deletions forward/forward.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package forward

import (
"bytes"
"context"
"errors"
"fmt"
"math/rand"
"net/http"
"net/url"
"strings"
"time"

v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1"
"github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/portforward"
"k8s.io/client-go/transport/spdy"
)

var (
podConnectionRetryTimeout = 1 * time.Second
)

type PortForward struct {
restConfig *rest.Config
podClient v1.PodController
namespace string
labelSelector string
ports []string

readyCh chan struct{}
readyErr chan error
cancel context.CancelFunc
}

func New(restConfig *rest.Config, podClient v1.PodController, namespace string, labelSelector string, ports []string) (*PortForward, error) {
if restConfig == nil {
return nil, fmt.Errorf("restConfig must not be nil")
}
if podClient == nil {
return nil, fmt.Errorf("podClient must not be nil")
}
if labelSelector == "" {
return nil, fmt.Errorf("labelSelector must not be empty")
}
if len(ports) == 0 {
return nil, fmt.Errorf("ports must not be empty")
}
if namespace == "" {
return nil, fmt.Errorf("namespace must not be empty")
}

for _, p := range ports {
if strings.HasPrefix(p, "0:") {
return nil, fmt.Errorf("cannot bind port zero")
}
}

return &PortForward{
restConfig: restConfig,
podClient: podClient,
namespace: namespace,
labelSelector: labelSelector,
ports: ports,
readyCh: make(chan struct{}, 1),
}, nil
}

func (r *PortForward) Stop() {
r.cancel()
}

func (r *PortForward) Start() error {
ctx, cancel := context.WithCancel(context.Background())

r.cancel = cancel
r.readyCh = make(chan struct{}, 1)
r.readyErr = make(chan error, 1)

go func() {
for {
select {
case <-ctx.Done():
logrus.Infoln("Goroutine stopped.")
return
default:
err := r.runForwarder(ctx, r.readyCh, r.ports)
if err != nil {
if errors.Is(err, portforward.ErrLostConnectionToPod) {
logrus.Errorf("Lost connection to pod: %v, retrying in %d secs.", err, podConnectionRetryTimeout/time.Second)
} else {
logrus.Errorf("Non-restartable error: %v", err)
r.readyErr <- err
return
}
}
}
}
}()

for {
select {
case <-ctx.Done():
return nil

case <-r.readyCh:
return nil

case err := <-r.readyErr:
if err != nil {
return err
}
return nil
}
}
}

func (r *PortForward) runForwarder(ctx context.Context, readyCh chan struct{}, ports []string) error {
podName, err := lookForPodName(ctx, r.namespace, r.labelSelector, r.podClient)
if err != nil {
return err
}
logrus.Infof("Selected pod %q for label %q", podName, r.labelSelector)

path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", r.namespace, podName)
hostIP := strings.TrimPrefix(r.restConfig.Host, "https://")
serverURL := url.URL{
Scheme: "https",
Path: path,
Host: hostIP,
}

roundTripper, upgrader, err := spdy.RoundTripperFor(r.restConfig)
if err != nil {
return err
}
dialer := spdy.NewDialer(upgrader, &http.Client{
Transport: roundTripper,
}, http.MethodPost, &serverURL)

stdout, stderr := new(bytes.Buffer), new(bytes.Buffer)
forwarder, err := portforward.New(dialer, ports, ctx.Done(), readyCh, stdout, stderr)
if err != nil {
return err
}

return forwarder.ForwardPorts()
}

func lookForPodName(ctx context.Context, namespace, labelSelector string, podClient v1.PodClient) (string, error) {
for {
select {
case <-ctx.Done():
return "", ctx.Err()
default:
pods, err := podClient.List(namespace, metav1.ListOptions{
LabelSelector: labelSelector,
})
if err != nil {
return "", err
}
if len(pods.Items) < 1 {
logrus.Debugf("no pod found with label selector %q, retrying in 1s", labelSelector)
time.Sleep(time.Second)
continue
}
i := rand.Intn(len(pods.Items))
return pods.Items[i].Name, nil
}
}
}
75 changes: 58 additions & 17 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,30 +1,71 @@
module github.com/rancher/remotedialer

go 1.20
go 1.23

require (
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.1
github.com/gorilla/websocket v1.5.3
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.16.0
github.com/prometheus/client_golang v1.19.1
github.com/rancher/dynamiclistener v0.6.1
github.com/rancher/wrangler/v3 v3.0.1-rc.2
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.10.0
k8s.io/api v0.31.1
k8s.io/apimachinery v0.31.1
k8s.io/client-go v0.31.1
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch v5.9.0+incompatible // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/moby/spdystream v0.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rancher/lasso v0.0.0-20240924233157-8f384efc8813 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/term v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
207 changes: 173 additions & 34 deletions go.sum

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions proxy/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package proxy

import (
"fmt"
"os"
"strconv"
)

type Config struct {
TLSName string // certificate client name (SAN)
CAName string // certificate authority secret name
CertCANamespace string // certificate secret namespace
CertCAName string // certificate secret name
Secret string // remotedialer secret
ProxyPort int // tcp remotedialer-proxy port
PeerPort int // cluster-external service port
HTTPSPort int // https remotedialer-proxy port
}

func requiredString(key string) (string, error) {
value := os.Getenv(key)
if value == "" {
return "", fmt.Errorf("%s cannot be empty", key)
}
return value, nil
}

func requiredPort(key string) (int, error) {
valueStr := os.Getenv(key)
port, err := strconv.Atoi(valueStr)
if err != nil {
return 0, fmt.Errorf("failed to read %s: %w", key, err)
}
if port <= 0 {
return 0, fmt.Errorf("%s should be greater than 0", key)
}
return port, nil
}

func ConfigFromEnvironment() (*Config, error) {
var err error
var config Config

if config.TLSName, err = requiredString("TLS_NAME"); err != nil {
return nil, err
}
if config.CAName, err = requiredString("CA_NAME"); err != nil {
return nil, err
}
if config.CertCANamespace, err = requiredString("CERT_CA_NAMESPACE"); err != nil {
return nil, err
}
if config.CertCAName, err = requiredString("CERT_CA_NAME"); err != nil {
return nil, err
}
if config.Secret, err = requiredString("SECRET"); err != nil {
return nil, err
}
if config.ProxyPort, err = requiredPort("PROXY_PORT"); err != nil {
return nil, err
}
if config.PeerPort, err = requiredPort("PEER_PORT"); err != nil {
return nil, err
}
if config.HTTPSPort, err = requiredPort("HTTPS_PORT"); err != nil {
return nil, err
}

return &config, nil
}
154 changes: 154 additions & 0 deletions proxy/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package proxy

import (
"context"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"time"

"github.com/gorilla/mux"
"github.com/rancher/dynamiclistener"
"github.com/rancher/dynamiclistener/server"

"github.com/rancher/wrangler/v3/pkg/generated/controllers/core"
"github.com/sirupsen/logrus"
"k8s.io/client-go/rest"

"github.com/rancher/remotedialer"
)

const (
listClientsRetryCount = 10
listClientSleepTime = 1 * time.Second
)

func runProxyListener(ctx context.Context, cfg *Config, server *remotedialer.Server) error {
l, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", cfg.ProxyPort)) //this RDP app starts only once and always running
if err != nil {
return err
}
defer l.Close()

for {
conn, err := l.Accept() // the client of 6666 is kube-apiserver, according to the APIService object spec, just to this TCP 6666
if err != nil {
logrus.Errorf("proxy TCP connection accept failed: %v", err)
continue
}

go func() {
var retryTimes = 0
for {
clients := server.ListClients()
if len(clients) == 0 {
retryTimes++
if retryTimes > listClientsRetryCount {
conn.Close()
return
}

logrus.Info("proxy TCP connection failed: no clients, retrying in a sec")
time.Sleep(listClientSleepTime)
} else {
client := clients[rand.Intn(len(clients))]
peerAddr := fmt.Sprintf(":%d", cfg.PeerPort) // rancher's special https server for imperative API
clientConn, err := server.Dialer(client)(ctx, "tcp", peerAddr)
if err != nil {
logrus.Errorf("proxy dialing %s failed: %v", peerAddr, err)
conn.Close()
return
}

go pipe(conn, clientConn)
go pipe(clientConn, conn)
break
}
}
}()
}
}

func pipe(a, b net.Conn) {
defer func(a net.Conn) {
if err := a.Close(); err != nil {
logrus.Errorf("proxy TCP connection close failed: %v", err)
}
}(a)
defer func(b net.Conn) {
if err := b.Close(); err != nil {
logrus.Errorf("proxy TCP connection close failed: %v", err)
}
}(b)
n, err := io.Copy(a, b)
if err != nil {
logrus.Errorf("proxy copy failed: %v", err)
return
}
logrus.Debugf("proxy copied %d bytes to %v from %v", n, a.LocalAddr(), b.LocalAddr())
}

func Start(cfg *Config, restConfig *rest.Config) error {
logrus.SetLevel(logrus.DebugLevel)
ctx := context.Background()

// Setting Up Default Authorizer
authorizer := func(req *http.Request) (string, bool, error) {
id := req.Header.Get("X-API-Tunnel-Secret")
if id != cfg.Secret {
return "", false, fmt.Errorf("X-API-Tunnel-Secret not specified in request header")
}
return id, true, nil
}

// Initializing Remote Dialer Server
remoteDialerServer := remotedialer.New(authorizer, remotedialer.DefaultErrorWriter)

router := mux.NewRouter()
router.Handle("/connect", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
logrus.Info("got a connection")
remoteDialerServer.ServeHTTP(w, req)
}))

go func() {
if err := runProxyListener(ctx, cfg, remoteDialerServer); err != nil {
logrus.Errorf("proxy listener failed to start in the background: %v", err)
}
}()

// Setting Up Secret Controller
core, err := core.NewFactoryFromConfigWithOptions(restConfig, nil)
if err != nil {
return fmt.Errorf("build secret controller failed w/ err: %w", err)
}

if err := core.Start(ctx, 1); err != nil {
return fmt.Errorf("secretController factory start failed: %w", err)
}

secretController := core.Core().V1().Secret()

// Setting Up Remote Dialer HTTPS Server
if err := server.ListenAndServe(ctx, cfg.HTTPSPort, 0, router, &server.ListenOpts{
Secrets: secretController,
CAName: cfg.CAName,
CertName: cfg.CertCAName,
CertNamespace: cfg.CertCANamespace,
TLSListenerConfig: dynamiclistener.Config{
SANs: []string{cfg.TLSName},
FilterCN: func(cns ...string) []string {
return []string{cfg.TLSName}
},
RegenerateCerts: func() bool {
return true
},
ExpirationDaysCheck: 10,
},
}); err != nil {
return fmt.Errorf("extension server exited with an error: %w", err)
}
<-ctx.Done()
return nil
}
222 changes: 222 additions & 0 deletions proxyclient/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package proxyclient

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"sync"
"time"

"github.com/gorilla/websocket"
"github.com/rancher/remotedialer"
v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1"

"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
defaultServerAddr = "wss://127.0.0.1"
defaultServerPort = 5555
defaultServerPath = "/connect"
retryTimeout = 1 * time.Second
certificateWatchInterval = 10 * time.Second
)

type PortForwarder interface {
Start() error
Stop()
}

type ProxyClientOpt func(*ProxyClient)

type ProxyClient struct {
forwarder PortForwarder
serverUrl string
serverConnectSecret string

dialer *websocket.Dialer
dialerMtx sync.Mutex

secretController v1.SecretController
namespace string
certSecretName string
certServerName string

onConnect func(ctx context.Context, session *remotedialer.Session) error
}

func New(ctx context.Context, serverSharedSecret, namespace, certSecretName, certServerName string, secretController v1.SecretController, forwarder PortForwarder, opts ...ProxyClientOpt) (*ProxyClient, error) {
if secretController == nil {
return nil, fmt.Errorf("SecretController required")
}

if forwarder == nil {
return nil, fmt.Errorf("a PortForwarder must be provided")
}

if namespace == "" {
return nil, fmt.Errorf("namespace required")
}

if certSecretName == "" {
return nil, fmt.Errorf("certSecretName required")
}

if serverSharedSecret == "" {
return nil, fmt.Errorf("server shared secret must be provided")
}

serverUrl := fmt.Sprintf("%s:%d%s", defaultServerAddr, defaultServerPort, defaultServerPath)

client := &ProxyClient{
serverUrl: serverUrl,
forwarder: forwarder,
serverConnectSecret: serverSharedSecret,
certSecretName: certSecretName,
certServerName: certServerName,
namespace: namespace,
}

if err := client.buildDialer(ctx, secretController); err != nil {
return nil, fmt.Errorf("dialer build failed %w: ", err)
}

for _, opt := range opts {
opt(client)
}

return client, nil
}

func (c *ProxyClient) buildDialer(ctx context.Context, secretController v1.SecretController) error {
secretController.OnChange(ctx, "remotedialer-proxy", func(_ string, newSecret *corev1.Secret) (*corev1.Secret, error) {
if newSecret.Name == c.certSecretName && newSecret.Namespace == c.namespace {
rootCAs, err := buildCertFromSecret(c.namespace, c.certSecretName, newSecret)
if err != nil {
logrus.Errorf("build certificate failed: %s", err.Error())
return nil, err
}

c.dialerMtx.Lock()
c.dialer = &websocket.Dialer{
TLSClientConfig: &tls.Config{
RootCAs: rootCAs,
ServerName: c.certServerName,
},
}
c.dialerMtx.Unlock()
logrus.Infof("certificate updated successfully")
}

return newSecret, nil
})

secret, err := secretController.Get(c.namespace, c.certSecretName, metav1.GetOptions{})
if err != nil {
return err
}

rootCAs, err := buildCertFromSecret(c.namespace, c.certSecretName, secret)
if err != nil {
return fmt.Errorf("build certificate failed: %w", err)
}

c.dialerMtx.Lock()
c.dialer = &websocket.Dialer{
TLSClientConfig: &tls.Config{
RootCAs: rootCAs,
ServerName: c.certServerName,
},
}
c.dialerMtx.Unlock()

return nil
}

func buildCertFromSecret(namespace, certSecretName string, secret *corev1.Secret) (*x509.CertPool, error) {
crtData, exists := secret.Data["tls.crt"]
if !exists {
return nil, fmt.Errorf("secret %s/%s missing tls.crt field", namespace, certSecretName)
}

rootCAs := x509.NewCertPool()
if ok := rootCAs.AppendCertsFromPEM(crtData); !ok {
return nil, fmt.Errorf("failed to parse tls.crt from secret into a CA pool")
}

return rootCAs, nil
}

func (c *ProxyClient) Run(ctx context.Context) {
go func() {
for {
select {
case <-ctx.Done():
logrus.Infof("ProxyClient: ClientConnect finished. If no error, the session closed cleanly.")
return

default:
if err := c.forwarder.Start(); err != nil {
logrus.Errorf("remotedialer.ProxyClient error: %s ", err)
time.Sleep(retryTimeout)
continue
}

logrus.Infof("ProxyClient connecting to %s", c.serverUrl)

headers := http.Header{}
headers.Set("X-API-Tunnel-Secret", c.serverConnectSecret)

onConnectAuth := func(proto, address string) bool { return true }
onConnect := func(sessionCtx context.Context, session *remotedialer.Session) error {
logrus.Infoln("ProxyClient: remotedialer session connected!")
if c.onConnect != nil {
return c.onConnect(sessionCtx, session)
}
return nil
}

c.dialerMtx.Lock()
dialer := c.dialer
c.dialerMtx.Unlock()

if err := remotedialer.ClientConnect(ctx, c.serverUrl, headers, dialer, onConnectAuth, onConnect); err != nil {
logrus.Errorf("remotedialer.ClientConnect error: %s", err.Error())
c.forwarder.Stop()
time.Sleep(retryTimeout)
}
}
}
}()

<-ctx.Done()
}

func (c *ProxyClient) Stop() {
if c.forwarder != nil {
c.forwarder.Stop()
logrus.Infoln("ProxyClient: port-forward stopped.")
}
}

func WithServerURL(serverUrl string) ProxyClientOpt {
return func(pc *ProxyClient) {
pc.serverUrl = serverUrl
}
}

func WithOnConnectCallback(onConnect func(ctx context.Context, session *remotedialer.Session) error) ProxyClientOpt {
return func(pc *ProxyClient) {
pc.onConnect = onConnect
}
}

func WithCustomDialer(dialer *websocket.Dialer) ProxyClientOpt {
return func(pc *ProxyClient) {
pc.dialer = dialer
}
}
4 changes: 4 additions & 0 deletions server.go
Original file line number Diff line number Diff line change
@@ -79,6 +79,10 @@ func (s *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
}

func (s *Server) ListClients() []string {
return s.sessions.listClients()
}

func (s *Server) auth(req *http.Request) (clientKey string, authed, peer bool, err error) {
id := req.Header.Get(ID)
token := req.Header.Get(Token)
11 changes: 11 additions & 0 deletions session_manager.go
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import (
"sync"

"github.com/gorilla/websocket"

"github.com/rancher/remotedialer/metrics"
)

@@ -66,6 +67,16 @@ func (sm *sessionManager) addListener(listener sessionListener) {
}
}

func (sm *sessionManager) listClients() []string {
sm.Lock()
defer sm.Unlock()
clients := make([]string, 0, len(sm.clients))
for c := range sm.clients {
clients = append(clients, c)
}
return clients
}

func (sm *sessionManager) getDialer(clientKey string) (Dialer, error) {
sm.Lock()
defer sm.Unlock()