diff --git a/Dockerfile.proxy b/Dockerfile.proxy new file mode 100644 index 0000000..3166ebb --- /dev/null +++ b/Dockerfile.proxy @@ -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"] diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go new file mode 100644 index 0000000..dc16b2f --- /dev/null +++ b/cmd/proxy/main.go @@ -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) + } +} diff --git a/cmd/proxy/proxy.yaml b/cmd/proxy/proxy.yaml new file mode 100644 index 0000000..d8330eb --- /dev/null +++ b/cmd/proxy/proxy.yaml @@ -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 diff --git a/examples/fakek8s/Dockerfile b/examples/fakek8s/Dockerfile new file mode 100644 index 0000000..7ec1ace --- /dev/null +++ b/examples/fakek8s/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.23 as builder +WORKDIR /app +COPY go.mod . +RUN go mod download +COPY . . +RUN go build -o /app/tcp-client main.go + +FROM debian:bookworm-slim +COPY --from=builder /app/tcp-client . + +RUN ls . + +CMD ["./tcp-client"] \ No newline at end of file diff --git a/examples/fakek8s/fakek8s.yaml b/examples/fakek8s/fakek8s.yaml new file mode 100644 index 0000000..c2cd35a --- /dev/null +++ b/examples/fakek8s/fakek8s.yaml @@ -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: "remotedialer-proxy.cattle-system.svc.cluster.local" + - name: TARGET_PORT + value: "6666" + - name: SEND_INTERVAL + value: "1" diff --git a/examples/fakek8s/go.mod b/examples/fakek8s/go.mod new file mode 100644 index 0000000..b0dda9e --- /dev/null +++ b/examples/fakek8s/go.mod @@ -0,0 +1,3 @@ +module dummy/fakek8s + +go 1.23 diff --git a/examples/fakek8s/main.go b/examples/fakek8s/main.go new file mode 100644 index 0000000..d6741ef --- /dev/null +++ b/examples/fakek8s/main.go @@ -0,0 +1,113 @@ +package main + +import ( + "context" + "fmt" + "net" + "os" + "os/signal" + "strconv" + "syscall" + "time" +) + +var ( + targetHost = "remotedialer-proxy.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) + } +} diff --git a/examples/proxyclient/main.go b/examples/proxyclient/main.go new file mode 100644 index 0000000..04ecd0a --- /dev/null +++ b/examples/proxyclient/main.go @@ -0,0 +1,191 @@ +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=remotedialer-proxy" + certSecretName = "remotedialer-proxy-cert" + certServerName = "remotedialer-proxy" + connectSecret = "secret" + 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) + } + + podClient := coreFactory.Core().V1().Pod() + + portForwarder, err := forward.New(cfg, podClient, namespace, label, ports) + if err != nil { + logrus.Fatal(err) + } + + proxyClient, err := proxyclient.New( + ctx, + connectSecret, + namespace, + certSecretName, + certServerName, + cfg, + 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) +} diff --git a/forward/forward.go b/forward/forward.go new file mode 100644 index 0000000..ac6f68d --- /dev/null +++ b/forward/forward.go @@ -0,0 +1,168 @@ +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" +) + +type PortForward struct { + restConfig *rest.Config + podClient v1.PodController + namespace string + labelSelector string + ports []string + + readyCh chan struct{} + 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 { + var readyErr error + + ctx, cancel := context.WithCancel(context.Background()) + + r.cancel = cancel + r.readyCh = make(chan struct{}, 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 (no automatic retry in this refactor): %v", err) + } else { + logrus.Errorf("Non-restartable error: %v", err) + r.readyCh <- struct{}{} + readyErr = err + return + } + } + } + } + }() + + // wait for the port forward to be ready if not failed or cancelled + for { + select { + case <-ctx.Done(): + return nil + + case <-r.readyCh: + if readyErr != nil { + return readyErr + } + 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 + } + } +} diff --git a/go.mod b/go.mod index 6e885c5..8376b97 100644 --- a/go.mod +++ b/go.mod @@ -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/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.0 + github.com/rancher/wrangler/v3 v3.0.1-rc.2 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.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 ) diff --git a/go.sum b/go.sum index b2853c7..2b8df16 100644 --- a/go.sum +++ b/go.sum @@ -1,60 +1,199 @@ +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= +github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8= +github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= -github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= -github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= -github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= -github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rancher/dynamiclistener v0.6.0 h1:M7x8Nq+GY0UORULANuW/AH1ocnyZaqlmTuviMQAHL1Q= +github.com/rancher/dynamiclistener v0.6.0/go.mod h1:7VNEQhAwzbYJ08S1MYb6B4vili6K7CcrG4cNZXq1j+s= +github.com/rancher/lasso v0.0.0-20240924233157-8f384efc8813 h1:V/LY8pUHZG9Kc+xEDWDOryOnCU6/Q+Lsr9QQEQnshpU= +github.com/rancher/lasso v0.0.0-20240924233157-8f384efc8813/go.mod h1:IxgTBO55lziYhTEETyVKiT8/B5Rg92qYiRmcIIYoPgI= +github.com/rancher/wrangler/v3 v3.0.1-rc.2 h1:sHrZTPNco7SCNw372sv51DMK9a53ra/YboL4sQJjEQM= +github.com/rancher/wrangler/v3 v3.0.1-rc.2/go.mod h1:eXqcPIuGWblud9Wd1Auh7AWRHd6gs2H24asMMPuUR/s= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= +k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= +k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= +k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= +k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/proxy/config.go b/proxy/config.go new file mode 100644 index 0000000..03ec854 --- /dev/null +++ b/proxy/config.go @@ -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 +} diff --git a/proxy/server.go b/proxy/server.go new file mode 100644 index 0000000..6e3074c --- /dev/null +++ b/proxy/server.go @@ -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 +} diff --git a/proxyclient/client.go b/proxyclient/client.go new file mode 100644 index 0000000..6b4e8fe --- /dev/null +++ b/proxyclient/client.go @@ -0,0 +1,239 @@ +package proxyclient + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + "sync" + "time" + + "github.com/gorilla/websocket" + "github.com/rancher/remotedialer" + "github.com/rancher/wrangler/v3/pkg/generated/controllers/core" + 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" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" +) + +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, restConfig *rest.Config, forwarder PortForwarder, opts ...ProxyClientOpt) (*ProxyClient, error) { + if restConfig == nil { + return nil, fmt.Errorf("restConfig 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, restConfig); 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, restConfig *rest.Config) error { + core, err := core.NewFactoryFromConfigWithOptions(restConfig, nil) + if err != nil { + return fmt.Errorf("build secret controller failed: %w", err) + } + + secretController := core.Core().V1().Secret() + secretController.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + UpdateFunc: func(oldSecret, newSecret interface{}) { + updatedSecretCert, ok := newSecret.(*corev1.Secret) + if ok { + if updatedSecretCert.Name == c.certSecretName { + rootCAs, err := buildCertFromSecret(c.namespace, c.certSecretName, updatedSecretCert) + if err != nil { + logrus.Errorf("build certificate failed: %s", err.Error()) + return + } + + c.dialerMtx.Lock() + c.dialer = &websocket.Dialer{ + TLSClientConfig: &tls.Config{ + RootCAs: rootCAs, + ServerName: c.certServerName, + }, + } + c.dialerMtx.Unlock() + logrus.Infof("certificate updated successfully") + } + } + }, + }) + + if err := core.Start(ctx, 1); err != nil { + return fmt.Errorf("secret controller factory start failed: %w", err) + } + + 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 + } +} diff --git a/server.go b/server.go index 17cf77f..e752a21 100644 --- a/server.go +++ b/server.go @@ -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) diff --git a/session_manager.go b/session_manager.go index 6fa0a05..36eef21 100644 --- a/session_manager.go +++ b/session_manager.go @@ -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()