Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add --reuse-sock flag so browser IDEs can reuse another SSH connections SSH_AUTH_SOCK #1471

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
15 changes: 9 additions & 6 deletions cmd/helper/ssh_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
9 changes: 8 additions & 1 deletion cmd/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type SSHCmd struct {

Stdio bool
JumpContainer bool
ReuseSSHAuthSock string
AgentForwarding bool
GPGAgentForwarding bool
GitSSHSignatureForwarding bool
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This will always be printing when we connect, can you make this log level Debug please?

command += fmt.Sprintf(" --reuse-ssh-auth-sock=%s", cmd.ReuseSSHAuthSock)
}
if cmd.Debug {
command += " --debug"
}
Expand Down Expand Up @@ -484,7 +491,7 @@ func (cmd *SSHCmd) startServices(
log log.Logger,
) {
if cmd.User != "" {
err := tunnel.RunInContainer(
err := tunnel.RunServices(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you revert this naming change please? Makes it harder to backmerge and it's not really required in this PR.
If you absolutely want to change it, please open a new one

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I changed this function's name was because it runs a server locally, so the name is misleading IMO

ctx,
devPodConfig,
containerClient,
Expand Down
97 changes: 91 additions & 6 deletions cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can potentially be confusing if you don't know what SSH_AUTH_SOCK is and how it relates to the ssh agent. Could you make this a debug log please

}

// run devpod agent up
result, err := cmd.devPodUp(ctx, devPodConfig, client, log)
if err != nil {
Expand Down Expand Up @@ -274,6 +289,7 @@ func (cmd *UpCmd) Run(
ideConfig.Options,
cmd.GitUsername,
cmd.GitToken,
cmd.SSHAuthSockID,
log,
)
case string(config.IDERustRover):
Expand Down Expand Up @@ -310,6 +326,7 @@ func (cmd *UpCmd) Run(
ideConfig.Options,
cmd.GitUsername,
cmd.GitToken,
cmd.SSHAuthSockID,
log,
)
case string(config.IDEJupyterDesktop):
Expand All @@ -322,6 +339,7 @@ func (cmd *UpCmd) Run(
ideConfig.Options,
cmd.GitUsername,
cmd.GitToken,
cmd.SSHAuthSockID,
log)
case string(config.IDEMarimo):
return startMarimoInBrowser(
Expand All @@ -333,6 +351,7 @@ func (cmd *UpCmd) Run(
ideConfig.Options,
cmd.GitUsername,
cmd.GitToken,
cmd.SSHAuthSockID,
log)
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -606,6 +625,7 @@ func startMarimoInBrowser(
extraPorts,
gitUsername,
gitToken,
authSockID,
logger,
)
}
Expand All @@ -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 {
Expand Down Expand Up @@ -664,6 +684,7 @@ func startJupyterNotebookInBrowser(
extraPorts,
gitUsername,
gitToken,
authSockID,
logger,
)
}
Expand All @@ -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 {
Expand Down Expand Up @@ -719,6 +740,7 @@ func startJupyterDesktop(
extraPorts,
gitUsername,
gitToken,
authSockID,
logger,
)
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -813,6 +835,7 @@ func startVSCodeInBrowser(
extraPorts,
gitUsername,
gitToken,
authSockID,
logger,
)
}
Expand Down Expand Up @@ -848,16 +871,75 @@ 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,
client client2.BaseWorkspaceClient,
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 {
Expand All @@ -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 {
Expand All @@ -887,7 +970,7 @@ func startBrowserTunnel(
}

// run in container
err := tunnel.RunInContainer(
err := tunnel.RunServices(
ctx,
devPodConfig,
containerClient,
Expand Down Expand Up @@ -994,6 +1077,8 @@ func createSSHCommand(
}
args = append(args, extraArgs...)

logger.Debug("Connecting with SSH command ", execPath, args)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it would be a useful debug log so we have the flags the SSH connection made


return exec.CommandContext(ctx, execPath, args...), nil
}

Expand Down
15 changes: 15 additions & 0 deletions docs/uml/up_sequence.puml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
4 changes: 4 additions & 0 deletions pkg/devcontainer/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/ide/jupyter/jupyter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/ide/marimo/marimo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/ide/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Loading
Loading