Repository: hashicorp/vagrant (Ruby)

Issue:
Vagrant keep-alive for SSH has no network activity
### Vagrant version

Vagrant 2.2.19

### Host operating system

* Windows 11

### Guest operating system

* Ubuntu 18.04.3 LTS (`hashicorp/bionic64`)
* Ubuntu 22.04 LTS (`ubuntu/jammy64`)
* Centos 7 (`bento/centos-7`)

### Vagrantfile

```ruby
# -*- mode: ruby -*-

Vagrant.configure("2") do |config|
  config.vm.box = "hashicorp/bionic64"
  config.ssh.keep_alive = true
  config.vm.provision "shell", inline: 'echo "$(date) start_wait"; sleep 60; echo "$(date) done_wait";'
end
```

### Debug output

Collected Vagrant debug log and GuestVM tcpdump: https://gist.github.com/apestov/303eb2053a4293fffe866e681333bdfe

### Expected behavior

Enabled SSH keep-alive will generate continuous network activity to keep connections alive for long-running scripts.

### Actual behavior

There is __no network activity__ between Host and Guest (tcpdump) during command execution despite `DEBUG ssh: Sending SSH keep-alive...` messages are displayed by Vagrant.

* Vagrant debug log has 12 messages `Sending SSH keep-alive` in one minute interval (21:25:29-21:26:29)

    ```
        default: Sun May 29 21:25:29 UTC 2022 start_wait
    [12 times] DEBUG ssh: Sending SSH keep-alive...
        default: Sun May 29 21:26:29 UTC 2022 done_wait
    ```

* The same time GuestVM tcpdump has __no__ network packets during this interval (21:25:29 - 21:26:29)

    ```
    21:25:29.223373 IP 10.0.2.2.50715 > 10.0.2.15.22: Flags [.], ack 8226, win 65535, length 0
    21:26:29.230944 IP 10.0.2.15.22 > 10.0.2.2.50715: Flags [P.], seq 8226:8358, ack 5842, win 41600, length 132
    ```

### Steps to reproduce

1. [Terminal1/Host] Start guest VM: `vagrant up --no-provision`

2. [Terminal2/Guest] Monitor network traffic in the guest VM excluding the monitoring traffic by itself

    ```
    $ sudo netstat -tpn | grep sshd
       tcp        0      0 10.0.2.15:22            10.0.2.2:50259          ESTABLISHED 2163/sshd: vagrant

    $ sudo tcpdump -nn -i any 'tcp and port 22 and port not 50259'
    ```

4. [Terminal1/Host] Provision guest VM: `vagrant provision --debug`

### Notes

If change `plugins/communicators/ssh/communicator.rb` to explicitly pass to `Net::SSH.start()` keepalive params (keepalive=true, keepalive_interval=5)  that generates background traffic (:white_check_mark:).

### References



Discussion:

Patch:
diff --git a/plugins/communicators/ssh/communicator.rb b/plugins/communicators/ssh/communicator.rb
index fd15804c694..3e660710e93 100644
--- a/plugins/communicators/ssh/communicator.rb
+++ b/plugins/communicators/ssh/communicator.rb
@@ -456,6 +456,11 @@ def connect(**opts)
                   connect_opts[:remote_user] = ssh_info[:remote_user]
                 end
 
+                if @machine.config.ssh.keep_alive
+                  connect_opts[:keepalive] = true
+                  connect_opts[:keepalive_interval] = 5
+                end
+
                 @logger.info("Attempting to connect to SSH...")
                 @logger.info("  - Host: #{ssh_info[:host]}")
                 @logger.info("  - Port: #{ssh_info[:port]}")
@@ -683,21 +688,6 @@ def shell_execute(connection, command, **opts)
         end
 
         begin
-          keep_alive = nil
-
-          if machine_config_ssh.keep_alive
-            # Begin sending keep-alive packets while we wait for the script
-            # to complete. This avoids connections closing on long-running
-            # scripts.
-            keep_alive = Thread.new do
-              loop do
-                sleep 5
-                @logger.debug("Sending SSH keep-alive...")
-                connection.send_global_request("keep-alive@openssh.com")
-              end
-            end
-          end
-
           # Wait for the channel to complete
           begin
             channel.wait
@@ -711,9 +701,6 @@ def shell_execute(connection, command, **opts)
           rescue Net::SSH::Disconnect
             raise Vagrant::Errors::SSHDisconnected
           end
-        ensure
-          # Kill the keep-alive thread
-          keep_alive.kill if keep_alive
         end
 
         # If we're in a PTY, we now finally parse the output


Test patch:
diff --git a/test/unit/plugins/communicators/ssh/communicator_test.rb b/test/unit/plugins/communicators/ssh/communicator_test.rb
index a2c929cd4a8..314bceb66d6 100644
--- a/test/unit/plugins/communicators/ssh/communicator_test.rb
+++ b/test/unit/plugins/communicators/ssh/communicator_test.rb
@@ -669,6 +669,32 @@
         ).and_return(true)
         communicator.send(:connect)
       end
+
+      context "keep ssh connection alive" do
+        let(:ssh) do
+          double("ssh",
+            timeout: 1,
+            host: nil,
+            port: 5986,
+            guest_port: 5986,
+            pty: false,
+            keep_alive: true,
+            insert_key: insert_ssh_key,
+            export_command_template: export_command_template,
+            shell: 'bash -l'
+          )
+        end
+
+        it "sets keepalive settings" do
+          expect(Net::SSH).to receive(:start).with(
+            nil, nil, hash_including(
+              keepalive: true,
+              keepalive_interval: 5
+            )
+          ).and_return(true)
+          communicator.send(:connect)
+        end
+      end
     end
 
     context "with keys_only disabled and verify_host_key enabled" do