diff --git a/cmd/mutagen/sync/create.go b/cmd/mutagen/sync/create.go index c00e8f2c..8606bbf7 100644 --- a/cmd/mutagen/sync/create.go +++ b/cmd/mutagen/sync/create.go @@ -589,6 +589,9 @@ var createConfiguration struct { // permission propagation mode, taking priority over defaultGroup on beta if // specified. defaultGroupBeta string + // sudo specifies if the agent should be started with 'sudo'. This can + // be useful when root SSH is disabled. + sudo bool } func init() { diff --git a/pkg/agent/dial.go b/pkg/agent/dial.go index 4c8f3632..78a20779 100644 --- a/pkg/agent/dial.go +++ b/pkg/agent/dial.go @@ -32,7 +32,7 @@ const ( // remote environment is cmd.exe-based and returns hints as to whether or not // installation should be attempted and whether or not the remote environment is // cmd.exe-based. -func connect(logger *logging.Logger, transport Transport, mode, prompter string, cmdExe bool) (io.ReadWriteCloser, bool, bool, error) { +func connect(logger *logging.Logger, transport Transport, mode, prompter string, cmdExe bool, sudo bool) (io.ReadWriteCloser, bool, bool, error) { // Compute the agent invocation command, relative to the user's home // directory on the remote. Unless we have reason to assume that this is a // cmd.exe environment, we construct a path using forward slashes. This will @@ -66,6 +66,9 @@ func connect(logger *logging.Logger, transport Transport, mode, prompter string, // Compute the command to invoke. command := fmt.Sprintf("%s %s", agentInvocationPath, mode) + if sudo && !cmdExe { + command = "sudo " + command + } // Create an agent process. message := "Connecting to agent (POSIX)..." @@ -168,7 +171,7 @@ func connect(logger *logging.Logger, transport Transport, mode, prompter string, // Dial connects to an agent-based endpoint using the specified transport, // connection mode, and prompter. -func Dial(logger *logging.Logger, transport Transport, mode, prompter string) (io.ReadWriteCloser, error) { +func Dial(logger *logging.Logger, transport Transport, mode string, sudo bool, prompter string) (io.ReadWriteCloser, error) { // Validate that the mode is sane. if !(mode == ModeSynchronizer || mode == ModeForwarder) { panic("invalid agent dial mode") @@ -177,11 +180,11 @@ func Dial(logger *logging.Logger, transport Transport, mode, prompter string) (i // Attempt a connection. If this fails but we detect a Windows cmd.exe // environment in the process, then re-attempt a connection under the // cmd.exe assumption. - stream, tryInstall, cmdExe, err := connect(logger, transport, mode, prompter, false) + stream, tryInstall, cmdExe, err := connect(logger, transport, mode, prompter, false, sudo) if err == nil { return stream, nil } else if cmdExe { - stream, tryInstall, cmdExe, err = connect(logger, transport, mode, prompter, true) + stream, tryInstall, cmdExe, err = connect(logger, transport, mode, prompter, true, sudo) if err == nil { return stream, nil } @@ -199,7 +202,7 @@ func Dial(logger *logging.Logger, transport Transport, mode, prompter string) (i } // Re-attempt connectivity. - stream, _, _, err = connect(logger, transport, mode, prompter, cmdExe) + stream, _, _, err = connect(logger, transport, mode, prompter, cmdExe, sudo) if err != nil { return nil, err } diff --git a/pkg/agent/transport/ssh/transport.go b/pkg/agent/transport/ssh/transport.go index ebe56f29..d82cfa94 100644 --- a/pkg/agent/transport/ssh/transport.go +++ b/pkg/agent/transport/ssh/transport.go @@ -184,7 +184,7 @@ func (t *sshTransport) ClassifyError(processState *os.ProcessState, errorOutput // hypothesis (instead of the cmd.exe hypothesis). if process.IsPOSIXShellInvalidCommand(processState) { return true, false, nil - } else if process.IsPOSIXShellCommandNotFound(processState) { + } else if process.IsPOSIXShellCommandNotFound(processState, errorOutput) { return true, false, nil } else if process.OutputIsWindowsInvalidCommand(errorOutput) { // A Windows invalid command error doesn't necessarily indicate that diff --git a/pkg/configuration/synchronization/configuration.go b/pkg/configuration/synchronization/configuration.go index 84c65e89..5f7f4292 100644 --- a/pkg/configuration/synchronization/configuration.go +++ b/pkg/configuration/synchronization/configuration.go @@ -63,6 +63,9 @@ type Configuration struct { // setting ownership of new files and directories in "portable" // permission propagation mode. DefaultGroup string `yaml:"defaultGroup"` + // Sudo specifies if the agent should be started with 'sudo'. This can + // be useful when root SSH is disabled. + Sudo bool `yaml:"sudo"` } `yaml:"permissions"` } @@ -86,5 +89,6 @@ func (c *Configuration) Configuration() *synchronization.Configuration { DefaultDirectoryMode: uint32(c.Permissions.DefaultDirectoryMode), DefaultOwner: c.Permissions.DefaultOwner, DefaultGroup: c.Permissions.DefaultGroup, + Sudo: c.Permissions.Sudo, } } diff --git a/pkg/forwarding/protocols/docker/protocol.go b/pkg/forwarding/protocols/docker/protocol.go index 41e97a7b..da46b1a9 100644 --- a/pkg/forwarding/protocols/docker/protocol.go +++ b/pkg/forwarding/protocols/docker/protocol.go @@ -65,7 +65,7 @@ func (p *protocolHandler) Connect( // cancellation. go func() { // Perform the dialing operation. - stream, err := agent.Dial(logger, transport, agent.ModeForwarder, prompter) + stream, err := agent.Dial(logger, transport, agent.ModeForwarder, false, prompter) // Transmit the result or, if cancelled, close the stream. select { diff --git a/pkg/forwarding/protocols/ssh/protocol.go b/pkg/forwarding/protocols/ssh/protocol.go index e3fdd7fe..ab193f21 100644 --- a/pkg/forwarding/protocols/ssh/protocol.go +++ b/pkg/forwarding/protocols/ssh/protocol.go @@ -73,7 +73,7 @@ func (p *protocolHandler) Connect( // cancellation. go func() { // Perform the dialing operation. - stream, err := agent.Dial(logger, transport, agent.ModeForwarder, prompter) + stream, err := agent.Dial(logger, transport, agent.ModeForwarder, false, prompter) // Transmit the result or, if cancelled, close the stream. select { diff --git a/pkg/process/exit_code.go b/pkg/process/exit_code.go index 68f62719..2eca7ca5 100644 --- a/pkg/process/exit_code.go +++ b/pkg/process/exit_code.go @@ -2,6 +2,7 @@ package process import ( "os" + "strings" ) const ( @@ -35,6 +36,13 @@ func IsPOSIXShellInvalidCommand(state *os.ProcessState) bool { // IsPOSIXShellCommandNotFound returns whether or not a process state represents // a "command not found" error from a POSIX shell. -func IsPOSIXShellCommandNotFound(state *os.ProcessState) bool { - return state.ExitCode() == posixShellCommandNotFoundExitCode +func IsPOSIXShellCommandNotFound(state *os.ProcessState, errorOutput string) bool { + if state.ExitCode() == posixShellCommandNotFoundExitCode { + return true + } + if state.ExitCode() == 1 && + strings.HasSuffix(strings.TrimSpace(errorOutput), "command not found") { + return true + } + return false } diff --git a/pkg/synchronization/configuration.go b/pkg/synchronization/configuration.go index 5a5a1522..1db8e281 100644 --- a/pkg/synchronization/configuration.go +++ b/pkg/synchronization/configuration.go @@ -161,7 +161,8 @@ func (c *Configuration) Equal(other *Configuration) bool { c.DefaultFileMode == other.DefaultFileMode && c.DefaultDirectoryMode == other.DefaultDirectoryMode && c.DefaultOwner == other.DefaultOwner && - c.DefaultGroup == other.DefaultGroup + c.DefaultGroup == other.DefaultGroup && + c.Sudo == other.Sudo } // MergeConfigurations merges two configurations of differing priorities. Both @@ -278,6 +279,11 @@ func MergeConfigurations(lower, higher *Configuration) *Configuration { result.DefaultGroup = lower.DefaultGroup } + // Merge sudo. + if higher.Sudo || lower.Sudo { + result.Sudo = true + } + // Done. return result } diff --git a/pkg/synchronization/configuration.pb.go b/pkg/synchronization/configuration.pb.go index 17a8b8f5..a27c8d8c 100644 --- a/pkg/synchronization/configuration.pb.go +++ b/pkg/synchronization/configuration.pb.go @@ -80,6 +80,9 @@ type Configuration struct { // ownership of new files and directories in "portable" permission // propagation mode. DefaultGroup string `protobuf:"bytes,66,opt,name=defaultGroup,proto3" json:"defaultGroup,omitempty"` + // Sudo specifies if the agent should be started with 'sudo'. This can + // be useful when root SSH is disabled. + Sudo bool `protobuf:"varint,67,opt,name=sudo,proto3" json:"sudo,omitempty"` } func (x *Configuration) Reset() { @@ -226,6 +229,13 @@ func (x *Configuration) GetDefaultGroup() string { return "" } +func (x *Configuration) GetSudo() bool { + if x != nil { + return x.Sudo + } + return false +} + var File_synchronization_configuration_proto protoreflect.FileDescriptor var file_synchronization_configuration_proto_rawDesc = []byte{ @@ -248,7 +258,7 @@ var file_synchronization_configuration_proto_rawDesc = []byte{ 0x6f, 0x72, 0x65, 0x2f, 0x6d, 0x6f, 0x64, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2d, 0x73, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x73, 0x79, 0x6d, 0x62, 0x6f, 0x6c, 0x69, 0x63, 0x5f, 0x6c, 0x69, - 0x6e, 0x6b, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xbb, 0x06, + 0x6e, 0x6b, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xcf, 0x06, 0x0a, 0x0d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4b, 0x0a, 0x13, 0x73, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x63, @@ -300,11 +310,12 @@ var file_synchronization_configuration_proto_rawDesc = []byte{ 0x6e, 0x65, 0x72, 0x18, 0x41, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x22, 0x0a, 0x0c, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x42, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x64, - 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x33, 0x5a, 0x31, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6d, 0x75, 0x74, 0x61, 0x67, 0x65, - 0x6e, 0x2d, 0x69, 0x6f, 0x2f, 0x6d, 0x75, 0x74, 0x61, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x6b, 0x67, - 0x2f, 0x73, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x73, + 0x75, 0x64, 0x6f, 0x18, 0x43, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x73, 0x75, 0x64, 0x6f, 0x42, + 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6d, 0x75, + 0x74, 0x61, 0x67, 0x65, 0x6e, 0x2d, 0x69, 0x6f, 0x2f, 0x6d, 0x75, 0x74, 0x61, 0x67, 0x65, 0x6e, + 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x79, 0x6e, 0x63, 0x68, 0x72, 0x6f, 0x6e, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/pkg/synchronization/configuration.proto b/pkg/synchronization/configuration.proto index e397767a..4a7f75ee 100644 --- a/pkg/synchronization/configuration.proto +++ b/pkg/synchronization/configuration.proto @@ -116,5 +116,9 @@ message Configuration { // propagation mode. string defaultGroup = 66; - // Fields 67-80 are reserved for future permission configuration parameters. + // Sudo specifies if the agent should be started with 'sudo'. This can + // be useful when root SSH is disabled. + bool sudo = 67; + + // Fields 68-80 are reserved for future permission configuration parameters. } diff --git a/pkg/synchronization/protocols/docker/protocol.go b/pkg/synchronization/protocols/docker/protocol.go index b062db79..3c6eab64 100644 --- a/pkg/synchronization/protocols/docker/protocol.go +++ b/pkg/synchronization/protocols/docker/protocol.go @@ -58,7 +58,7 @@ func (h *protocolHandler) Connect( // cancellation. go func() { // Perform the dialing operation. - stream, err := agent.Dial(logger, transport, agent.ModeSynchronizer, prompter) + stream, err := agent.Dial(logger, transport, agent.ModeSynchronizer, false, prompter) // Transmit the result or, if cancelled, close the stream. select { diff --git a/pkg/synchronization/protocols/ssh/protocol.go b/pkg/synchronization/protocols/ssh/protocol.go index 4ad7c598..5c69bb49 100644 --- a/pkg/synchronization/protocols/ssh/protocol.go +++ b/pkg/synchronization/protocols/ssh/protocol.go @@ -62,11 +62,13 @@ func (h *protocolHandler) Connect( // Create a channel to deliver the dialing result. results := make(chan dialResult) + logger.Debug("configuration.Sudo:", configuration.Sudo) + // Perform dialing in a background Goroutine so that we can monitor for // cancellation. go func() { // Perform the dialing operation. - stream, err := agent.Dial(logger, transport, agent.ModeSynchronizer, prompter) + stream, err := agent.Dial(logger, transport, agent.ModeSynchronizer, configuration.Sudo, prompter) // Transmit the result or, if cancelled, close the stream. select {