Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/networks/usernet/client.go
2611 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package usernet
5
6
import (
7
"context"
8
"encoding/json"
9
"errors"
10
"fmt"
11
"net"
12
"net/http"
13
"os"
14
"strconv"
15
"time"
16
17
gvproxyclient "github.com/containers/gvisor-tap-vsock/pkg/client"
18
"github.com/containers/gvisor-tap-vsock/pkg/types"
19
20
"github.com/lima-vm/lima/v2/pkg/httpclientutil"
21
"github.com/lima-vm/lima/v2/pkg/limatype"
22
"github.com/lima-vm/lima/v2/pkg/limayaml"
23
"github.com/lima-vm/lima/v2/pkg/networks/usernet/dnshosts"
24
)
25
26
type Client struct {
27
Directory string
28
29
client *http.Client
30
delegate *gvproxyclient.Client
31
base string
32
subnet net.IP
33
}
34
35
func (c *Client) ConfigureDriver(ctx context.Context, inst *limatype.Instance, sshLocalPort int) error {
36
macAddress := limayaml.MACAddress(inst.Dir)
37
ipAddress, err := c.ResolveIPAddress(ctx, macAddress)
38
if err != nil {
39
return err
40
}
41
if sshLocalPort != 0 {
42
err = c.ResolveAndForwardSSH(ipAddress, sshLocalPort)
43
if err != nil {
44
return err
45
}
46
}
47
hosts := inst.Config.HostResolver.Hosts
48
if hosts == nil {
49
hosts = make(map[string]string)
50
}
51
hosts[fmt.Sprintf("%s.internal", inst.Hostname)] = ipAddress
52
err = c.AddDNSHosts(hosts)
53
return err
54
}
55
56
func (c *Client) UnExposeSSH(sshPort int) error {
57
return c.delegate.Unexpose(&types.UnexposeRequest{
58
Local: fmt.Sprintf("127.0.0.1:%d", sshPort),
59
Protocol: "tcp",
60
})
61
}
62
63
func (c *Client) AddDNSHosts(hosts map[string]string) error {
64
hosts["host.lima.internal"] = GatewayIP(c.subnet)
65
zones := dnshosts.ExtractZones(hosts)
66
for _, zone := range zones {
67
err := c.delegate.AddDNS(&zone)
68
if err != nil {
69
return err
70
}
71
}
72
return nil
73
}
74
75
func (c *Client) ResolveAndForwardSSH(ipAddr string, sshPort int) error {
76
err := c.delegate.Expose(&types.ExposeRequest{
77
Local: fmt.Sprintf("127.0.0.1:%d", sshPort),
78
Remote: fmt.Sprintf("%s:22", ipAddr),
79
Protocol: "tcp",
80
})
81
if err != nil {
82
return err
83
}
84
return nil
85
}
86
87
func (c *Client) ResolveIPAddress(ctx context.Context, vmMacAddr string) (string, error) {
88
resolveIPAddressTimeout := 2 * time.Minute
89
resolveIPAddressTimeoutEnv := os.Getenv("LIMA_USERNET_RESOLVE_IP_ADDRESS_TIMEOUT")
90
if resolveIPAddressTimeoutEnv != "" {
91
if parsedTimeout, err := strconv.Atoi(resolveIPAddressTimeoutEnv); err == nil {
92
resolveIPAddressTimeout = time.Duration(parsedTimeout) * time.Minute
93
}
94
}
95
ctx, cancel := context.WithTimeout(ctx, resolveIPAddressTimeout)
96
defer cancel()
97
ticker := time.NewTicker(500 * time.Millisecond)
98
for {
99
select {
100
case <-ctx.Done():
101
return "", errors.New("usernet unable to resolve IP for SSH forwarding")
102
case <-ticker.C:
103
leases, err := c.Leases(ctx)
104
if err != nil {
105
return "", err
106
}
107
108
for ipAddr, leaseAddr := range leases {
109
if vmMacAddr == leaseAddr {
110
return ipAddr, nil
111
}
112
}
113
}
114
}
115
}
116
117
func (c *Client) Leases(ctx context.Context) (map[string]string, error) {
118
u := fmt.Sprintf("%s%s", c.base, "/services/dhcp/leases")
119
res, err := httpclientutil.Get(ctx, c.client, u)
120
if err != nil {
121
return nil, err
122
}
123
defer res.Body.Close()
124
dec := json.NewDecoder(res.Body)
125
var leases map[string]string
126
if err := dec.Decode(&leases); err != nil {
127
return nil, err
128
}
129
return leases, nil
130
}
131
132
// WaitOpeningSSHPort Wait until the guest ssh server is available.
133
func (c *Client) WaitOpeningSSHPort(ctx context.Context, inst *limatype.Instance) error {
134
// This timeout is based on the maximum wait time for the first essential requirement.
135
timeoutSeconds := 600
136
ctx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
137
defer cancel()
138
macAddress := limayaml.MACAddress(inst.Dir)
139
ipAddr, err := c.ResolveIPAddress(ctx, macAddress)
140
if err != nil {
141
return err
142
}
143
// -1 avoids both sides timing out simultaneously.
144
u := fmt.Sprintf("%s/extension/wait_port?ip=%s&port=22&timeout=%d", c.base, ipAddr, timeoutSeconds-1)
145
res, err := httpclientutil.Get(ctx, c.client, u)
146
if err != nil {
147
return err
148
}
149
defer res.Body.Close()
150
if res.StatusCode != http.StatusOK {
151
return errors.New("failed to wait for SSH port")
152
}
153
return nil
154
}
155
156
func NewClientByName(nwName string) *Client {
157
endpointSock, err := Sock(nwName, EndpointSock)
158
if err != nil {
159
return nil
160
}
161
subnet, err := Subnet(nwName)
162
if err != nil {
163
return nil
164
}
165
return NewClient(endpointSock, subnet)
166
}
167
168
func NewClient(endpointSock string, subnet net.IP) *Client {
169
return create(endpointSock, subnet, "http://lima")
170
}
171
172
func create(sock string, subnet net.IP, base string) *Client {
173
client := &http.Client{
174
Transport: &http.Transport{
175
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
176
var d net.Dialer
177
return d.DialContext(ctx, "unix", sock)
178
},
179
},
180
}
181
delegate := gvproxyclient.New(client, "http://lima")
182
return &Client{
183
client: client,
184
delegate: delegate,
185
base: base,
186
subnet: subnet,
187
}
188
}
189
190