From 8415124ff109a5ac94437ebb4de1886e5ff308a9 Mon Sep 17 00:00:00 2001 From: Nick Santos Date: Wed, 3 Feb 2021 16:29:13 -0500 Subject: [PATCH] socat: add a utility to forward ports to a remote docker server (#94) --- internal/socat/socat.go | 92 +++++++++++++++++++++++++++++++++++++++++ pkg/cmd/root.go | 1 + pkg/cmd/socat.go | 49 ++++++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 internal/socat/socat.go create mode 100644 pkg/cmd/socat.go diff --git a/internal/socat/socat.go b/internal/socat/socat.go new file mode 100644 index 0000000..1a3f3fa --- /dev/null +++ b/internal/socat/socat.go @@ -0,0 +1,92 @@ +// Manage socat network routers for remote docker instances. +package socat + +import ( + "context" + "fmt" + "net" + "os/exec" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" +) + +const serviceName = "ctlptl-portforward-service" + +type ContainerClient interface { + ContainerInspect(ctx context.Context, containerID string) (types.ContainerJSON, error) + ContainerRemove(ctx context.Context, id string, options types.ContainerRemoveOptions) error +} + +type Controller struct { + client ContainerClient +} + +func NewController(client ContainerClient) *Controller { + return &Controller{client: client} +} + +func DefaultController(ctx context.Context) (*Controller, error) { + client, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return nil, err + } + + client.NegotiateAPIVersion(ctx) + return NewController(client), nil +} + +// Connect a port on the local machine to a port on a remote docker machine. +func (c *Controller) ConnectRemoteDockerPort(ctx context.Context, port int) error { + err := c.StartRemotePortforwarder(ctx) + if err != nil { + return err + } + return c.StartLocalPortforwarder(ctx, port) +} + +// Create a port-forwarding server on the same machine that's running +// Docker. This server accepts connections and routes them to localhost ports +// on the same machine. +func (c *Controller) StartRemotePortforwarder(ctx context.Context) error { + container, err := c.client.ContainerInspect(ctx, serviceName) + if err == nil && container.State.Running { + // The service is already running! + return nil + } else if err == nil { + // The service exists, but is not running + err := c.client.ContainerRemove(ctx, serviceName, types.ContainerRemoveOptions{Force: true}) + if err != nil { + return fmt.Errorf("creating remote portforwarder: %v", err) + } + } else if !client.IsErrNotFound(err) { + return fmt.Errorf("inspecting remote portforwarder: %v", err) + } + + cmd := exec.Command("docker", "run", "-d", "-it", + "--name", serviceName, "--net=host", "--restart=always", + "--entrypoint", "/bin/sh", "alpine/socat", "-c", "while true; do sleep 1000; done") + return cmd.Run() +} + +// Create a port-forwarding server on the local machine, forwarding connections +// to the same port on the remote Docker server. +func (c *Controller) StartLocalPortforwarder(ctx context.Context, port int) error { + cmd := exec.Command("socat", fmt.Sprintf("TCP-LISTEN:%d,reuseaddr,fork", port), + fmt.Sprintf("EXEC:'docker exec -i %s socat STDIO TCP:localhost:%d'", serviceName, port)) + err := cmd.Start() + if err != nil { + return fmt.Errorf("creating local portforwarder: %v", err) + } + + for i := 0; i < 100; i++ { + conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) + if err == nil { + _ = conn.Close() + return nil + } + time.Sleep(100 * time.Millisecond) + } + return fmt.Errorf("timed out waiting for local portforwarder") +} diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index e62aae9..f1b6dcf 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -20,6 +20,7 @@ func NewRootCommand() *cobra.Command { rootCmd.AddCommand(NewDockerDesktopCommand()) rootCmd.AddCommand(newDocsCommand(rootCmd)) rootCmd.AddCommand(analytics.NewCommand()) + rootCmd.AddCommand(NewSocatCommand()) return rootCmd } diff --git a/pkg/cmd/socat.go b/pkg/cmd/socat.go new file mode 100644 index 0000000..3adffc8 --- /dev/null +++ b/pkg/cmd/socat.go @@ -0,0 +1,49 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "strconv" + + "github.com/spf13/cobra" + "github.com/tilt-dev/ctlptl/internal/socat" +) + +func NewSocatCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "socat", + Short: "Use socat to connect components. Experimental.", + } + + cmd.AddCommand(&cobra.Command{ + Use: "connect-remote-docker", + Short: "Connects a local port to a remote port on a machine running Docker", + Example: " ctlptl socat connect-remote-docker [port]\n", + Run: connectRemoteDocker, + Args: cobra.ExactArgs(1), + }) + + return cmd +} + +func connectRemoteDocker(cmd *cobra.Command, args []string) { + port, err := strconv.Atoi(args[0]) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "connect-remote-docker: %v\n", err) + os.Exit(1) + } + + ctx := context.Background() + c, err := socat.DefaultController(ctx) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "connect-remote-docker: %v\n", err) + os.Exit(1) + } + + err = c.ConnectRemoteDockerPort(ctx, port) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "connect-remote-docker: %v\n", err) + os.Exit(1) + } +}