From b4cf40f259d36e7ec7292a9c62383107d191736d Mon Sep 17 00:00:00 2001 From: Bryan Kneis Date: Thu, 12 Dec 2024 15:52:54 +0000 Subject: [PATCH 01/15] Add --reuse-sock flag so browser IDEs can reuse another SSH connections SSH_AUTH_SOCK --- cmd/helper/ssh_server.go | 5 +- cmd/ssh.go | 10 ++++ cmd/up.go | 63 ++++++++++++++++++++++++++ pkg/devcontainer/setup.go | 9 ++++ pkg/ssh/server/ssh.go | 24 ++++++++-- vendor/github.com/loft-sh/ssh/agent.go | 17 ++++--- 6 files changed, 117 insertions(+), 11 deletions(-) diff --git a/cmd/helper/ssh_server.go b/cmd/helper/ssh_server.go index e32cf6841..e7954dcbb 100644 --- a/cmd/helper/ssh_server.go +++ b/cmd/helper/ssh_server.go @@ -26,6 +26,7 @@ type SSHServerCmd struct { Address string Stdio bool TrackActivity bool + ReuseAuthSock bool Workdir string } @@ -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().BoolVar(&cmd.ReuseAuthSock, "reuse-sock", false, "If true a SSH_AUTH_SOCK is expected to already be available in the workspace and the connection reuses this instead of creating another") + _ = sshCmd.Flags().MarkHidden("reuse-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.ReuseAuthSock, log.Default.ErrorStreamOnly()) if err != nil { return err } diff --git a/cmd/ssh.go b/cmd/ssh.go index 280642181..84045075e 100644 --- a/cmd/ssh.go +++ b/cmd/ssh.go @@ -52,6 +52,7 @@ type SSHCmd struct { Stdio bool JumpContainer bool + ReuseAuthSock bool 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().BoolVar(&cmd.ReuseAuthSock, "reuse-sock", false, "If true a SSH_AUTH_SOCK is expected to already be available in the workspace and the connection reuses this instead of creating another") + _ = sshCmd.Flags().MarkHidden("reuse-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") @@ -147,6 +150,10 @@ func (cmd *SSHCmd) Run( cmd.Context = devPodConfig.DefaultContext } + if cmd.ReuseAuthSock { + log.Info("Reusing SSH_AUTH_SOCK") + } + // check if regular workspace client workspaceClient, ok := client.(client2.WorkspaceClient) if ok { @@ -430,6 +437,9 @@ 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.ReuseAuthSock { + command += " --reuse-sock=true" + } if cmd.Debug { command += " --debug" } diff --git a/cmd/up.go b/cmd/up.go index a4d1f1c94..eaad84bf5 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -848,6 +848,59 @@ func parseAddressAndPort(bindAddressOption string, defaultPort int) (string, int return address, portName, nil } +// setupBackhaul sets up a long running command in the container to keep the SSH agent alive +func setupBackhaul( + client client2.BaseWorkspaceClient, + 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", + "--reuse-sock=true", + "--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, @@ -858,6 +911,13 @@ func startBrowserTunnel( gitUsername, gitToken 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 + go func() { + if err := setupBackhaul(client, 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 +926,7 @@ func startBrowserTunnel( cmd, err := createSSHCommand(ctx, client, logger, []string{ "--log-output=raw", + "--reuse-sock=true", "--stdio", }) if err != nil { @@ -994,6 +1055,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/pkg/devcontainer/setup.go b/pkg/devcontainer/setup.go index ee6b40d9c..7865b41f4 100644 --- a/pkg/devcontainer/setup.go +++ b/pkg/devcontainer/setup.go @@ -97,6 +97,9 @@ func (r *runner) setupContainer( // ssh tunnel sshTunnelCmd := fmt.Sprintf("'%s' helper ssh-server --stdio", agent.ContainerDevPodHelperLocation) + if reusesAuthSock(r.WorkspaceConfig.Workspace.IDE.Name) { + sshTunnelCmd += " --reuse-sock=true" + } if r.Log.GetLevel() == logrus.DebugLevel { sshTunnelCmd += " --debug" } @@ -162,3 +165,9 @@ func filterWorkspaceMounts(mounts []*config.Mount, baseFolder string, log log.Lo return retMounts } + +// reusesAuthSock determines if the --reuse-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 == "zed" || ide == "jupyternotebook" || ide == "jlab" +} diff --git a/pkg/ssh/server/ssh.go b/pkg/ssh/server/ssh.go index 2bbe32794..5d70a893c 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 bool, 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 bool 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(), "shared-auth-agent") + 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/vendor/github.com/loft-sh/ssh/agent.go b/vendor/github.com/loft-sh/ssh/agent.go index cda61ef45..f869090d7 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" @@ -35,16 +35,19 @@ func AgentRequested(sess Session) bool { // 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 +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 From 76d2fb08f444d2c1125ca22e512ed626de778709 Mon Sep 17 00:00:00 2001 From: Bryan Kneis Date: Thu, 12 Dec 2024 15:58:30 +0000 Subject: [PATCH 02/15] Fix comment --- cmd/ssh.go | 5 +---- cmd/up.go | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/cmd/ssh.go b/cmd/ssh.go index 84045075e..508577935 100644 --- a/cmd/ssh.go +++ b/cmd/ssh.go @@ -150,10 +150,6 @@ func (cmd *SSHCmd) Run( cmd.Context = devPodConfig.DefaultContext } - if cmd.ReuseAuthSock { - log.Info("Reusing SSH_AUTH_SOCK") - } - // check if regular workspace client workspaceClient, ok := client.(client2.WorkspaceClient) if ok { @@ -438,6 +434,7 @@ 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.ReuseAuthSock { + log.Info("Reusing SSH_AUTH_SOCK") command += " --reuse-sock=true" } if cmd.Debug { diff --git a/cmd/up.go b/cmd/up.go index eaad84bf5..1f47a3549 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -848,7 +848,7 @@ func parseAddressAndPort(bindAddressOption string, defaultPort int) (string, int return address, portName, nil } -// setupBackhaul sets up a long running command in the container to keep the SSH agent alive +// setupBackhaul sets up a long running command in the container to ensure an SSH connection is kept alive func setupBackhaul( client client2.BaseWorkspaceClient, log log.Logger, From aca4f4682f2002ca484c72d5133f090a333acd2d Mon Sep 17 00:00:00 2001 From: Bryan Kneis Date: Fri, 13 Dec 2024 11:23:27 +0000 Subject: [PATCH 03/15] Fix lint error --- cmd/up.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cmd/up.go b/cmd/up.go index 1f47a3549..c256109c8 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -849,10 +849,7 @@ func parseAddressAndPort(bindAddressOption string, defaultPort int) (string, int } // setupBackhaul sets up a long running command in the container to ensure an SSH connection is kept alive -func setupBackhaul( - client client2.BaseWorkspaceClient, - log log.Logger, -) error { +func setupBackhaul(client client2.BaseWorkspaceClient, log log.Logger) error { execPath, err := os.Executable() if err != nil { From 39fe247512a94dcde09b3fdbb48805e5f6e4986a Mon Sep 17 00:00:00 2001 From: Bryan Kneis Date: Fri, 13 Dec 2024 11:26:59 +0000 Subject: [PATCH 04/15] Fix lint error --- cmd/up.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/up.go b/cmd/up.go index c256109c8..7788a127c 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -850,7 +850,6 @@ func parseAddressAndPort(bindAddressOption string, defaultPort int) (string, int // setupBackhaul sets up a long running command in the container to ensure an SSH connection is kept alive func setupBackhaul(client client2.BaseWorkspaceClient, log log.Logger) error { - execPath, err := os.Executable() if err != nil { return err From d7cba57bcbb47e14247c4a9ab1cb305bd8fde80e Mon Sep 17 00:00:00 2001 From: Bryan Kneis Date: Fri, 13 Dec 2024 11:38:16 +0000 Subject: [PATCH 05/15] Update UML --- docs/uml/up_sequence.puml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/uml/up_sequence.puml b/docs/uml/up_sequence.puml index 84d685265..54e7dc93f 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-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 From e9b620d0fbf6c5b24bb3c537916b9c151ca9caff Mon Sep 17 00:00:00 2001 From: Bryan Kneis Date: Mon, 16 Dec 2024 08:36:12 +0000 Subject: [PATCH 06/15] Use random string to identify auth sock --- cmd/agent/container_tunnel.go | 2 +- cmd/agent/workspace/up.go | 1 + cmd/helper/ssh_server.go | 4 +-- cmd/ssh.go | 8 ++--- cmd/up.go | 32 +++++++++++++++---- .../clientimplementation/workspace_client.go | 4 +++ pkg/devcontainer/run.go | 1 + pkg/devcontainer/setup.go | 4 +-- pkg/provider/workspace.go | 4 +++ pkg/ssh/server/ssh.go | 8 ++--- 10 files changed, 49 insertions(+), 19 deletions(-) diff --git a/cmd/agent/container_tunnel.go b/cmd/agent/container_tunnel.go index d47eca53f..c58a71166 100644 --- a/cmd/agent/container_tunnel.go +++ b/cmd/agent/container_tunnel.go @@ -135,7 +135,7 @@ func startDevContainer(ctx context.Context, workspaceConfig *provider2.AgentWork func StartContainer(ctx context.Context, runner devcontainer.Runner, log log.Logger, workspaceConfig *provider2.AgentWorkspaceInfo) (*config.Result, error) { log.Debugf("Starting DevPod container...") - result, err := runner.Up(ctx, devcontainer.UpOptions{NoBuild: true}, workspaceConfig.InjectTimeout) + result, err := runner.Up(ctx, devcontainer.UpOptions{NoBuild: true, AuthSockID: workspaceConfig.AuthSockID}, workspaceConfig.InjectTimeout) if err != nil { return result, err } diff --git a/cmd/agent/workspace/up.go b/cmd/agent/workspace/up.go index 9dab9e938..7b95acfe3 100644 --- a/cmd/agent/workspace/up.go +++ b/cmd/agent/workspace/up.go @@ -128,6 +128,7 @@ func (cmd *UpCmd) devPodUp(ctx context.Context, workspaceInfo *provider2.AgentWo result, err := runner.Up(ctx, devcontainer.UpOptions{ CLIOptions: workspaceInfo.CLIOptions, RegistryCache: workspaceInfo.RegistryCache, + AuthSockID: workspaceInfo.AuthSockID, }, workspaceInfo.InjectTimeout) if err != nil { return nil, err diff --git a/cmd/helper/ssh_server.go b/cmd/helper/ssh_server.go index e7954dcbb..9f72e0f8f 100644 --- a/cmd/helper/ssh_server.go +++ b/cmd/helper/ssh_server.go @@ -26,7 +26,7 @@ type SSHServerCmd struct { Address string Stdio bool TrackActivity bool - ReuseAuthSock bool + ReuseAuthSock string Workdir string } @@ -45,7 +45,7 @@ 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().BoolVar(&cmd.ReuseAuthSock, "reuse-sock", false, "If true a SSH_AUTH_SOCK is expected to already be available in the workspace and the connection reuses this instead of creating another") + sshCmd.Flags().StringVar(&cmd.ReuseAuthSock, "reuse-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-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") diff --git a/cmd/ssh.go b/cmd/ssh.go index 508577935..9023a9a41 100644 --- a/cmd/ssh.go +++ b/cmd/ssh.go @@ -52,7 +52,7 @@ type SSHCmd struct { Stdio bool JumpContainer bool - ReuseAuthSock bool + ReuseAuthSock string AgentForwarding bool GPGAgentForwarding bool GitSSHSignatureForwarding bool @@ -111,7 +111,7 @@ 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().BoolVar(&cmd.ReuseAuthSock, "reuse-sock", false, "If true a SSH_AUTH_SOCK is expected to already be available in the workspace and the connection reuses this instead of creating another") + sshCmd.Flags().StringVar(&cmd.ReuseAuthSock, "reuse-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-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") @@ -433,9 +433,9 @@ 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.ReuseAuthSock { + if cmd.ReuseAuthSock != "" { log.Info("Reusing SSH_AUTH_SOCK") - command += " --reuse-sock=true" + command += fmt.Sprintf(" --reuse-sock=%s", cmd.ReuseAuthSock) } if cmd.Debug { command += " --debug" diff --git a/cmd/up.go b/cmd/up.go index 7788a127c..deeb1c5c5 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "io" + "math/rand" "net" "os" "os/exec" @@ -139,6 +140,16 @@ func NewUpCmd(f *flags.GlobalFlags) *cobra.Command { return upCmd } +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) +} + // Run runs the command logic func (cmd *UpCmd) Run( ctx context.Context, @@ -151,6 +162,10 @@ func (cmd *UpCmd) Run( cmd.Recreate = true } + if cmd.IDE == "openvscode" { + cmd.AuthSockID = RandStringBytes(10) + } + // 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.AuthSockID, log, ) case string(config.IDERustRover): @@ -606,6 +622,7 @@ func startMarimoInBrowser( extraPorts, gitUsername, gitToken, + "", logger, ) } @@ -664,6 +681,7 @@ func startJupyterNotebookInBrowser( extraPorts, gitUsername, gitToken, + "", logger, ) } @@ -719,6 +737,7 @@ func startJupyterDesktop( extraPorts, gitUsername, gitToken, + "", logger, ) } @@ -765,7 +784,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 +832,7 @@ func startVSCodeInBrowser( extraPorts, gitUsername, gitToken, + authSockID, logger, ) } @@ -849,7 +869,7 @@ func parseAddressAndPort(bindAddressOption string, defaultPort int) (string, int } // setupBackhaul sets up a long running command in the container to ensure an SSH connection is kept alive -func setupBackhaul(client client2.BaseWorkspaceClient, log log.Logger) error { +func setupBackhaul(client client2.BaseWorkspaceClient, authSockId string, log log.Logger) error { execPath, err := os.Executable() if err != nil { return err @@ -864,7 +884,7 @@ func setupBackhaul(client client2.BaseWorkspaceClient, log log.Logger) error { execPath, "ssh", "--agent-forwarding=true", - "--reuse-sock=true", + fmt.Sprintf("--reuse-sock=%s", authSockId), "--start-services=false", "--user", remoteUser, @@ -904,13 +924,13 @@ 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 go func() { - if err := setupBackhaul(client, logger); err != nil { + if err := setupBackhaul(client, authSockID, logger); err != nil { logger.Error("Failed to setup backhaul SSH connection: ", err) } }() @@ -922,7 +942,7 @@ func startBrowserTunnel( cmd, err := createSSHCommand(ctx, client, logger, []string{ "--log-output=raw", - "--reuse-sock=true", + fmt.Sprintf("--reuse-sock=%s", authSockID), "--stdio", }) if err != nil { diff --git a/pkg/client/clientimplementation/workspace_client.go b/pkg/client/clientimplementation/workspace_client.go index 695ab1c16..1edb0ba3d 100644 --- a/pkg/client/clientimplementation/workspace_client.go +++ b/pkg/client/clientimplementation/workspace_client.go @@ -206,6 +206,10 @@ func (s *workspaceClient) agentInfo(cliOptions provider.CLIOptions) (string, *pr // Set registry cache from context option agentInfo.RegistryCache = s.devPodConfig.ContextOption(config.ContextOptionRegistryCache) + if cliOptions.AuthSockID != "" { + agentInfo.AuthSockID = cliOptions.AuthSockID + } + // marshal config out, err := json.Marshal(agentInfo) if err != nil { diff --git a/pkg/devcontainer/run.go b/pkg/devcontainer/run.go index 7671c1236..dc8910da3 100644 --- a/pkg/devcontainer/run.go +++ b/pkg/devcontainer/run.go @@ -88,6 +88,7 @@ type UpOptions struct { NoBuild bool ForceBuild bool RegistryCache string + AuthSockID string } func (r *runner) Up(ctx context.Context, options UpOptions, timeout time.Duration) (*config.Result, error) { diff --git a/pkg/devcontainer/setup.go b/pkg/devcontainer/setup.go index 7865b41f4..2c453ca2e 100644 --- a/pkg/devcontainer/setup.go +++ b/pkg/devcontainer/setup.go @@ -98,7 +98,7 @@ func (r *runner) setupContainer( // ssh tunnel sshTunnelCmd := fmt.Sprintf("'%s' helper ssh-server --stdio", agent.ContainerDevPodHelperLocation) if reusesAuthSock(r.WorkspaceConfig.Workspace.IDE.Name) { - sshTunnelCmd += " --reuse-sock=true" + sshTunnelCmd += fmt.Sprintf(" --reuse-sock=%s", r.WorkspaceConfig.AuthSockID) } if r.Log.GetLevel() == logrus.DebugLevel { sshTunnelCmd += " --debug" @@ -169,5 +169,5 @@ func filterWorkspaceMounts(mounts []*config.Mount, baseFolder string, log log.Lo // reusesAuthSock determines if the --reuse-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 == "zed" || ide == "jupyternotebook" || ide == "jlab" + return ide == "openvscode" || ide == "marimo" || ide == "jupyternotebook" || ide == "jlab" } diff --git a/pkg/provider/workspace.go b/pkg/provider/workspace.go index fe681db12..e013b7287 100644 --- a/pkg/provider/workspace.go +++ b/pkg/provider/workspace.go @@ -191,6 +191,9 @@ type AgentWorkspaceInfo struct { // RegistryCache defines the registry to use for caching builds RegistryCache string `json:"registryCache,omitempty"` + + // ID to use when looking for SSH_AUTH_SOCK, defaults to a new random ID if not set (only used for browser IDEs) + AuthSockID string `json:"authSockID,omitempty"` } type CLIOptions struct { @@ -228,6 +231,7 @@ type CLIOptions struct { ForceDockerless bool `json:"forceDockerless,omitempty"` ForceInternalBuildKit bool `json:"forceInternalBuildKit,omitempty"` SSHKey string `json:"sshkey,omitempty"` + AuthSockID string `json:"authSockID,omitempty"` } type BuildOptions struct { diff --git a/pkg/ssh/server/ssh.go b/pkg/ssh/server/ssh.go index 5d70a893c..7b75a53af 100644 --- a/pkg/ssh/server/ssh.go +++ b/pkg/ssh/server/ssh.go @@ -24,7 +24,7 @@ import ( var DefaultPort = 8022 -func NewServer(addr string, hostKey []byte, keys []ssh.PublicKey, workdir string, reuseSock bool, 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 @@ -112,7 +112,7 @@ type Server struct { currentUser string shell []string workdir string - reuseSock bool + reuseSock string sshServer ssh.Server log log.Logger } @@ -132,8 +132,8 @@ func (s *Server) handler(sess ssh.Session) { // 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(), "shared-auth-agent") + if s.reuseSock != "" { + dir = filepath.Join(os.TempDir(), s.reuseSock) err = os.MkdirAll(dir, 0777) if err != nil { s.exitWithError(sess, perrors.Wrap(err, "creating SSH_AUTH_SOCK dir in /tmp")) From d2ae304a2ab0ae91f96442c0d9c10af5b5f7086a Mon Sep 17 00:00:00 2001 From: Bryan Kneis Date: Mon, 16 Dec 2024 08:43:22 +0000 Subject: [PATCH 07/15] Detect default IDE as well as explicit --- cmd/up.go | 9 ++++++++- pkg/devcontainer/setup.go | 9 ++------- pkg/ide/types.go | 6 ++++++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/cmd/up.go b/cmd/up.go index deeb1c5c5..51417e579 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -25,6 +25,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" @@ -162,7 +163,13 @@ func (cmd *UpCmd) Run( cmd.Recreate = true } - if cmd.IDE == "openvscode" { + // 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 ide.ReusesAuthSock(targetIDE) { cmd.AuthSockID = RandStringBytes(10) } diff --git a/pkg/devcontainer/setup.go b/pkg/devcontainer/setup.go index 2c453ca2e..7a595aef7 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,7 +98,7 @@ func (r *runner) setupContainer( // ssh tunnel sshTunnelCmd := fmt.Sprintf("'%s' helper ssh-server --stdio", agent.ContainerDevPodHelperLocation) - if reusesAuthSock(r.WorkspaceConfig.Workspace.IDE.Name) { + if ide.ReusesAuthSock(r.WorkspaceConfig.Workspace.IDE.Name) { sshTunnelCmd += fmt.Sprintf(" --reuse-sock=%s", r.WorkspaceConfig.AuthSockID) } if r.Log.GetLevel() == logrus.DebugLevel { @@ -165,9 +166,3 @@ func filterWorkspaceMounts(mounts []*config.Mount, baseFolder string, log log.Lo return retMounts } - -// reusesAuthSock determines if the --reuse-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/ide/types.go b/pkg/ide/types.go index 9a2fe76ec..960794e1f 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-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" +} From 9e7f788476c0dd90e57b2f8aea2d61b0ed47b6c8 Mon Sep 17 00:00:00 2001 From: Bryan Kneis Date: Mon, 16 Dec 2024 10:13:56 +0000 Subject: [PATCH 08/15] Add some docs --- docs/internal/networking.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/internal/networking.md diff --git a/docs/internal/networking.md b/docs/internal/networking.md new file mode 100644 index 000000000..1167f8c33 --- /dev/null +++ b/docs/internal/networking.md @@ -0,0 +1,23 @@ +# Networking in DevPod + +## SSH Tunnels + +DevPod uses openssh's client to connect to a SSH server running in the devcontainer. Since the SSH server is not addressable on the local network the local ssh-agent is configured using SSH_CONFIG (~/.ssh/config) to use `devpod ssh ...` as the ProxyCommand. This establishes a connection between the local machine and devcontainer (see [cmd/ssh.go](../../cmd/ssh.go) for usage and `~/.ssh/config` for the exact command). This command uses a DevPod provider to establish the "outer tunnel", for kubernetes this is "kubectl exec", docker is "docker exec" etc. This provider command is executed in a shell from the local environment where the STDIO of the outer tunnel (e.g. kubectl exec) is mapped to the shell. The command for this outer tunnel is `devpod helper ssh-server ...`, which spawns an SSH server on the devcontainer using the STDIO of the outer tunnel (mapped to the local machines shell). + +The implementation of our SSH server is provided by DevPod (helper ssh-server), where a fork of [gliderlabs/ssh](https://github.com/gliderlabs/ssh) has been used (see `pkg/ssh` and [cmd/helper/ssh_server](../../cmd/helper/ssh_server.go) for usage). The SSH server can now be thought of as a L7 application layer to provide custom functionality to DevPod, such as tunneling the STDIO from the local machine, port forwarding, agent forwarding etc. + +### SSH Agent Forwarding + +In order for SSH to multiplex it's listening socket, the server uses [channels](https://www.rfc-editor.org/rfc/rfc4254#section-5) to isolate the request types. Each SSH client connected to the server has an encrypted tunnel and this consists of multiple channels. Each channel performs different actions to provide functionality like agent forwarding, tcp/ip forwarding, SFTP etc. When a client connects to the server it can request these channels using a request type. + +In order for DevPod to authenticate a local user with git in a remote devcontainer, it uses agent forwarding to forward the local SSH_AUTH_SOCK to the devcontainer. To do so the client sends a request for channel type "auth-agent@openssh.com", on the server side this gets set in the request's context and the handler can check for it and act according ([see usage](../../pkg/ssh/server/ssh.go)). In our particular case this involves creating a unix socket to bind the forwarded agent to and setting the environments SSH_AUTH_SOCK environment variable to co ordinate with the devcontainer's ssh-agent. + +### Debugging + +If agent forwarding is enabled then the env var `SSH_AUTH_SOCK` should be available in the workspace. Once inside the workspace you should verify the socket exists `ls -la $SSH_AUTH_SOCK`, if it does then you should expect `ssh -T git@github.com` to return 0 error code. Otherwise the agent forwarding is not working (if the local ssh-agent is authenticated with github). + +### Useful reading + - https://www.howtogeek.com/devops/what-is-ssh-agent-forwarding-and-how-do-you-use-it/ + - https://www.rfc-editor.org/rfc/rfc4254 + - https://www.rfc-editor.org/rfc/rfc4251 + - https://datatracker.ietf.org/doc/html/rfc4253 \ No newline at end of file From 5983f55ad40828755e80802a836322e4042075dc Mon Sep 17 00:00:00 2001 From: Bryan Kneis Date: Mon, 16 Dec 2024 10:20:48 +0000 Subject: [PATCH 09/15] Use official ssh dep --- go.mod | 2 +- go.sum | 4 ++-- vendor/github.com/loft-sh/ssh/agent.go | 5 +++-- vendor/modules.txt | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) 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/vendor/github.com/loft-sh/ssh/agent.go b/vendor/github.com/loft-sh/ssh/agent.go index f869090d7..4d2ef811d 100644 --- a/vendor/github.com/loft-sh/ssh/agent.go +++ b/vendor/github.com/loft-sh/ssh/agent.go @@ -33,8 +33,9 @@ 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. +// 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 == "" { 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 From c9fb98a846e4e210d8ae541dfc4b5c0467f1f8a5 Mon Sep 17 00:00:00 2001 From: Bryan Kneis Date: Mon, 16 Dec 2024 15:25:47 +0000 Subject: [PATCH 10/15] Add doc blocks] --- cmd/ssh.go | 2 +- cmd/up.go | 2 +- pkg/tunnel/container.go | 24 +++++++++++++++++------- pkg/tunnel/direct.go | 4 ++++ pkg/tunnel/forwarder.go | 15 ++++++++++----- pkg/tunnel/services.go | 4 +++- 6 files changed, 36 insertions(+), 15 deletions(-) diff --git a/cmd/ssh.go b/cmd/ssh.go index 9023a9a41..ecf8e70b2 100644 --- a/cmd/ssh.go +++ b/cmd/ssh.go @@ -491,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 51417e579..bc8412967 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -971,7 +971,7 @@ func startBrowserTunnel( } // run in container - err := tunnel.RunInContainer( + err := tunnel.RunServices( ctx, devPodConfig, containerClient, diff --git a/pkg/tunnel/container.go b/pkg/tunnel/container.go index addbea65e..87001a22a 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 is a struct that contains the workspace client to use, if proxy is +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, runInContainer Handler, envVars map[string]string) error { // compress info workspaceInfo, _, err := c.client.AgentInfo(provider.CLIOptions{Proxy: c.proxy}) if err != nil { 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{} From 631f7421eb99bc16d780895185aa8a79359bcf99 Mon Sep 17 00:00:00 2001 From: Bryan Kneis Date: Tue, 17 Dec 2024 06:41:07 +0000 Subject: [PATCH 11/15] Pass auth sock id to other IDEs --- cmd/up.go | 15 +++++++++------ pkg/tunnel/container.go | 6 +++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/cmd/up.go b/cmd/up.go index bc8412967..dc1ad4f43 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -333,6 +333,7 @@ func (cmd *UpCmd) Run( ideConfig.Options, cmd.GitUsername, cmd.GitToken, + cmd.AuthSockID, log, ) case string(config.IDEJupyterDesktop): @@ -345,6 +346,7 @@ func (cmd *UpCmd) Run( ideConfig.Options, cmd.GitUsername, cmd.GitToken, + cmd.AuthSockID, log) case string(config.IDEMarimo): return startMarimoInBrowser( @@ -356,6 +358,7 @@ func (cmd *UpCmd) Run( ideConfig.Options, cmd.GitUsername, cmd.GitToken, + cmd.AuthSockID, log) } } @@ -582,7 +585,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 { @@ -629,7 +632,7 @@ func startMarimoInBrowser( extraPorts, gitUsername, gitToken, - "", + authSockID, logger, ) } @@ -641,7 +644,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 { @@ -688,7 +691,7 @@ func startJupyterNotebookInBrowser( extraPorts, gitUsername, gitToken, - "", + authSockID, logger, ) } @@ -700,7 +703,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 { @@ -744,7 +747,7 @@ func startJupyterDesktop( extraPorts, gitUsername, gitToken, - "", + authSockID, logger, ) } diff --git a/pkg/tunnel/container.go b/pkg/tunnel/container.go index 87001a22a..5fd522374 100644 --- a/pkg/tunnel/container.go +++ b/pkg/tunnel/container.go @@ -34,7 +34,7 @@ func NewContainerTunnel(client client.WorkspaceClient, proxy bool, log log.Logge } } -// ContainerTunnel is a struct that contains the workspace client to use, if proxy is +// ContainerTunnel manages the state of the tunnel to the container type ContainerTunnel struct { client client.WorkspaceClient updateConfigInterval time.Duration @@ -184,7 +184,7 @@ func (c *ContainerTunnel) updateConfig(ctx context.Context, sshClient *ssh.Clien } // runInContainer uses the connected SSH client to execute runInContainer on the remote -func (c *ContainerTunnel) runInContainer(ctx context.Context, sshClient *ssh.Client, runInContainer Handler, envVars map[string]string) error { +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 { @@ -237,5 +237,5 @@ func (c *ContainerTunnel) runInContainer(ctx context.Context, sshClient *ssh.Cli c.log.Debugf("Successfully connected to container") // start handler - return runInContainer(cancelCtx, containerClient) + return handler(cancelCtx, containerClient) } From 9e40c61856b3fc1f1be7551264b4c08a8f904047 Mon Sep 17 00:00:00 2001 From: Bryan Kneis Date: Tue, 17 Dec 2024 11:24:07 +0000 Subject: [PATCH 12/15] Fix marimo and jlab --- pkg/ide/jupyter/jupyter.go | 2 +- pkg/ide/marimo/marimo.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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) } From 990cb47851bf000517f9dd6b436e8016972836af Mon Sep 17 00:00:00 2001 From: Bryan Kneis Date: Wed, 18 Dec 2024 10:34:20 +0000 Subject: [PATCH 13/15] Move SSHAuthSockID to CLi options --- cmd/agent/container_tunnel.go | 2 +- cmd/agent/workspace/up.go | 2 +- cmd/up.go | 15 +++++++----- docs/internal/networking.md | 23 ------------------- .../clientimplementation/workspace_client.go | 4 ---- pkg/devcontainer/run.go | 1 - pkg/devcontainer/setup.go | 2 +- pkg/provider/workspace.go | 5 +--- 8 files changed, 13 insertions(+), 41 deletions(-) delete mode 100644 docs/internal/networking.md diff --git a/cmd/agent/container_tunnel.go b/cmd/agent/container_tunnel.go index c58a71166..d47eca53f 100644 --- a/cmd/agent/container_tunnel.go +++ b/cmd/agent/container_tunnel.go @@ -135,7 +135,7 @@ func startDevContainer(ctx context.Context, workspaceConfig *provider2.AgentWork func StartContainer(ctx context.Context, runner devcontainer.Runner, log log.Logger, workspaceConfig *provider2.AgentWorkspaceInfo) (*config.Result, error) { log.Debugf("Starting DevPod container...") - result, err := runner.Up(ctx, devcontainer.UpOptions{NoBuild: true, AuthSockID: workspaceConfig.AuthSockID}, workspaceConfig.InjectTimeout) + result, err := runner.Up(ctx, devcontainer.UpOptions{NoBuild: true}, workspaceConfig.InjectTimeout) if err != nil { return result, err } diff --git a/cmd/agent/workspace/up.go b/cmd/agent/workspace/up.go index 7b95acfe3..4f31b0c37 100644 --- a/cmd/agent/workspace/up.go +++ b/cmd/agent/workspace/up.go @@ -99,6 +99,7 @@ func (cmd *UpCmd) Run(ctx context.Context) error { } func (cmd *UpCmd) up(ctx context.Context, workspaceInfo *provider2.AgentWorkspaceInfo, tunnelClient tunnel.TunnelClient, logger log.Logger) error { + // create devcontainer result, err := cmd.devPodUp(ctx, workspaceInfo, logger) if err != nil { @@ -128,7 +129,6 @@ func (cmd *UpCmd) devPodUp(ctx context.Context, workspaceInfo *provider2.AgentWo result, err := runner.Up(ctx, devcontainer.UpOptions{ CLIOptions: workspaceInfo.CLIOptions, RegistryCache: workspaceInfo.RegistryCache, - AuthSockID: workspaceInfo.AuthSockID, }, workspaceInfo.InjectTimeout) if err != nil { return nil, err diff --git a/cmd/up.go b/cmd/up.go index dc1ad4f43..97c5c6871 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -169,8 +169,11 @@ func (cmd *UpCmd) Run( if cmd.IDE != "" { targetIDE = cmd.IDE } - if ide.ReusesAuthSock(targetIDE) { - cmd.AuthSockID = RandStringBytes(10) + if !cmd.Proxy && ide.ReusesAuthSock(targetIDE) { + cmd.SSHAuthSockID = 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 @@ -296,7 +299,7 @@ func (cmd *UpCmd) Run( ideConfig.Options, cmd.GitUsername, cmd.GitToken, - cmd.AuthSockID, + cmd.SSHAuthSockID, log, ) case string(config.IDERustRover): @@ -333,7 +336,7 @@ func (cmd *UpCmd) Run( ideConfig.Options, cmd.GitUsername, cmd.GitToken, - cmd.AuthSockID, + cmd.SSHAuthSockID, log, ) case string(config.IDEJupyterDesktop): @@ -346,7 +349,7 @@ func (cmd *UpCmd) Run( ideConfig.Options, cmd.GitUsername, cmd.GitToken, - cmd.AuthSockID, + cmd.SSHAuthSockID, log) case string(config.IDEMarimo): return startMarimoInBrowser( @@ -358,7 +361,7 @@ func (cmd *UpCmd) Run( ideConfig.Options, cmd.GitUsername, cmd.GitToken, - cmd.AuthSockID, + cmd.SSHAuthSockID, log) } } diff --git a/docs/internal/networking.md b/docs/internal/networking.md deleted file mode 100644 index 1167f8c33..000000000 --- a/docs/internal/networking.md +++ /dev/null @@ -1,23 +0,0 @@ -# Networking in DevPod - -## SSH Tunnels - -DevPod uses openssh's client to connect to a SSH server running in the devcontainer. Since the SSH server is not addressable on the local network the local ssh-agent is configured using SSH_CONFIG (~/.ssh/config) to use `devpod ssh ...` as the ProxyCommand. This establishes a connection between the local machine and devcontainer (see [cmd/ssh.go](../../cmd/ssh.go) for usage and `~/.ssh/config` for the exact command). This command uses a DevPod provider to establish the "outer tunnel", for kubernetes this is "kubectl exec", docker is "docker exec" etc. This provider command is executed in a shell from the local environment where the STDIO of the outer tunnel (e.g. kubectl exec) is mapped to the shell. The command for this outer tunnel is `devpod helper ssh-server ...`, which spawns an SSH server on the devcontainer using the STDIO of the outer tunnel (mapped to the local machines shell). - -The implementation of our SSH server is provided by DevPod (helper ssh-server), where a fork of [gliderlabs/ssh](https://github.com/gliderlabs/ssh) has been used (see `pkg/ssh` and [cmd/helper/ssh_server](../../cmd/helper/ssh_server.go) for usage). The SSH server can now be thought of as a L7 application layer to provide custom functionality to DevPod, such as tunneling the STDIO from the local machine, port forwarding, agent forwarding etc. - -### SSH Agent Forwarding - -In order for SSH to multiplex it's listening socket, the server uses [channels](https://www.rfc-editor.org/rfc/rfc4254#section-5) to isolate the request types. Each SSH client connected to the server has an encrypted tunnel and this consists of multiple channels. Each channel performs different actions to provide functionality like agent forwarding, tcp/ip forwarding, SFTP etc. When a client connects to the server it can request these channels using a request type. - -In order for DevPod to authenticate a local user with git in a remote devcontainer, it uses agent forwarding to forward the local SSH_AUTH_SOCK to the devcontainer. To do so the client sends a request for channel type "auth-agent@openssh.com", on the server side this gets set in the request's context and the handler can check for it and act according ([see usage](../../pkg/ssh/server/ssh.go)). In our particular case this involves creating a unix socket to bind the forwarded agent to and setting the environments SSH_AUTH_SOCK environment variable to co ordinate with the devcontainer's ssh-agent. - -### Debugging - -If agent forwarding is enabled then the env var `SSH_AUTH_SOCK` should be available in the workspace. Once inside the workspace you should verify the socket exists `ls -la $SSH_AUTH_SOCK`, if it does then you should expect `ssh -T git@github.com` to return 0 error code. Otherwise the agent forwarding is not working (if the local ssh-agent is authenticated with github). - -### Useful reading - - https://www.howtogeek.com/devops/what-is-ssh-agent-forwarding-and-how-do-you-use-it/ - - https://www.rfc-editor.org/rfc/rfc4254 - - https://www.rfc-editor.org/rfc/rfc4251 - - https://datatracker.ietf.org/doc/html/rfc4253 \ No newline at end of file diff --git a/pkg/client/clientimplementation/workspace_client.go b/pkg/client/clientimplementation/workspace_client.go index 1edb0ba3d..695ab1c16 100644 --- a/pkg/client/clientimplementation/workspace_client.go +++ b/pkg/client/clientimplementation/workspace_client.go @@ -206,10 +206,6 @@ func (s *workspaceClient) agentInfo(cliOptions provider.CLIOptions) (string, *pr // Set registry cache from context option agentInfo.RegistryCache = s.devPodConfig.ContextOption(config.ContextOptionRegistryCache) - if cliOptions.AuthSockID != "" { - agentInfo.AuthSockID = cliOptions.AuthSockID - } - // marshal config out, err := json.Marshal(agentInfo) if err != nil { diff --git a/pkg/devcontainer/run.go b/pkg/devcontainer/run.go index dc8910da3..7671c1236 100644 --- a/pkg/devcontainer/run.go +++ b/pkg/devcontainer/run.go @@ -88,7 +88,6 @@ type UpOptions struct { NoBuild bool ForceBuild bool RegistryCache string - AuthSockID string } func (r *runner) Up(ctx context.Context, options UpOptions, timeout time.Duration) (*config.Result, error) { diff --git a/pkg/devcontainer/setup.go b/pkg/devcontainer/setup.go index 7a595aef7..9141535e2 100644 --- a/pkg/devcontainer/setup.go +++ b/pkg/devcontainer/setup.go @@ -99,7 +99,7 @@ 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-sock=%s", r.WorkspaceConfig.AuthSockID) + sshTunnelCmd += fmt.Sprintf(" --reuse-sock=%s", r.WorkspaceConfig.CLIOptions.SSHAuthSockID) } if r.Log.GetLevel() == logrus.DebugLevel { sshTunnelCmd += " --debug" diff --git a/pkg/provider/workspace.go b/pkg/provider/workspace.go index e013b7287..f436be4ad 100644 --- a/pkg/provider/workspace.go +++ b/pkg/provider/workspace.go @@ -191,9 +191,6 @@ type AgentWorkspaceInfo struct { // RegistryCache defines the registry to use for caching builds RegistryCache string `json:"registryCache,omitempty"` - - // ID to use when looking for SSH_AUTH_SOCK, defaults to a new random ID if not set (only used for browser IDEs) - AuthSockID string `json:"authSockID,omitempty"` } type CLIOptions struct { @@ -220,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"` @@ -231,7 +229,6 @@ type CLIOptions struct { ForceDockerless bool `json:"forceDockerless,omitempty"` ForceInternalBuildKit bool `json:"forceInternalBuildKit,omitempty"` SSHKey string `json:"sshkey,omitempty"` - AuthSockID string `json:"authSockID,omitempty"` } type BuildOptions struct { From b3810f12050a02313d7a96a172fe70d09f9a9487 Mon Sep 17 00:00:00 2001 From: Bryan Kneis Date: Wed, 18 Dec 2024 10:41:37 +0000 Subject: [PATCH 14/15] PR tidy up --- cmd/agent/workspace/up.go | 1 - cmd/helper/ssh_server.go | 18 +++++++++--------- cmd/ssh.go | 10 +++++----- cmd/up.go | 18 ++++-------------- docs/uml/up_sequence.puml | 2 +- pkg/devcontainer/setup.go | 2 +- pkg/ide/types.go | 2 +- pkg/ssh/server/ssh.go | 2 +- pkg/util/rand.go | 15 +++++++++++++++ 9 files changed, 37 insertions(+), 33 deletions(-) create mode 100644 pkg/util/rand.go diff --git a/cmd/agent/workspace/up.go b/cmd/agent/workspace/up.go index 4f31b0c37..9dab9e938 100644 --- a/cmd/agent/workspace/up.go +++ b/cmd/agent/workspace/up.go @@ -99,7 +99,6 @@ func (cmd *UpCmd) Run(ctx context.Context) error { } func (cmd *UpCmd) up(ctx context.Context, workspaceInfo *provider2.AgentWorkspaceInfo, tunnelClient tunnel.TunnelClient, logger log.Logger) error { - // create devcontainer result, err := cmd.devPodUp(ctx, workspaceInfo, logger) if err != nil { diff --git a/cmd/helper/ssh_server.go b/cmd/helper/ssh_server.go index 9f72e0f8f..02dd229ca 100644 --- a/cmd/helper/ssh_server.go +++ b/cmd/helper/ssh_server.go @@ -22,12 +22,12 @@ import ( type SSHServerCmd struct { *flags.GlobalFlags - Token string - Address string - Stdio bool - TrackActivity bool - ReuseAuthSock string - Workdir string + Token string + Address string + Stdio bool + TrackActivity bool + ReuseSSHAuthSock string + Workdir string } // NewSSHServerCmd creates a new ssh command @@ -45,8 +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.ReuseAuthSock, "reuse-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-sock") + 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 @@ -92,7 +92,7 @@ func (cmd *SSHServerCmd) Run(_ *cobra.Command, _ []string) error { } // start the server - server, err := helperssh.NewServer(cmd.Address, hostKey, keys, cmd.Workdir, cmd.ReuseAuthSock, 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 ecf8e70b2..308d50cd0 100644 --- a/cmd/ssh.go +++ b/cmd/ssh.go @@ -52,7 +52,7 @@ type SSHCmd struct { Stdio bool JumpContainer bool - ReuseAuthSock string + ReuseSSHAuthSock string AgentForwarding bool GPGAgentForwarding bool GitSSHSignatureForwarding bool @@ -111,8 +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.ReuseAuthSock, "reuse-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-sock") + 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") @@ -433,9 +433,9 @@ 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.ReuseAuthSock != "" { + if cmd.ReuseSSHAuthSock != "" { log.Info("Reusing SSH_AUTH_SOCK") - command += fmt.Sprintf(" --reuse-sock=%s", cmd.ReuseAuthSock) + command += fmt.Sprintf(" --reuse-ssh-auth-sock=%s", cmd.ReuseSSHAuthSock) } if cmd.Debug { command += " --debug" diff --git a/cmd/up.go b/cmd/up.go index 97c5c6871..91a17090d 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "io" - "math/rand" "net" "os" "os/exec" @@ -39,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" @@ -141,16 +141,6 @@ func NewUpCmd(f *flags.GlobalFlags) *cobra.Command { return upCmd } -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) -} - // Run runs the command logic func (cmd *UpCmd) Run( ctx context.Context, @@ -170,7 +160,7 @@ func (cmd *UpCmd) Run( targetIDE = cmd.IDE } if !cmd.Proxy && ide.ReusesAuthSock(targetIDE) { - cmd.SSHAuthSockID = RandStringBytes(10) + 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") @@ -897,7 +887,7 @@ func setupBackhaul(client client2.BaseWorkspaceClient, authSockId string, log lo execPath, "ssh", "--agent-forwarding=true", - fmt.Sprintf("--reuse-sock=%s", authSockId), + fmt.Sprintf("--reuse-ssh-auth-sock=%s", authSockId), "--start-services=false", "--user", remoteUser, @@ -955,7 +945,7 @@ func startBrowserTunnel( cmd, err := createSSHCommand(ctx, client, logger, []string{ "--log-output=raw", - fmt.Sprintf("--reuse-sock=%s", authSockID), + fmt.Sprintf("--reuse-ssh-auth-sock=%s", authSockID), "--stdio", }) if err != nil { diff --git a/docs/uml/up_sequence.puml b/docs/uml/up_sequence.puml index 54e7dc93f..cc2c9ea79 100644 --- a/docs/uml/up_sequence.puml +++ b/docs/uml/up_sequence.puml @@ -64,7 +64,7 @@ Agent --> DevPod: deactivate Agent alt if using browser based IDE (openvscode, marimo, jupyter) -DevPod -> ContainerAgent: devpod ssh --reuse-sock +DevPod -> ContainerAgent: devpod ssh --reuse-ssh-auth-sock end DevPod -> IDE: Start diff --git a/pkg/devcontainer/setup.go b/pkg/devcontainer/setup.go index 9141535e2..c0c713af2 100644 --- a/pkg/devcontainer/setup.go +++ b/pkg/devcontainer/setup.go @@ -99,7 +99,7 @@ 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-sock=%s", r.WorkspaceConfig.CLIOptions.SSHAuthSockID) + 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/types.go b/pkg/ide/types.go index 960794e1f..07a9b67a8 100644 --- a/pkg/ide/types.go +++ b/pkg/ide/types.go @@ -38,7 +38,7 @@ func (o Options) GetValue(values map[string]config.OptionValue, key string) stri return "" } -// ReusesAuthSock determines if the --reuse-sock flag should be passed to the ssh server helper based on the IDE. +// 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/ssh/server/ssh.go b/pkg/ssh/server/ssh.go index 7b75a53af..649c676dc 100644 --- a/pkg/ssh/server/ssh.go +++ b/pkg/ssh/server/ssh.go @@ -133,7 +133,7 @@ func (s *Server) handler(sess ssh.Session) { // 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(), 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")) 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) +} From 2797aca961616495b8a423506886addf71c43559 Mon Sep 17 00:00:00 2001 From: Bryan Kneis Date: Wed, 18 Dec 2024 11:30:50 +0000 Subject: [PATCH 15/15] Wrap backhaul connection in check if reuse sock is set (not in proxy mode) --- cmd/up.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/cmd/up.go b/cmd/up.go index 91a17090d..f2f40370b 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -932,11 +932,14 @@ func startBrowserTunnel( ) 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 - go func() { - if err := setupBackhaul(client, authSockID, logger); err != nil { - logger.Error("Failed to setup backhaul SSH connection: ", err) - } - }() + // 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 {