Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/networks/usernet/gvproxy.go
2613 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package usernet
5
6
import (
7
"bufio"
8
"context"
9
"errors"
10
"fmt"
11
"net"
12
"net/http"
13
"os"
14
"runtime"
15
"strconv"
16
"strings"
17
"time"
18
19
"github.com/balajiv113/fd"
20
"github.com/containers/gvisor-tap-vsock/pkg/transport"
21
"github.com/containers/gvisor-tap-vsock/pkg/types"
22
"github.com/containers/gvisor-tap-vsock/pkg/virtualnetwork"
23
"github.com/sirupsen/logrus"
24
"golang.org/x/sync/errgroup"
25
)
26
27
type GVisorNetstackOpts struct {
28
MTU int
29
30
QemuSocket string
31
FdSocket string
32
Endpoint string
33
34
Subnet string
35
36
Async bool
37
38
DefaultLeases map[string]string
39
}
40
41
var opts *GVisorNetstackOpts
42
43
const gatewayMacAddr = "5a:94:ef:e4:0c:dd"
44
45
func StartGVisorNetstack(ctx context.Context, gVisorOpts *GVisorNetstackOpts) error {
46
opts = gVisorOpts
47
48
ip, ipNet, err := net.ParseCIDR(opts.Subnet)
49
if err != nil {
50
return err
51
}
52
gatewayIP := GatewayIP(ip)
53
54
leases := map[string]string{}
55
if opts.DefaultLeases != nil {
56
for k, v := range opts.DefaultLeases {
57
if ipNet.Contains(net.ParseIP(k)) {
58
leases[k] = v
59
}
60
}
61
}
62
leases[gatewayIP] = gatewayMacAddr
63
64
// The way gvisor-tap-vsock implemented slirp is different from tradition SLIRP,
65
// - GatewayIP handling all request, also answers DNS queries
66
// - based on NAT configuration, gateway forwards and translates calls to host
67
// Comparing this with QEMU SLIRP,
68
// - DNS is equivalent to GatewayIP
69
// - GatewayIP is equivalent to NAT configuration
70
config := types.Configuration{
71
Debug: false,
72
MTU: opts.MTU,
73
Subnet: opts.Subnet,
74
GatewayIP: gatewayIP,
75
GatewayMacAddress: gatewayMacAddr,
76
DHCPStaticLeases: leases,
77
Forwards: map[string]string{},
78
DNS: []types.Zone{},
79
DNSSearchDomains: searchDomains(),
80
NAT: map[string]string{
81
gatewayIP: "127.0.0.1",
82
},
83
GatewayVirtualIPs: []string{gatewayIP},
84
}
85
86
groupErrs, ctx := errgroup.WithContext(ctx)
87
err = run(ctx, groupErrs, &config)
88
if err != nil {
89
return err
90
}
91
if opts.Async {
92
return err
93
}
94
return groupErrs.Wait()
95
}
96
97
func run(ctx context.Context, g *errgroup.Group, configuration *types.Configuration) error {
98
vn, err := virtualnetwork.New(configuration)
99
if err != nil {
100
return err
101
}
102
103
ln, err := transport.Listen(fmt.Sprintf("unix://%s", opts.Endpoint))
104
if err != nil {
105
return err
106
}
107
108
httpServe(ctx, g, ln, muxWithExtension(vn))
109
110
if opts.QemuSocket != "" {
111
err = listenQEMU(ctx, vn)
112
if err != nil {
113
return err
114
}
115
}
116
if opts.FdSocket != "" {
117
err = listenFD(ctx, vn)
118
if err != nil {
119
return err
120
}
121
}
122
return nil
123
}
124
125
func listenQEMU(ctx context.Context, vn *virtualnetwork.VirtualNetwork) error {
126
var lc net.ListenConfig
127
listener, err := lc.Listen(ctx, "unix", opts.QemuSocket)
128
if err != nil {
129
return err
130
}
131
132
go func() {
133
defer listener.Close()
134
<-ctx.Done()
135
}()
136
137
go func() {
138
for {
139
conn, err := listener.Accept()
140
if err != nil {
141
if errors.Is(err, net.ErrClosed) {
142
return
143
}
144
logrus.Error("QEMU accept failed", err)
145
}
146
147
go func() {
148
err = vn.AcceptQemu(ctx, conn)
149
if err != nil {
150
logrus.Error("QEMU connection closed with error", err)
151
}
152
conn.Close()
153
}()
154
select {
155
case <-ctx.Done():
156
return
157
default:
158
continue
159
}
160
}
161
}()
162
163
return nil
164
}
165
166
func listenFD(ctx context.Context, vn *virtualnetwork.VirtualNetwork) error {
167
var lc net.ListenConfig
168
listener, err := lc.Listen(ctx, "unix", opts.FdSocket)
169
if err != nil {
170
return err
171
}
172
173
go func() {
174
defer listener.Close()
175
<-ctx.Done()
176
}()
177
178
go func() {
179
for {
180
conn, err := listener.Accept()
181
if err != nil {
182
if errors.Is(err, net.ErrClosed) {
183
return
184
}
185
logrus.Error("FD accept failed", err)
186
continue // since conn is nil
187
}
188
189
files, err := fd.Get(conn.(*net.UnixConn), 1, []string{"client"})
190
if err != nil {
191
logrus.Error("Failed to get FD via socket", err)
192
}
193
194
if len(files) != 1 {
195
logrus.Error("Invalid number of fd in response", err)
196
}
197
fileConn, err := net.FileConn(files[0])
198
if err != nil {
199
logrus.Error("Error in FD Socket", err)
200
}
201
files[0].Close()
202
203
go func() {
204
err = vn.AcceptBess(ctx, &UDPFileConn{Conn: fileConn})
205
if err != nil {
206
logrus.Error("FD connection closed with error", err)
207
}
208
fileConn.Close()
209
}()
210
select {
211
case <-ctx.Done():
212
return
213
default:
214
continue
215
}
216
}
217
}()
218
219
return nil
220
}
221
222
func httpServe(ctx context.Context, g *errgroup.Group, ln net.Listener, mux http.Handler) {
223
s := &http.Server{
224
Handler: mux,
225
ReadTimeout: 10 * time.Second,
226
WriteTimeout: 10 * time.Second,
227
}
228
g.Go(func() error {
229
<-ctx.Done()
230
return s.Close()
231
})
232
g.Go(func() error {
233
err := s.Serve(ln)
234
if err != nil {
235
if err == http.ErrServerClosed {
236
return nil
237
}
238
return err
239
}
240
return nil
241
})
242
}
243
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
addr := fmt.Sprintf("%s:%d", ip, port)
260
261
timeoutSeconds := 10
262
if timeoutString := r.URL.Query().Get("timeout"); timeoutString != "" {
263
timeout16, err := strconv.ParseUint(timeoutString, 10, 16)
264
if err != nil {
265
http.Error(w, err.Error(), http.StatusBadRequest)
266
return
267
}
268
timeoutSeconds = int(timeout16)
269
}
270
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds)*time.Second)
271
defer cancel()
272
// Wait until the port is available.
273
for {
274
conn, err := n.DialContextTCP(ctx, addr)
275
if err == nil {
276
conn.Close()
277
logrus.Debugf("Port is available on %s", addr)
278
w.WriteHeader(http.StatusOK)
279
break
280
}
281
select {
282
case <-ctx.Done():
283
msg := fmt.Sprintf("timed out waiting for port to become available on %s", addr)
284
logrus.Warn(msg)
285
http.Error(w, msg, http.StatusRequestTimeout)
286
return
287
default:
288
}
289
logrus.Debugf("Waiting for port to become available on %s", addr)
290
time.Sleep(1 * time.Second)
291
}
292
})
293
return m
294
}
295
296
func searchDomains() []string {
297
if runtime.GOOS != "windows" {
298
return resolveSearchDomain("/etc/resolv.conf")
299
}
300
return nil
301
}
302
303
func resolveSearchDomain(file string) []string {
304
f, err := os.Open(file)
305
if err != nil {
306
logrus.Errorf("open file error: %v", err)
307
return nil
308
}
309
defer f.Close()
310
sc := bufio.NewScanner(f)
311
searchPrefix := "search "
312
for sc.Scan() {
313
if after, ok := strings.CutPrefix(sc.Text(), searchPrefix); ok {
314
searchDomains := strings.Split(after, " ")
315
logrus.Debugf("Using search domains: %v", searchDomains)
316
return searchDomains
317
}
318
}
319
if err := sc.Err(); err != nil {
320
logrus.Errorf("scan file error: %v", err)
321
return nil
322
}
323
return nil
324
}
325
326