Skip to content

Commit 0a29768

Browse files
committed
vz: add SSH over AF_VSOCK
Since systemd v256 (Ubuntu 24.10), SSH is bound to AF_VSOCK port 22. https://github.com/systemd/systemd/releases/tag/v256 > - If the system is run in a VM providing AF_VSOCK support, it automatically binds sshd to AF_VSOCK port 22. https://discourse.ubuntu.com/t/oracular-oriole-release-notes/44878 > - When sshd is installed on a system, a new systemd generator, systemd-ssh-generator binds a socket-activated SSH server to local AF_VSOCK and AF_UNIX sockets under certain conditions. This changes to delay starting SSH port forwarding until the SSH server on the VM becomes ready. If AF_VSOCK port 22 can be connected, start a local SSH port as a proxy for AF_VSOCK port 22, instead of starting gvisor's port forwarder. SSH over VSOCK is faster than SSH over gvisor's port forwarder. This change is opt-out because it requires VZ and VM with systemd v256+, setting `LIMA_SSH_OVER_VSOCK=true` does not mean it works. To disable, set `LIMA_SSH_OVER_VSOCK=false`. Signed-off-by: Norio Nomura <[email protected]>
1 parent 85e0c77 commit 0a29768

File tree

6 files changed

+199
-8
lines changed

6 files changed

+199
-8
lines changed

pkg/driver/vz/vm_darwin.go

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,36 @@ func startVM(ctx context.Context, inst *limatype.Instance, sshLocalPort int) (*v
108108
filesToRemove[pidFile] = struct{}{}
109109
logrus.Info("[VZ] - vm state change: running")
110110

111-
err := usernetClient.ConfigureDriver(ctx, inst, sshLocalPort)
112-
if err != nil {
113-
errCh <- err
114-
}
111+
go func() {
112+
sshLocalPort := sshLocalPort
113+
useSSHOverVsock := true
114+
if envVar := os.Getenv("LIMA_SSH_OVER_VSOCK"); envVar != "" {
115+
b, err := strconv.ParseBool(envVar)
116+
if err != nil {
117+
logrus.WithError(err).Warnf("invalid LIMA_SSH_OVER_VSOCK value %q", envVar)
118+
} else {
119+
useSSHOverVsock = b
120+
}
121+
}
122+
if !useSSHOverVsock {
123+
logrus.Info("LIMA_SSH_OVER_VSOCK is false, skipping detection of SSH server on vsock port")
124+
} else if err := usernetClient.WaitOpeningSSHPort(ctx, inst); err == nil {
125+
hostAddress := net.JoinHostPort(inst.SSHAddress, strconv.Itoa(sshLocalPort))
126+
if err := wrapper.startVsockForwarder(ctx, 22, hostAddress); err == nil {
127+
logrus.Infof("Detected SSH server is listening on the vsock port; changed %s to proxy for the vsock port", hostAddress)
128+
sshLocalPort = 0 // disable gvisor ssh port forwarding
129+
} else {
130+
logrus.WithError(err).Warn("Failed to detect SSH server on vsock port, falling back to gvisor's forwarder")
131+
}
132+
} else {
133+
logrus.WithError(err).Warn("Failed to wait for the guest SSH server to become available, falling back to gvisor's forwarder")
134+
}
135+
err := usernetClient.ConfigureDriver(ctx, inst, sshLocalPort)
136+
if err != nil {
137+
errCh <- err
138+
}
139+
}()
140+
115141
case vz.VirtualMachineStateStopped:
116142
logrus.Info("[VZ] - vm state change: stopped")
117143
wrapper.mu.Lock()

pkg/driver/vz/vsock_forwarder.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//go:build darwin && !no_vz
2+
3+
// SPDX-FileCopyrightText: Copyright The Lima Authors
4+
// SPDX-License-Identifier: Apache-2.0
5+
6+
package vz
7+
8+
import (
9+
"context"
10+
"errors"
11+
"net"
12+
13+
"github.com/containers/gvisor-tap-vsock/pkg/tcpproxy"
14+
"github.com/sirupsen/logrus"
15+
)
16+
17+
func (m *virtualMachineWrapper) startVsockForwarder(ctx context.Context, vsockPort uint32, hostAddress string) error {
18+
// Test if the vsock port is open
19+
conn, err := m.dialVsock(ctx, vsockPort)
20+
if err != nil {
21+
return err
22+
}
23+
conn.Close()
24+
// Start listening on localhost:hostPort and forward to vsock:vsockPort
25+
_, _, err = net.SplitHostPort(hostAddress)
26+
if err != nil {
27+
return err
28+
}
29+
var lc net.ListenConfig
30+
l, err := lc.Listen(ctx, "tcp", hostAddress)
31+
if err != nil {
32+
return err
33+
}
34+
go func() {
35+
<-ctx.Done()
36+
l.Close()
37+
}()
38+
logrus.Infof("Started vsock forwarder: %s -> vsock:%d on VM", hostAddress, vsockPort)
39+
go func() {
40+
defer l.Close()
41+
for {
42+
conn, err := l.Accept()
43+
if err != nil {
44+
if errors.Is(err, net.ErrClosed) {
45+
return
46+
}
47+
logrus.WithError(err).Errorf("vsock forwarder accept error: %v", err)
48+
} else {
49+
p := tcpproxy.DialProxy{
50+
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
51+
return m.dialVsock(ctx, vsockPort)
52+
},
53+
}
54+
go p.HandleConn(conn)
55+
}
56+
select {
57+
case <-ctx.Done():
58+
return
59+
default:
60+
continue
61+
}
62+
}
63+
}()
64+
return nil
65+
}
66+
67+
func (m *virtualMachineWrapper) dialVsock(_ context.Context, port uint32) (conn net.Conn, err error) {
68+
for _, socket := range m.SocketDevices() {
69+
conn, err = socket.Connect(port)
70+
if err == nil {
71+
return conn, nil
72+
}
73+
}
74+
return nil, err
75+
}

