Skip to content

Commit 146e87a

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 37ac1fd commit 146e87a

File tree

7 files changed

+177
-26
lines changed

7 files changed

+177
-26
lines changed

cmd/limactl/usernet.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,13 @@ func usernetAction(cmd *cobra.Command, _ []string) error {
8080
// Environment Variables
8181
// LIMA_USERNET_RESOLVE_IP_ADDRESS_TIMEOUT: Specifies the timeout duration for resolving IP addresses in minutes. Default is 2 minutes.
8282

83-
return usernet.StartGVisorNetstack(cmd.Context(), &usernet.GVisorNetstackOpts{
83+
_, err = usernet.StartGVisorNetstack(cmd.Context(), &usernet.GVisorNetstackOpts{
8484
MTU: mtu,
8585
Endpoint: endpoint,
8686
QemuSocket: qemuSocket,
8787
FdSocket: fdSocket,
8888
Subnet: subnet,
8989
DefaultLeases: leases,
9090
})
91+
return err
9192
}

pkg/driver/vz/vm_darwin.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,35 @@ 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, sshLocalPort); err == nil {
125+
if err := wrapper.startVsockForwarder(ctx, 22, uint32(sshLocalPort)); err == nil {
126+
logrus.Infof("Detected SSH server is listening on the vsock port; changed localhost:%d to proxy for the vsock port", sshLocalPort)
127+
sshLocalPort = 0 // disable gvisor ssh port forwarding
128+
} else {
129+
logrus.WithError(err).Warn("Failed to detect SSH server on vsock port, falling back to gvisor's forwarder")
130+
}
131+
} else {
132+
logrus.WithError(err).Warn("Failed to wait for the guest SSH server to become available, falling back to gvisor's forwarder")
133+
}
134+
err := usernetClient.ConfigureDriver(ctx, inst, sshLocalPort)
135+
if err != nil {
136+
errCh <- err
137+
}
138+
}()
139+
115140
case vz.VirtualMachineStateStopped:
116141
logrus.Info("[VZ] - vm state change: stopped")
117142
wrapper.mu.Lock()
@@ -147,7 +172,7 @@ func startUsernet(ctx context.Context, inst *limatype.Instance) (*usernet.Client
147172
os.RemoveAll(endpointSock)
148173
os.RemoveAll(vzSock)
149174
ctx, cancel := context.WithCancel(ctx)
150-
err = usernet.StartGVisorNetstack(ctx, &usernet.GVisorNetstackOpts{
175+
vn, err := usernet.StartGVisorNetstack(ctx, &usernet.GVisorNetstackOpts{
151176
MTU: 1500,
152177
Endpoint: endpointSock,
153178
FdSocket: vzSock,
@@ -162,7 +187,7 @@ func startUsernet(ctx context.Context, inst *limatype.Instance) (*usernet.Client
162187
return nil, nil, err
163188
}
164189
subnetIP, _, err := net.ParseCIDR(networks.SlirpNetwork)
165-
return usernet.NewClient(endpointSock, subnetIP), cancel, err
190+
return usernet.NewClient(endpointSock, subnetIP, vn), cancel, err
166191
}
167192

168193
func createVM(ctx context.Context, inst *limatype.Instance) (*vz.VirtualMachine, error) {

pkg/driver/vz/vsock_forwarder.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
"fmt"
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, hostPort uint32) 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+
var lc net.ListenConfig
26+
l, err := lc.Listen(ctx, "tcp", fmt.Sprintf("127.0.0.1:%d", hostPort))
27+
if err != nil {
28+
return err
29+
}
30+
logrus.Infof("started vsock forwarder: localhost:%d -> vsock:%d on VM", hostPort, vsockPort)
31+
go func() {
32+
defer l.Close()
33+
for {
34+
conn, err := l.Accept()
35+
if err != nil {
36+
logrus.WithError(err).Errorf("vsock forwarder accept error: %v", err)
37+
return
38+
}
39+
p := tcpproxy.DialProxy{
40+
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
41+
return m.dialVsock(ctx, vsockPort)
42+
},
43+
}
44+
go p.HandleConn(conn)
45+
select {
46+
case <-ctx.Done():
47+
return
48+
default:
49+
continue
50+
}
51+
}
52+
}()
53+
return nil
54+
}
55+
56+
func (m *virtualMachineWrapper) dialVsock(_ context.Context, port uint32) (conn net.Conn, err error) {
57+
for _, socket := range m.SocketDevices() {
58+
conn, err = socket.Connect(port)
59+
if err == nil {
60+
return conn, nil
61+
}
62+
}
63+
return nil, err
64+
}

pkg/networks/usernet/client.go

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616

1717
gvproxyclient "github.com/containers/gvisor-tap-vsock/pkg/client"
1818
"github.com/containers/gvisor-tap-vsock/pkg/types"
19+
"github.com/containers/gvisor-tap-vsock/pkg/virtualnetwork"
20+
"github.com/sirupsen/logrus"
1921

2022
"github.com/lima-vm/lima/v2/pkg/httpclientutil"
2123
"github.com/lima-vm/lima/v2/pkg/limatype"
@@ -30,6 +32,8 @@ type Client struct {
3032
delegate *gvproxyclient.Client
3133
base string
3234
subnet net.IP
35+
36+
vn *virtualnetwork.VirtualNetwork
3337
}
3438

3539
func (c *Client) ConfigureDriver(ctx context.Context, inst *limatype.Instance, sshLocalPort int) error {
@@ -38,9 +42,11 @@ func (c *Client) ConfigureDriver(ctx context.Context, inst *limatype.Instance, s
3842
if err != nil {
3943
return err
4044
}
41-
err = c.ResolveAndForwardSSH(ipAddress, sshLocalPort)
42-
if err != nil {
43-
return err
45+
if sshLocalPort != 0 {
46+
err = c.ResolveAndForwardSSH(ipAddress, sshLocalPort)
47+
if err != nil {
48+
return err
49+
}
4450
}
4551
hosts := inst.Config.HostResolver.Hosts
4652
if hosts == nil {
@@ -127,6 +133,37 @@ func (c *Client) Leases(ctx context.Context) (map[string]string, error) {
127133
return leases, nil
128134
}
129135

136+
// Wait until the guest ssh server is available.
137+
func (c *Client) WaitOpeningSSHPort(ctx context.Context, inst *limatype.Instance, blockingPort int) error {
138+
if c.vn == nil {
139+
return errors.New("internal error: vn is nil")
140+
}
141+
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
142+
defer cancel()
143+
macAddress := limayaml.MACAddress(inst.Dir)
144+
ipAddr, err := c.ResolveIPAddress(ctx, macAddress)
145+
if err != nil {
146+
return err
147+
}
148+
// Wait until the guest ssh server is available.
149+
for {
150+
conn, err := c.vn.DialContextTCP(ctx, fmt.Sprintf("%s:22", ipAddr))
151+
if err == nil {
152+
conn.Close()
153+
logrus.Infof("Guest SSH server is available on %s:22", ipAddr)
154+
break
155+
}
156+
select {
157+
case <-ctx.Done():
158+
return errors.New("timed out waiting for SSH server to be available")
159+
default:
160+
}
161+
logrus.Infof("Waiting for guest ssh server to be available on %s:22", ipAddr)
162+
time.Sleep(500 * time.Millisecond)
163+
}
164+
return nil
165+
}
166+
130167
func NewClientByName(nwName string) *Client {
131168
endpointSock, err := Sock(nwName, EndpointSock)
132169
if err != nil {
@@ -136,14 +173,14 @@ func NewClientByName(nwName string) *Client {
136173
if err != nil {
137174
return nil
138175
}
139-
return NewClient(endpointSock, subnet)
176+
return NewClient(endpointSock, subnet, nil)
140177
}
141178

142-
func NewClient(endpointSock string, subnet net.IP) *Client {
143-
return create(endpointSock, subnet, "http://lima")
179+
func NewClient(endpointSock string, subnet net.IP, vn *virtualnetwork.VirtualNetwork) *Client {
180+
return create(endpointSock, subnet, "http://lima", vn)
144181
}
145182

146-
func create(sock string, subnet net.IP, base string) *Client {
183+
func create(sock string, subnet net.IP, base string, vn *virtualnetwork.VirtualNetwork) *Client {
147184
client := &http.Client{
148185
Transport: &http.Transport{
149186
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
@@ -158,5 +195,6 @@ func create(sock string, subnet net.IP, base string) *Client {
158195
delegate: delegate,
159196
base: base,
160197
subnet: subnet,
198+
vn: vn,
161199
}
162200
}

pkg/networks/usernet/gvproxy.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@ var opts *GVisorNetstackOpts
4040

4141
const gatewayMacAddr = "5a:94:ef:e4:0c:dd"
4242

43-
func StartGVisorNetstack(ctx context.Context, gVisorOpts *GVisorNetstackOpts) error {
43+
func StartGVisorNetstack(ctx context.Context, gVisorOpts *GVisorNetstackOpts) (*virtualnetwork.VirtualNetwork, error) {
4444
opts = gVisorOpts
4545

4646
ip, ipNet, err := net.ParseCIDR(opts.Subnet)
4747
if err != nil {
48-
return err
48+
return nil, err
4949
}
5050
gatewayIP := GatewayIP(ip)
5151

@@ -82,41 +82,41 @@ func StartGVisorNetstack(ctx context.Context, gVisorOpts *GVisorNetstackOpts) er
8282
}
8383

8484
groupErrs, ctx := errgroup.WithContext(ctx)
85-
err = run(ctx, groupErrs, &config)
85+
vn, err := run(ctx, groupErrs, &config)
8686
if err != nil {
87-
return err
87+
return nil, err
8888
}
8989
if opts.Async {
90-
return err
90+
return vn, nil
9191
}
92-
return groupErrs.Wait()
92+
return vn, groupErrs.Wait()
9393
}
9494

95-
func run(ctx context.Context, g *errgroup.Group, configuration *types.Configuration) error {
95+
func run(ctx context.Context, g *errgroup.Group, configuration *types.Configuration) (*virtualnetwork.VirtualNetwork, error) {
9696
vn, err := virtualnetwork.New(configuration)
9797
if err != nil {
98-
return err
98+
return nil, err
9999
}
100100

101101
ln, err := transport.Listen(fmt.Sprintf("unix://%s", opts.Endpoint))
102102
if err != nil {
103-
return err
103+
return nil, err
104104
}
105105
httpServe(ctx, g, ln, vn.Mux())
106106

107107
if opts.QemuSocket != "" {
108108
err = listenQEMU(ctx, vn)
109109
if err != nil {
110-
return err
110+
return nil, err
111111
}
112112
}
113113
if opts.FdSocket != "" {
114114
err = listenFD(ctx, vn)
115115
if err != nil {
116-
return err
116+
return nil, err
117117
}
118118
}
119-
return nil
119+
return vn, nil
120120
}
121121

122122
func listenQEMU(ctx context.Context, vn *virtualnetwork.VirtualNetwork) error {

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`
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)