From 1f9852d76cf4fd600a16c03e9210c98f556f9e9b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 4 Aug 2023 14:36:50 +0530 Subject: [PATCH] ssh kitten: Easily forward the remote control socket tot he remote host --- docs/changelog.rst | 2 ++ docs/kittens/ssh.rst | 5 +++ kittens/ssh/main.go | 78 ++++++++++++++++++++++++++++++++++++++++---- kittens/ssh/main.py | 10 ++++++ 4 files changed, 88 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d4e0657c1..6b2ae16fb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -40,6 +40,8 @@ Detailed list of changes - A new :doc:`transfer kitten ` that can be used to transfer files efficiently over the TTY device +- ssh kitten: A new configuration directive `to automatically forward the kitty remote control socket ` + - kitten @ set-user-vars: New remote control command to set user variables on a window (:iss:`6502`) diff --git a/docs/kittens/ssh.rst b/docs/kittens/ssh.rst index f9cc0e912..ae664511b 100644 --- a/docs/kittens/ssh.rst +++ b/docs/kittens/ssh.rst @@ -11,9 +11,14 @@ Truly convenient SSH * Easily :opt:`change terminal colors ` when connecting to remote hosts +* Automatically :opt:`forward the kitty remote control socket ` to configured hosts + .. versionadded:: 0.25.0 Automatic shell integration, file transfer and reuse of connections +.. versionadded:: 0.30.0 + Automatic forwarding of remote control sockets + The ssh kitten allows you to login easily to remote hosts, and automatically setup the environment there to be as comfortable as your local shell. You can specify environment variables to set on the remote host and files to copy there, diff --git a/kittens/ssh/main.go b/kittens/ssh/main.go index 6645d5a91..7fed0deed 100644 --- a/kittens/ssh/main.go +++ b/kittens/ssh/main.go @@ -177,6 +177,7 @@ type connection_data struct { echo_on bool request_data bool literal_env map[string]string + listen_on string test_script string dont_create_shm bool @@ -246,6 +247,9 @@ func serialize_env(cd *connection_data, get_local_env func(string) (string, bool add_env("KITTY_REMOTE", cd.host_opts.Remote_kitty.String()) } add_env("KITTY_PUBLIC_KEY", os.Getenv("KITTY_PUBLIC_KEY")) + if cd.listen_on != "" { + add_env("KITTY_LISTEN_ON", cd.listen_on) + } return final_env_instructions(cd.script_type == "py", get_local_env, env...), ksi } @@ -622,28 +626,88 @@ func run_ssh(ssh_args, server_args, found_extra_args []string) (rc int, err erro } return 1, unix.Exec(utils.FindExe(delegate_cmd[0]), utils.Concat(delegate_cmd, ssh_args, server_args), os.Environ()) } + master_is_alive, master_checked := false, false + var control_master_args []string if host_opts.Share_connections { kpid, err := strconv.Atoi(os.Getenv("KITTY_PID")) if err != nil { return 1, fmt.Errorf("Invalid KITTY_PID env var not an integer: %#v", os.Getenv("KITTY_PID")) } - cpargs, err := connection_sharing_args(kpid) + control_master_args, err = connection_sharing_args(kpid) if err != nil { return 1, err } - cmd = slices.Insert(cmd, insertion_point, cpargs...) + cmd = slices.Insert(cmd, insertion_point, control_master_args...) } use_kitty_askpass := host_opts.Askpass == Askpass_native || (host_opts.Askpass == Askpass_unless_set && os.Getenv("SSH_ASKPASS") == "") need_to_request_data := true if use_kitty_askpass { need_to_request_data = set_askpass() } - if need_to_request_data && host_opts.Share_connections { - check_cmd := slices.Insert(cmd, 1, "-O", "check") - err = exec.Command(check_cmd[0], check_cmd[1:]...).Run() - if err == nil { - need_to_request_data = false + master_is_functional := func() bool { + if master_checked { + return master_is_alive } + master_checked = true + check_cmd := slices.Insert(cmd, 1, "-O", "check") + master_is_alive = exec.Command(check_cmd[0], check_cmd[1:]...).Run() == nil + return master_is_alive + } + + if need_to_request_data && host_opts.Share_connections && master_is_functional() { + need_to_request_data = false + } + run_control_master := func() error { + cmcmd := slices.Clone(cmd[:insertion_point]) + cmcmd = append(cmcmd, control_master_args...) + cmcmd = append(cmcmd, "-N", "-f") + cmcmd = append(cmcmd, "--", hostname) + c := exec.Command(cmcmd[0], cmcmd[1:]...) + c.Stdin, c.Stdout, c.Stderr = os.Stdin, os.Stdout, os.Stderr + err := c.Run() + if err != nil { + err = fmt.Errorf("Failed to start SSH ControlMaster with cmdline: %s and error: %w", strings.Join(cmcmd, " "), err) + } + master_checked = false + master_is_alive = false + return err + } + if host_opts.Forward_remote_control && os.Getenv("KITTY_LISTEN_ON") != "" { + if !host_opts.Share_connections { + return 1, fmt.Errorf("Cannot use forward_remote_control=yes without share_connections=yes as it relies on SSH Controlmasters") + } + if !master_is_functional() { + if err = run_control_master(); err != nil { + return 1, err + } + if !master_is_functional() { + return 1, fmt.Errorf("SSH ControlMaster not functional after being started explicitly") + } + } + protocol, listen_on, found := strings.Cut(os.Getenv("KITTY_LISTEN_ON"), ":") + if !found { + return 1, fmt.Errorf("Invalid KITTY_LISTEN_ON: %#v", os.Getenv("KITTY_LISTEN_ON")) + } + if protocol == "unix" && strings.HasPrefix(listen_on, "@") { + return 1, fmt.Errorf("Cannot forward kitty remote control socket when an abstract UNIX socket (%s) is used, due to limitations in OpenSSH. Use either a path based one or a TCP socket", listen_on) + } + cmcmd := slices.Clone(cmd[:insertion_point]) + cmcmd = append(cmcmd, control_master_args...) + cmcmd = append(cmcmd, "-R", "0:"+listen_on, "-O", "forward") + cmcmd = append(cmcmd, "--", hostname) + c := exec.Command(cmcmd[0], cmcmd[1:]...) + b := bytes.Buffer{} + c.Stdout = &b + c.Stderr = os.Stderr + if err := c.Run(); err != nil { + return 1, fmt.Errorf("%s\nSetup of port forward in SSH ControlMaster failed with error: %w", string(b.Bytes()), err) + } + port, err := strconv.Atoi(strings.TrimSpace(string(b.Bytes()))) + if err != nil { + os.Stderr.Write(b.Bytes()) + return 1, fmt.Errorf("Setup of port forward in SSH ControlMaster failed with error: invalid resolved port returned: %s", b.String()) + } + cd.listen_on = "tcp:localhost:" + strconv.Itoa(port) } term, err := tty.OpenControllingTerm(tty.SetNoEcho) if err != nil { diff --git a/kittens/ssh/main.py b/kittens/ssh/main.py index a6b17039d..68937d842 100644 --- a/kittens/ssh/main.py +++ b/kittens/ssh/main.py @@ -208,6 +208,16 @@ For example using :code:`delegate ssh` will run the ssh command with all argumen to the kitten, except kitten specific ones. This is useful if some hosts are not capable of supporting the ssh kitten. ''') + +opt('forward_remote_control', 'no', option_type='to_bool', long_text=''' +Forward the kitty remote control socket to the remote host. This allows using the kitty +remote control facilities from the remote host. WARNING: This allows any software +on the remote host full access to the local computer, so only do it for trusted remote hosts. +Note that this does not work with abstract UNIX sockets such as :file:`@mykitty` because of SSH limitations. +This option uses SSH socket forwarding to forward the socket pointed to by the :envvar:`KITTY_LISTEN_ON` +environment variable. +''') + egr() # }}}