pkg/networks/usernet/client.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@ func (c *Client) ConfigureDriver(ctx context.Context, inst *limatype.Instance, s
3838
if err != nil {
3939
return err
4040
}
41-
err = c.ResolveAndForwardSSH(ipAddress, sshLocalPort)
42-
if err != nil {
43-
return err
41+
if sshLocalPort != 0 {
42+
err = c.ResolveAndForwardSSH(ipAddress, sshLocalPort)
43+
if err != nil {
44+
return err
45+
}
4446
}
4547
hosts := inst.Config.HostResolver.Hosts
4648
if hosts == nil {
@@ -127,6 +129,27 @@ func (c *Client) Leases(ctx context.Context) (map[string]string, error) {
127129
return leases, nil
128130
}
129131

132+
// WaitOpeningSSHPort Wait until the guest ssh server is available.
133+
func (c *Client) WaitOpeningSSHPort(ctx context.Context, inst *limatype.Instance) error {
134+
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
135+
defer cancel()
136+
macAddress := limayaml.MACAddress(inst.Dir)
137+
ipAddr, err := c.ResolveIPAddress(ctx, macAddress)
138+
if err != nil {
139+
return err
140+
}
141+
u := fmt.Sprintf("%s/extension/wait_port?ip=%s&port=22", c.base, ipAddr)
142+
res, err := httpclientutil.Get(ctx, c.client, u)
143+
if err != nil {
144+
return err
145+
}
146+
defer res.Body.Close()
147+
if res.StatusCode != http.StatusOK {
148+
return errors.New("failed to wait for SSH port")
149+
}
150+
return nil
151+
}
152+
130153
func NewClientByName(nwName string) *Client {
131154
endpointSock, err := Sock(nwName, EndpointSock)
132155
if err != nil {

pkg/networks/usernet/gvproxy.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"net/http"
1313
"os"
1414
"runtime"
15+
"strconv"
1516
"strings"
1617
"time"
1718

@@ -103,7 +104,8 @@ func run(ctx context.Context, g *errgroup.Group, configuration *types.Configurat
103104
if err != nil {
104105
return err
105106
}
106-
httpServe(ctx, g, ln, vn.Mux())
107+
108+
httpServe(ctx, g, ln, muxWithExtension(vn))
107109

108110
if opts.QemuSocket != "" {
109111
err = listenQEMU(ctx, vn)
@@ -239,6 +241,48 @@ func httpServe(ctx context.Context, g *errgroup.Group, ln net.Listener, mux http
239241
})
240242
}
241243

244+
func muxWithExtension(n *virtualnetwork.VirtualNetwork) *http.ServeMux {
245+
m := n.Mux()
246+
m.HandleFunc("/extension/wait_port", func(w http.ResponseWriter, r *http.Request) {
247+
ip := r.URL.Query().Get("ip")
248+
if net.ParseIP(ip) == nil {
249+
msg := fmt.Sprintf("invalid ip address: %s", ip)
250+
http.Error(w, msg, http.StatusBadRequest)
251+
return
252+
}
253+
port16, err := strconv.ParseUint(r.URL.Query().Get("port"), 10, 16)
254+
if err != nil {
255+
http.Error(w, err.Error(), http.StatusBadRequest)
256+
return
257+
}
258+
port := uint16(port16)
259+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
260+
defer cancel()
261+
addr := fmt.Sprintf("%s:%d", ip, port)
262+
// Wait until the port is available.
263+
for {
264+
conn, err := n.DialContextTCP(ctx, addr)
265+
if err == nil {
266+
conn.Close()
267+
logrus.Infof("Port is available on %s", addr)
268+
w.WriteHeader(http.StatusOK)
269+
break
270+
}
271+
select {
272+
case <-ctx.Done():
273+
msg := fmt.Sprintf("timed out waiting for port to become available on %s", addr)
274+
logrus.Warn(msg)
275+
http.Error(w, msg, http.StatusRequestTimeout)
276+
return
277+
default:
278+
}
279+
logrus.Infof("Waiting for port to become available on %s", addr)
280+
time.Sleep(500 * time.Millisecond)
281+
}
282+
})
283+
return m
284+
}
285+
242286
func searchDomains() []string {
243287
if runtime.GOOS != "windows" {
244288
return resolveSearchDomain("/etc/resolv.conf")

website/content/en/docs/config/environment-variables.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,15 @@ This page documents the environment variables used in Lima.
106106
lima
107107
```
108108

109+
### `LIMA_SSH_OVER_VSOCK`
110+
- **Description**: Specifies to use vsock for SSH connection instead of port forwarding.
111+
- **Default**: `true` (since v2.0.0)
112+
- **Usage**:
113+
```sh
114+
export LIMA_SSH_OVER_VSOCK=true
115+
```
116+
- **Note**: This variable is effective only if the VM is VZ based and systemd is v256 or later (e.g. Ubuntu 24.10+).
117+
109118
### `LIMA_SSH_PORT_FORWARDER`
110119

111120
- **Description**: Specifies to use the SSH port forwarder (slow) instead of gRPC (fast, previously unstable)

website/content/en/docs/config/port.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ LIMA_SSH_PORT_FORWARDER=true limactl start
3636
- Doesn't support UDP based port forwarding
3737
- Spawns child process on host for running SSH master.
3838

39+
#### SSH over AF_VSOCK
40+
41+
| ⚡ Requirement | Lima >= 2.0 |
42+
|---------------|-------------|
43+
44+
If VM is VZ based and systemd is v256 or later (e.g. Ubuntu 24.10+), Lima uses AF_VSOCK for communication between host and guest.
45+
SSH based port forwarding is much faster when using AF_VSOCK compared to traditional virtual network based port forwarding.
46+
47+
To disable this feature, set `LIMA_SSH_OVER_VSOCK` to `false`:
48+
49+
```bash
50+
export LIMA_SSH_OVER_VSOCK=false
51+
```
52+
3953
### Using GRPC
4054

4155
| ⚡ Requirement | Lima >= 1.0 |

0 commit comments

Comments
 (0)