diff --git a/cmd/helper/ssh_server.go b/cmd/helper/ssh_server.go index e32cf6841..02dd229ca 100644 --- a/cmd/helper/ssh_server.go +++ b/cmd/helper/ssh_server.go @@ -22,11 +22,12 @@ import ( type SSHServerCmd struct { *flags.GlobalFlags - Token string - Address string - Stdio bool - TrackActivity bool - Workdir string + Token string + Address string + Stdio bool + TrackActivity bool + ReuseSSHAuthSock string + Workdir string } // NewSSHServerCmd creates a new ssh command @@ -44,6 +45,8 @@ func NewSSHServerCmd(flags *flags.GlobalFlags) *cobra.Command { sshCmd.Flags().StringVar(&cmd.Address, "address", fmt.Sprintf("0.0.0.0:%d", helperssh.DefaultPort), "Address to listen to") sshCmd.Flags().BoolVar(&cmd.Stdio, "stdio", false, "Will listen on stdout and stdin instead of an address") sshCmd.Flags().BoolVar(&cmd.TrackActivity, "track-activity", false, "If enabled will write the last activity time to a file") + sshCmd.Flags().StringVar(&cmd.ReuseSSHAuthSock, "reuse-ssh-auth-sock", "", "If set, the SSH_AUTH_SOCK is expected to already be available in the workspace (under /tmp using the key provided) and the connection reuses this instead of creating a new one") + _ = sshCmd.Flags().MarkHidden("reuse-ssh-auth-sock") sshCmd.Flags().StringVar(&cmd.Token, "token", "", "Base64 encoded token to use") sshCmd.Flags().StringVar(&cmd.Workdir, "workdir", "", "Directory where commands will run on the host") return sshCmd @@ -89,7 +92,7 @@ func (cmd *SSHServerCmd) Run(_ *cobra.Command, _ []string) error { } // start the server - server, err := helperssh.NewServer(cmd.Address, hostKey, keys, cmd.Workdir, log.Default.ErrorStreamOnly()) + server, err := helperssh.NewServer(cmd.Address, hostKey, keys, cmd.Workdir, cmd.ReuseSSHAuthSock, log.Default.ErrorStreamOnly()) if err != nil { return err } diff --git a/cmd/ssh.go b/cmd/ssh.go index 280642181..308d50cd0 100644 --- a/cmd/ssh.go +++ b/cmd/ssh.go @@ -52,6 +52,7 @@ type SSHCmd struct { Stdio bool JumpContainer bool + ReuseSSHAuthSock string AgentForwarding bool GPGAgentForwarding bool GitSSHSignatureForwarding bool @@ -110,6 +111,8 @@ func NewSSHCmd(f *flags.GlobalFlags) *cobra.Command { sshCmd.Flags().StringVar(&cmd.WorkDir, "workdir", "", "The working directory in the container") sshCmd.Flags().BoolVar(&cmd.Proxy, "proxy", false, "If true will act as intermediate proxy for a proxy provider") sshCmd.Flags().BoolVar(&cmd.AgentForwarding, "agent-forwarding", true, "If true forward the local ssh keys to the remote machine") + sshCmd.Flags().StringVar(&cmd.ReuseSSHAuthSock, "reuse-ssh-auth-sock", "", "If set, the SSH_AUTH_SOCK is expected to already be available in the workspace (under /tmp using the key provided) and the connection reuses this instead of creating a new one") + _ = sshCmd.Flags().MarkHidden("reuse-ssh-auth-sock") sshCmd.Flags().BoolVar(&cmd.GPGAgentForwarding, "gpg-agent-forwarding", false, "If true forward the local gpg-agent to the remote machine") sshCmd.Flags().BoolVar(&cmd.Stdio, "stdio", false, "If true will tunnel connection through stdout and stdin") sshCmd.Flags().BoolVar(&cmd.StartServices, "start-services", true, "If false will not start any port-forwarding or git / docker credentials helper") @@ -430,6 +433,10 @@ func (cmd *SSHCmd) startTunnel(ctx context.Context, devPodConfig *config.Config, log.Debugf("Run outer container tunnel") command := fmt.Sprintf("'%s' helper ssh-server --track-activity --stdio --workdir '%s'", agent.ContainerDevPodHelperLocation, workdir) + if cmd.ReuseSSHAuthSock != "" { + log.Info("Reusing SSH_AUTH_SOCK") + command += fmt.Sprintf(" --reuse-ssh-auth-sock=%s", cmd.ReuseSSHAuthSock) + } if cmd.Debug { command += " --debug" } @@ -484,7 +491,7 @@ func (cmd *SSHCmd) startServices( log log.Logger, ) { if cmd.User != "" { - err := tunnel.RunInContainer( + err := tunnel.RunServices( ctx, devPodConfig, containerClient, diff --git a/cmd/up.go b/cmd/up.go index a4d1f1c94..f2f40370b 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -24,6 +24,7 @@ import ( config2 "github.com/loft-sh/devpod/pkg/devcontainer/config" "github.com/loft-sh/devpod/pkg/devcontainer/sshtunnel" dpFlags "github.com/loft-sh/devpod/pkg/flags" + "github.com/loft-sh/devpod/pkg/ide" "github.com/loft-sh/devpod/pkg/ide/fleet" "github.com/loft-sh/devpod/pkg/ide/jetbrains" "github.com/loft-sh/devpod/pkg/ide/jupyter" @@ -37,6 +38,7 @@ import ( provider2 "github.com/loft-sh/devpod/pkg/provider" devssh "github.com/loft-sh/devpod/pkg/ssh" "github.com/loft-sh/devpod/pkg/tunnel" + "github.com/loft-sh/devpod/pkg/util" "github.com/loft-sh/devpod/pkg/version" workspace2 "github.com/loft-sh/devpod/pkg/workspace" "github.com/loft-sh/log" @@ -151,6 +153,19 @@ func (cmd *UpCmd) Run( cmd.Recreate = true } + // check if we are a browser IDE and need to reuse the SSH_AUTH_SOCK + targetIDE := client.WorkspaceConfig().IDE.Name + // Check override + if cmd.IDE != "" { + targetIDE = cmd.IDE + } + if !cmd.Proxy && ide.ReusesAuthSock(targetIDE) { + cmd.SSHAuthSockID = util.RandStringBytes(10) + log.Debug("Reusing SSH_AUTH_SOCK", cmd.SSHAuthSockID) + } else if cmd.Proxy && ide.ReusesAuthSock(targetIDE) { + log.Info("Reusing SSH_AUTH_SOCK is not supported with proxy mode, consider launching the IDE from the platform UI") + } + // run devpod agent up result, err := cmd.devPodUp(ctx, devPodConfig, client, log) if err != nil { @@ -274,6 +289,7 @@ func (cmd *UpCmd) Run( ideConfig.Options, cmd.GitUsername, cmd.GitToken, + cmd.SSHAuthSockID, log, ) case string(config.IDERustRover): @@ -310,6 +326,7 @@ func (cmd *UpCmd) Run( ideConfig.Options, cmd.GitUsername, cmd.GitToken, + cmd.SSHAuthSockID, log, ) case string(config.IDEJupyterDesktop): @@ -322,6 +339,7 @@ func (cmd *UpCmd) Run( ideConfig.Options, cmd.GitUsername, cmd.GitToken, + cmd.SSHAuthSockID, log) case string(config.IDEMarimo): return startMarimoInBrowser( @@ -333,6 +351,7 @@ func (cmd *UpCmd) Run( ideConfig.Options, cmd.GitUsername, cmd.GitToken, + cmd.SSHAuthSockID, log) } } @@ -559,7 +578,7 @@ func startMarimoInBrowser( client client2.BaseWorkspaceClient, user string, ideOptions map[string]config.OptionValue, - gitUsername, gitToken string, + gitUsername, gitToken, authSockID string, logger log.Logger, ) error { if forwardGpg { @@ -606,6 +625,7 @@ func startMarimoInBrowser( extraPorts, gitUsername, gitToken, + authSockID, logger, ) } @@ -617,7 +637,7 @@ func startJupyterNotebookInBrowser( client client2.BaseWorkspaceClient, user string, ideOptions map[string]config.OptionValue, - gitUsername, gitToken string, + gitUsername, gitToken, authSockID string, logger log.Logger, ) error { if forwardGpg { @@ -664,6 +684,7 @@ func startJupyterNotebookInBrowser( extraPorts, gitUsername, gitToken, + authSockID, logger, ) } @@ -675,7 +696,7 @@ func startJupyterDesktop( client client2.BaseWorkspaceClient, user string, ideOptions map[string]config.OptionValue, - gitUsername, gitToken string, + gitUsername, gitToken, authSockID string, logger log.Logger, ) error { if forwardGpg { @@ -719,6 +740,7 @@ func startJupyterDesktop( extraPorts, gitUsername, gitToken, + authSockID, logger, ) } @@ -765,7 +787,7 @@ func startVSCodeInBrowser( client client2.BaseWorkspaceClient, workspaceFolder, user string, ideOptions map[string]config.OptionValue, - gitUsername, gitToken string, + gitUsername, gitToken, authSockID string, logger log.Logger, ) error { if forwardGpg { @@ -813,6 +835,7 @@ func startVSCodeInBrowser( extraPorts, gitUsername, gitToken, + authSockID, logger, ) } @@ -848,6 +871,55 @@ func parseAddressAndPort(bindAddressOption string, defaultPort int) (string, int return address, portName, nil } +// setupBackhaul sets up a long running command in the container to ensure an SSH connection is kept alive +func setupBackhaul(client client2.BaseWorkspaceClient, authSockId string, log log.Logger) error { + execPath, err := os.Executable() + if err != nil { + return err + } + + remoteUser, err := devssh.GetUser(client.WorkspaceConfig().ID, client.WorkspaceConfig().SSHConfigPath) + if err != nil { + remoteUser = "root" + } + + dotCmd := exec.Command( + execPath, + "ssh", + "--agent-forwarding=true", + fmt.Sprintf("--reuse-ssh-auth-sock=%s", authSockId), + "--start-services=false", + "--user", + remoteUser, + "--context", + client.Context(), + client.Workspace(), + "--log-output=raw", + "--command", + "while true; do sleep 6000000; done", // sleep infinity is not available on all systems + ) + + if log.GetLevel() == logrus.DebugLevel { + dotCmd.Args = append(dotCmd.Args, "--debug") + } + + log.Info("Setting up backhaul SSH connection") + + writer := log.Writer(logrus.InfoLevel, false) + + dotCmd.Stdout = writer + dotCmd.Stderr = writer + + err = dotCmd.Run() + if err != nil { + return err + } + + log.Infof("Done setting up backhaul") + + return nil +} + func startBrowserTunnel( ctx context.Context, devPodConfig *config.Config, @@ -855,9 +927,19 @@ func startBrowserTunnel( user, targetURL string, forwardPorts bool, extraPorts []string, - gitUsername, gitToken string, + gitUsername, gitToken, authSockID string, logger log.Logger, ) error { + // Setup a backhaul SSH connection using the remote user so there is an AUTH SOCK to use + // With normal IDEs this would be the SSH connection made by the IDE + // authSockID is not set when in proxy mode since we cannot use the proxies ssh-agent + if authSockID != "" { + go func() { + if err := setupBackhaul(client, authSockID, logger); err != nil { + logger.Error("Failed to setup backhaul SSH connection: ", err) + } + }() + } err := tunnel.NewTunnel( ctx, func(ctx context.Context, stdin io.Reader, stdout io.Writer) error { @@ -866,6 +948,7 @@ func startBrowserTunnel( cmd, err := createSSHCommand(ctx, client, logger, []string{ "--log-output=raw", + fmt.Sprintf("--reuse-ssh-auth-sock=%s", authSockID), "--stdio", }) if err != nil { @@ -887,7 +970,7 @@ func startBrowserTunnel( } // run in container - err := tunnel.RunInContainer( + err := tunnel.RunServices( ctx, devPodConfig, containerClient, @@ -994,6 +1077,8 @@ func createSSHCommand( } args = append(args, extraArgs...) + logger.Debug("Connecting with SSH command ", execPath, args) + return exec.CommandContext(ctx, execPath, args...), nil } diff --git a/docs/uml/up_sequence.puml b/docs/uml/up_sequence.puml index 84d685265..cc2c9ea79 100644 --- a/docs/uml/up_sequence.puml +++ b/docs/uml/up_sequence.puml @@ -63,5 +63,20 @@ deactivate ContainerAgent Agent --> DevPod: deactivate Agent +alt if using browser based IDE (openvscode, marimo, jupyter) +DevPod -> ContainerAgent: devpod ssh --reuse-ssh-auth-sock +end + DevPod -> IDE: Start + +alt if using normal IDE (vscode, intilliJ) +IDE -> ContainerAgent: devpod ssh +ContainerAgent --> IDE: ssh close +end + +alt if using browser based IDE (openvscode, marimo, jupyter) +ContainerAgent -> DevPod: ssh close +end + + @enduml \ No newline at end of file diff --git a/go.mod b/go.mod index bb503780d..3d09a6b39 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/loft-sh/api/v4 v4.0.0-alpha.6.0.20241129074910-a24d4104d586 github.com/loft-sh/log v0.0.0-20240219160058-26d83ffb46ac github.com/loft-sh/programming-language-detection v0.0.5 - github.com/loft-sh/ssh v0.0.4 + github.com/loft-sh/ssh v0.0.5 github.com/mattn/go-isatty v0.0.20 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d github.com/mitchellh/go-homedir v1.1.0 diff --git a/go.sum b/go.sum index 008086b28..76f51f755 100644 --- a/go.sum +++ b/go.sum @@ -360,8 +360,8 @@ github.com/loft-sh/log v0.0.0-20240219160058-26d83ffb46ac h1:Gz/7Lb7WgdgIv+KJz87 github.com/loft-sh/log v0.0.0-20240219160058-26d83ffb46ac/go.mod h1:YImeRjXH34Yf5E79T7UHBQpDZl9fIaaFRgyZ/bkY+UQ= github.com/loft-sh/programming-language-detection v0.0.5 h1:XiWlxtrf4t6Z7SQiob0JMKaCeMHCP3kWhB80wLt+EMY= github.com/loft-sh/programming-language-detection v0.0.5/go.mod h1:QGPQGKr9q1+rQS4OyisS5CPGY1a76SdNaZuk9oy+2cE= -github.com/loft-sh/ssh v0.0.4 h1:Ybopo9SQpkZjMQ1hbnD71ZcN1fwe5n3dS1qiFfJRIAA= -github.com/loft-sh/ssh v0.0.4/go.mod h1:jgAfPSNioyL2wdFesXY5Wi4pYpdNo4u7AzworofHeyU= +github.com/loft-sh/ssh v0.0.5 h1:CmLfBrbekAZmYhpS+urhqmUZW1XU9kUo2bi4lJiUFH8= +github.com/loft-sh/ssh v0.0.5/go.mod h1:jgAfPSNioyL2wdFesXY5Wi4pYpdNo4u7AzworofHeyU= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= diff --git a/pkg/devcontainer/setup.go b/pkg/devcontainer/setup.go index ee6b40d9c..c0c713af2 100644 --- a/pkg/devcontainer/setup.go +++ b/pkg/devcontainer/setup.go @@ -18,6 +18,7 @@ import ( "github.com/loft-sh/devpod/pkg/devcontainer/crane" "github.com/loft-sh/devpod/pkg/devcontainer/sshtunnel" "github.com/loft-sh/devpod/pkg/driver" + "github.com/loft-sh/devpod/pkg/ide" provider2 "github.com/loft-sh/devpod/pkg/provider" "github.com/loft-sh/log" "github.com/pkg/errors" @@ -97,6 +98,9 @@ func (r *runner) setupContainer( // ssh tunnel sshTunnelCmd := fmt.Sprintf("'%s' helper ssh-server --stdio", agent.ContainerDevPodHelperLocation) + if ide.ReusesAuthSock(r.WorkspaceConfig.Workspace.IDE.Name) { + sshTunnelCmd += fmt.Sprintf(" --reuse-ssh-auth-sock=%s", r.WorkspaceConfig.CLIOptions.SSHAuthSockID) + } if r.Log.GetLevel() == logrus.DebugLevel { sshTunnelCmd += " --debug" } diff --git a/pkg/ide/jupyter/jupyter.go b/pkg/ide/jupyter/jupyter.go index e12edf6ef..9a5c594bd 100644 --- a/pkg/ide/jupyter/jupyter.go +++ b/pkg/ide/jupyter/jupyter.go @@ -102,7 +102,7 @@ func (o *JupyterNotbookServer) Start() error { runCommand := fmt.Sprintf("jupyter notebook --ip='*' --NotebookApp.notebook_dir='%s' --NotebookApp.token='' --NotebookApp.password='' --no-browser --port '%s' --allow-root", o.workspaceFolder, strconv.Itoa(DefaultServerPort)) args := []string{} if o.userName != "" { - args = append(args, "su", o.userName, "-l", "-c", runCommand) + args = append(args, "su", o.userName, "-w", "SSH_AUTH_SOCK", "-l", "-c", runCommand) } else { args = append(args, "sh", "-l", "-c", runCommand) } diff --git a/pkg/ide/marimo/marimo.go b/pkg/ide/marimo/marimo.go index 4d68020a3..cf3a7f03b 100644 --- a/pkg/ide/marimo/marimo.go +++ b/pkg/ide/marimo/marimo.go @@ -98,7 +98,7 @@ func (s *Server) start() error { runCommand := fmt.Sprintf("marimo edit --headless --host 0.0.0.0 --port %s --token-password %s", strconv.Itoa(DefaultServerPort), token) args := []string{} if s.userName != "" { - args = append(args, "su", s.userName, "-l", "-c", runCommand) + args = append(args, "su", s.userName, "-w", "SSH_AUTH_SOCK", "-l", "-c", runCommand) } else { args = append(args, "sh", "-l", "-c", runCommand) } diff --git a/pkg/ide/types.go b/pkg/ide/types.go index 9a2fe76ec..07a9b67a8 100644 --- a/pkg/ide/types.go +++ b/pkg/ide/types.go @@ -37,3 +37,9 @@ func (o Options) GetValue(values map[string]config.OptionValue, key string) stri return "" } + +// ReusesAuthSock determines if the --reuse-ssh-auth-sock flag should be passed to the ssh server helper based on the IDE. +// Browser based IDEs use a browser tunnel to communicate with the remote server instead of an independent ssh connection +func ReusesAuthSock(ide string) bool { + return ide == "openvscode" || ide == "marimo" || ide == "jupyternotebook" || ide == "jlab" +} diff --git a/pkg/provider/workspace.go b/pkg/provider/workspace.go index fe681db12..f436be4ad 100644 --- a/pkg/provider/workspace.go +++ b/pkg/provider/workspace.go @@ -217,6 +217,7 @@ type CLIOptions struct { GitCloneStrategy git.CloneStrategy `json:"gitCloneStrategy,omitempty"` FallbackImage string `json:"fallbackImage,omitempty"` GitSSHSigningKey string `json:"gitSshSigningKey,omitempty"` + SSHAuthSockID string `json:"sshAuthSockID,omitempty"` // ID to use when looking for SSH_AUTH_SOCK, defaults to a new random ID if not set (only used for browser IDEs) // build options Repository string `json:"repository,omitempty"` diff --git a/pkg/ssh/server/ssh.go b/pkg/ssh/server/ssh.go index 2bbe32794..649c676dc 100644 --- a/pkg/ssh/server/ssh.go +++ b/pkg/ssh/server/ssh.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "os/user" + "path/filepath" "strings" "sync" "time" @@ -23,7 +24,7 @@ import ( var DefaultPort = 8022 -func NewServer(addr string, hostKey []byte, keys []ssh.PublicKey, workdir string, log log.Logger) (*Server, error) { +func NewServer(addr string, hostKey []byte, keys []ssh.PublicKey, workdir string, reuseSock string, log log.Logger) (*Server, error) { sh, err := shell.GetShell("") if err != nil { return nil, err @@ -39,6 +40,7 @@ func NewServer(addr string, hostKey []byte, keys []ssh.PublicKey, workdir string server := &Server{ shell: sh, workdir: workdir, + reuseSock: reuseSock, log: log, currentUser: currentUser.Username, sshServer: ssh.Server{ @@ -110,6 +112,7 @@ type Server struct { currentUser string shell []string workdir string + reuseSock string sshServer ssh.Server log log.Logger } @@ -125,14 +128,29 @@ func (s *Server) handler(sess ssh.Session) { s.exitWithError(sess, perrors.Wrap(err, "creating /tmp dir")) return } - l, err := ssh.NewAgentListener() + + // Check if we should create a "shared" socket to be reused by clients + // used for browser tunnels such as openvscode, since the IDE itself doesn't create an SSH connection it uses a "backhaul" connection and uses the existing socket + dir := "" + if s.reuseSock != "" { + dir = filepath.Join(os.TempDir(), fmt.Sprintf("auth-agent-%s", s.reuseSock)) + err = os.MkdirAll(dir, 0777) + if err != nil { + s.exitWithError(sess, perrors.Wrap(err, "creating SSH_AUTH_SOCK dir in /tmp")) + return + } + } + + l, tmpDir, err := ssh.NewAgentListener(dir) if err != nil { s.exitWithError(sess, perrors.Wrap(err, "start agent")) return } - defer l.Close() + defer os.RemoveAll(tmpDir) + go ssh.ForwardAgentConnections(l, sess) + cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", "SSH_AUTH_SOCK", l.Addr().String())) } diff --git a/pkg/tunnel/container.go b/pkg/tunnel/container.go index addbea65e..5fd522374 100644 --- a/pkg/tunnel/container.go +++ b/pkg/tunnel/container.go @@ -1,3 +1,6 @@ +// Package tunnel provides the functions used by the CLI to tunnel into a container using either +// a tunneled connection from the workspace client (using a machine provider) or a direct SSH connection +// from the proxy client (Ssh, k8s or docker provider) package tunnel import ( @@ -19,9 +22,11 @@ import ( "golang.org/x/crypto/ssh" ) -func NewContainerTunnel(client client.WorkspaceClient, proxy bool, log log.Logger) *ContainerHandler { +// NewContainerTunnel constructs a ContainerTunnel using the workspace client, if proxy is True then +// the workspace's agent config is not periodically updated +func NewContainerTunnel(client client.WorkspaceClient, proxy bool, log log.Logger) *ContainerTunnel { updateConfigInterval := time.Second * 30 - return &ContainerHandler{ + return &ContainerTunnel{ client: client, updateConfigInterval: updateConfigInterval, proxy: proxy, @@ -29,16 +34,19 @@ func NewContainerTunnel(client client.WorkspaceClient, proxy bool, log log.Logge } } -type ContainerHandler struct { +// ContainerTunnel manages the state of the tunnel to the container +type ContainerTunnel struct { client client.WorkspaceClient updateConfigInterval time.Duration proxy bool log log.Logger } +// Handler defines what to do once the tunnel has a client established type Handler func(ctx context.Context, containerClient *ssh.Client) error -func (c *ContainerHandler) Run(ctx context.Context, handler Handler, cfg *config.Config, envVars map[string]string) error { +// Run creates an "outer" tunnel to the host to start the SSH server so that the "inner" tunnel can connect to the container over SSH +func (c *ContainerTunnel) Run(ctx context.Context, handler Handler, cfg *config.Config, envVars map[string]string) error { if handler == nil { return nil } @@ -118,7 +126,7 @@ func (c *ContainerHandler) Run(ctx context.Context, handler Handler, cfg *config } // wait until we are done - if err := c.runRunInContainer(cancelCtx, sshClient, handler, envVars); err != nil { + if err := c.runInContainer(cancelCtx, sshClient, handler, envVars); err != nil { containerChan <- fmt.Errorf("run in container: %w", err) } else { containerChan <- nil @@ -134,7 +142,8 @@ func (c *ContainerHandler) Run(ctx context.Context, handler Handler, cfg *config } } -func (c *ContainerHandler) updateConfig(ctx context.Context, sshClient *ssh.Client) { +// updateConfig is called periodically to keep the workspace agent config up to date +func (c *ContainerTunnel) updateConfig(ctx context.Context, sshClient *ssh.Client) { for { select { case <-ctx.Done(): @@ -174,7 +183,8 @@ func (c *ContainerHandler) updateConfig(ctx context.Context, sshClient *ssh.Clie } } -func (c *ContainerHandler) runRunInContainer(ctx context.Context, sshClient *ssh.Client, runInContainer Handler, envVars map[string]string) error { +// runInContainer uses the connected SSH client to execute runInContainer on the remote +func (c *ContainerTunnel) runInContainer(ctx context.Context, sshClient *ssh.Client, handler Handler, envVars map[string]string) error { // compress info workspaceInfo, _, err := c.client.AgentInfo(provider.CLIOptions{Proxy: c.proxy}) if err != nil { @@ -227,5 +237,5 @@ func (c *ContainerHandler) runRunInContainer(ctx context.Context, sshClient *ssh c.log.Debugf("Successfully connected to container") // start handler - return runInContainer(cancelCtx, containerClient) + return handler(cancelCtx, containerClient) } diff --git a/pkg/tunnel/direct.go b/pkg/tunnel/direct.go index 949d21595..3e2ed5fa5 100644 --- a/pkg/tunnel/direct.go +++ b/pkg/tunnel/direct.go @@ -9,8 +9,12 @@ import ( "github.com/pkg/errors" ) +// Tunnel defines the function to create an "outer" tunnel type Tunnel func(ctx context.Context, stdin io.Reader, stdout io.Writer) error +// NewTunnel creates a tunnel to the devcontainer using generic functions to establish the "outer" and "inner" tunnel, used by proxy clients +// Here the tunnel will be an SSH connection with it's STDIO as arguments and the handler will be the function to execute the command +// using the connected SSH client. func NewTunnel(ctx context.Context, tunnel Tunnel, handler Handler) error { // create context cancelCtx, cancel := context.WithCancel(ctx) diff --git a/pkg/tunnel/forwarder.go b/pkg/tunnel/forwarder.go index 064912e68..64f16a204 100644 --- a/pkg/tunnel/forwarder.go +++ b/pkg/tunnel/forwarder.go @@ -10,6 +10,8 @@ import ( "golang.org/x/crypto/ssh" ) +// newForwarder returns a new forwarder using an SSH client and list of ports to forward, +// for each port a new go routine is used to manage the SSH channel func newForwarder(sshClient *ssh.Client, forwardedPorts []string, log log.Logger) netstat.Forwarder { return &forwarder{ sshClient: sshClient, @@ -19,8 +21,9 @@ func newForwarder(sshClient *ssh.Client, forwardedPorts []string, log log.Logger } } +// forwarder multiplexes a SSH client to forward ports to the remote container type forwarder struct { - m sync.Mutex + sync.Mutex sshClient *ssh.Client forwardedPorts []string @@ -29,9 +32,10 @@ type forwarder struct { log log.Logger } +// Forward opens an SSH channel in the existing connection with channel type "direct-tcpip" to forward the local port func (f *forwarder) Forward(port string) error { - f.m.Lock() - defer f.m.Unlock() + f.Lock() + defer f.Unlock() if f.isExcluded(port) || f.portMap[port] != nil { return nil @@ -52,9 +56,10 @@ func (f *forwarder) Forward(port string) error { return nil } +// StopForward stops the port forwarding for the given port func (f *forwarder) StopForward(port string) error { - f.m.Lock() - defer f.m.Unlock() + f.Lock() + defer f.Unlock() if f.isExcluded(port) || f.portMap[port] == nil { return nil diff --git a/pkg/tunnel/services.go b/pkg/tunnel/services.go index 97eabb3be..576ac1854 100644 --- a/pkg/tunnel/services.go +++ b/pkg/tunnel/services.go @@ -29,7 +29,8 @@ import ( "k8s.io/client-go/util/retry" ) -func RunInContainer( +// RunServices forwards the ports for a given workspace and uses it's SSH client to run the credentials server remotely and the services server locally to communicate with the container +func RunServices( ctx context.Context, devPodConfig *config.Config, containerClient *ssh.Client, @@ -144,6 +145,7 @@ func RunInContainer( }) } +// forwardDevContainerPorts forwards all the ports defined in the devcontainer.json func forwardDevContainerPorts(ctx context.Context, containerClient *ssh.Client, extraPorts []string, exitAfterTimeout time.Duration, log log.Logger) ([]string, error) { stdout := &bytes.Buffer{} stderr := &bytes.Buffer{} diff --git a/pkg/util/rand.go b/pkg/util/rand.go new file mode 100644 index 000000000..9e477778a --- /dev/null +++ b/pkg/util/rand.go @@ -0,0 +1,15 @@ +package util + +import ( + "math/rand" +) + +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +func RandStringBytes(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = letterBytes[rand.Intn(len(letterBytes))] + } + return string(b) +} diff --git a/vendor/github.com/loft-sh/ssh/agent.go b/vendor/github.com/loft-sh/ssh/agent.go index cda61ef45..4d2ef811d 100644 --- a/vendor/github.com/loft-sh/ssh/agent.go +++ b/vendor/github.com/loft-sh/ssh/agent.go @@ -2,8 +2,8 @@ package ssh import ( "io" - "io/ioutil" "net" + "os" "path/filepath" "sync" @@ -33,18 +33,22 @@ func AgentRequested(sess Session) bool { return sess.Context().Value(contextKeyAgentRequest) == true } -// NewAgentListener sets up a temporary Unix socket that can be communicated -// to the session environment and used for forwarding connections. -func NewAgentListener() (net.Listener, error) { - dir, err := ioutil.TempDir("", agentTempDir) - if err != nil { - return nil, err +// NewAgentListener sets up a temporary Unix socket, if dir is not specified, that can be communicated +// to the session environment and used for forwarding connections. If dir is specified, it will use the directory +// to create the socket file. +func NewAgentListener(dir string) (net.Listener, string, error) { + var err error + if dir == "" { + dir, err = os.MkdirTemp("", agentTempDir) + if err != nil { + return nil, "", err + } } l, err := net.Listen("unix", filepath.Join(dir, agentListenFile)) if err != nil { - return nil, err + return nil, "", err } - return l, nil + return l, dir, nil } // ForwardAgentConnections takes connections from a listener to proxy into the diff --git a/vendor/modules.txt b/vendor/modules.txt index 724d87da1..0d92c2fea 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -674,7 +674,7 @@ github.com/loft-sh/log/terminal # github.com/loft-sh/programming-language-detection v0.0.5 ## explicit; go 1.20 github.com/loft-sh/programming-language-detection/pkg/detector -# github.com/loft-sh/ssh v0.0.4 +# github.com/loft-sh/ssh v0.0.5 ## explicit; go 1.12 github.com/loft-sh/ssh # github.com/lucasb-eyer/go-colorful v1.2.0