Skip to content

guestSocket forwards die with the SSH ControlMaster and are never re-created (silent on vz) #5420

Description

@MartyPine

Description

Unix-socket forwards declared in portForwards (guestSocket rules, e.g. the docker socket in the docker template, or colima's docker.sock) are created exactly once at instance start, as ssh -O forward registrations against the SSH ControlMaster. If the master later dies, every forward it owned dies with it. The hostagent never detects this and never re-creates the forwards, so the host-side socket file is left behind as a stub that refuses every connection until the instance is restarted.

Host sleep is the reliable trigger (the master's TCP connection does not survive it). We also see the same failure on machines that have not slept, which points at idle-flow reaping in the usernet stack or network transitions killing a long-idle, keepalive-less connection — flagged as suspected rather than proven; the sleep case reproduces deterministically below.

On vz the failure is completely silent: the guest agent connection rides vsock (GuestAgentConn), which survives sleep, so the reconnect logic in watchGuestAgentEvents never fires. The hostagent keeps logging routine activity (time sync, guest agent events) with no error while the socket is dead. Inside the VM everything is healthy — when we first hit this in the field, dockerd had 24 h of uptime and 15 healthy containers while the host-side socket had been dead for hours. Users typically "fix" it with a full instance restart, which is how it hides inside vaguer reports.

Steps to reproduce

Any instance with a guestSocket port forward works; we used a docker runtime instance on vz.

  1. Start the instance and verify the socket forward works:
    $ curl --unix-socket ~/.lima/<inst>/sock/docker.sock http://localhost/_ping
    OK
  2. Find the ControlMaster:
    $ ssh -F ~/.lima/<inst>/ssh.config -O check lima-<inst>
    Master running (pid=13185)
  3. Simulate what host sleep does to the master's connection:
    $ kill -9 13185
  4. The socket is now dead and stays dead. We probed every 10 s for 120 s: 13/13 attempts refused, no recovery, and ha.stderr.log shows no error, no reconnect attempt, no re-forward — the hostagent has not noticed.
  5. limactl shell <inst> still works, and (since limactl shell --reconnect fails when the previous ssh master crashed (stale ssh.sock not cleaned up) #4913) revives a fresh master — but the socket forward stays dead even after that. Reviving the master does not re-issue the forward registrations; they must be re-issued explicitly.

Root cause (from reading master @ 0990395)

Proposed fix

Two small host-side changes; no guest changes needed.

1. Keepalives (prevention). The master should notice its connection is gone instead of persisting as a zombie, and regular traffic protects it from idle reaping:

// pkg/sshutil/sshutil.go, SSHOpts()
opts = append(opts,
    fmt.Sprintf("User=%s", username),
    "ControlMaster=auto",
    controlPath,
    "ControlPersist=yes",
    "ServerAliveInterval=15",
    "ServerAliveCountMax=4",
)

2. Reconcile loop in the hostagent (heal). Pseudo-code:

every 10s, while socket forwarding is active:
    if IsControlMasterRunning(instDir):
        continue                      # near-free in the healthy case
    RemoveStaleControlMaster(instDir)
    run a trivial remote command over the normal ssh path
        # re-spawns the master via ControlMaster=auto;
        # `ssh -O` alone cannot create one
    for each rule in portForwards where rule.GuestSocket != "":
        forwardSSH(verbCancel, rule)  # best-effort, releases stale registration
        forwardSSH(verbForward, rule)

We have built and field-tested exactly this (plus the keepalives) on a fork, based on v2.2.0: https://github.com/MartyPine/lima/tree/fix/hostagent-socket-forward-reconcile — including a unit test in the style of the existing ga.sock forward tests. Measured results: the baseline above never recovers; with the patch, detection happens within one 10 s tick and restore takes about a second (kill-to-healed of 3–9 s across repeated runs, 5 s in production use; the reconciler triggers exactly once per kill and never while healthy). Sharing the branch as evidence that the approach works, not as a finished PR — happy for it to be adapted or rewritten however the maintainers prefer.

Longer term, vz could route guestSocket forwards over the gRPC forwarder the way TCP ports already go — pkg/portfwd already has host-side unix listener support — which would remove SSH from the socket path entirely on vz. The reconciler would still matter for qemu and any fallback path, so the two are complementary.

Related issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions