Skip to content

Commit 974e806

Browse files
committed
Add vmnet package
The package manages the vmnet-helper[1] child process, providing connection to the vment network without running the guest as root. We will use vment-helper for the vfkit driver, which does not have a way to use shared network, when guests can access other guest in the network. We can use it later with the qemu driver as alternative to socket_vment. [1] https://github.com/nirs/vmnet-helper
1 parent 9592cd9 commit 974e806

File tree

1 file changed

+267
-0
lines changed

1 file changed

+267
-0
lines changed

pkg/drivers/vmnet/vmnet.go

+267
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
//go:build darwin
2+
3+
/*
4+
Copyright 2024 The Kubernetes Authors All rights reserved.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
// package vment provides the helper process connecting virtual machines to the
20+
// vmnet network.
21+
package vmnet
22+
23+
import (
24+
"crypto/sha256"
25+
"encoding/json"
26+
"errors"
27+
"fmt"
28+
"os"
29+
"os/exec"
30+
"path/filepath"
31+
"strconv"
32+
"strings"
33+
"syscall"
34+
35+
"github.com/docker/machine/libmachine/log"
36+
"github.com/docker/machine/libmachine/state"
37+
)
38+
39+
const (
40+
pidfileName = "vment-helper.pid"
41+
logfileName = "vment-helper.log"
42+
executablePath = "/opt/vmnet-helper/bin/vmnet-helper"
43+
)
44+
45+
// Helper manages the vmnet-helper process.
46+
type Helper struct {
47+
// The minikube machine name this helper is serving.
48+
MachineName string
49+
50+
// Minikube home directory.
51+
StorePath string
52+
53+
// Set when vmnet interface is started.
54+
macAddress string
55+
}
56+
57+
type interfaceInfo struct {
58+
MACAddress string `json:"vmnet_mac_address"`
59+
}
60+
61+
// HelperAvailable tells if vment-helper executable is installed and configured
62+
// correctly.
63+
func HelperAvailable() bool {
64+
version, err := exec.Command("sudo", "--non-interactive", executablePath, "--version").Output()
65+
if err != nil {
66+
log.Debugf("Failed to run vmnet-helper: %w", executablePath, err)
67+
return false
68+
}
69+
log.Debugf("Using vmnet-helper version %q", executablePath, version)
70+
return true
71+
}
72+
73+
// Start the vmnet-helper child process, creating the vmnet interface for the
74+
// machine. sock is a connected unix datagram socket to pass the helper child
75+
// process.
76+
func (h *Helper) Start(sock *os.File) error {
77+
cmd := exec.Command(
78+
"sudo",
79+
"--non-interactive",
80+
"--close-from", fmt.Sprintf("%d", sock.Fd()+1),
81+
executablePath,
82+
"--fd", fmt.Sprintf("%d", sock.Fd()),
83+
"--interface-id", uuidFromName("io.k8s.sigs.minikube."+h.MachineName),
84+
)
85+
86+
cmd.ExtraFiles = []*os.File{sock}
87+
88+
// Create vment-helper in a new process group so it is not harmed when
89+
// terminating the minikube process group.
90+
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
91+
92+
logfile, err := h.openLogfile()
93+
if err != nil {
94+
return fmt.Errorf("failed to open helper logfile: %w", err)
95+
}
96+
defer logfile.Close()
97+
cmd.Stderr = logfile
98+
99+
stdout, err := cmd.StdoutPipe()
100+
if err != nil {
101+
return fmt.Errorf("failed to create helper stdout pipe: %w", err)
102+
}
103+
defer stdout.Close()
104+
105+
if err := cmd.Start(); err != nil {
106+
return fmt.Errorf("failed to start vmnet-helper: %w", err)
107+
}
108+
109+
log.Infof("Started vmnet-helper (pid=%v)", cmd.Process.Pid)
110+
111+
if err := writePidfile(h.machinePath(pidfileName), cmd.Process.Pid); err != nil {
112+
return fmt.Errorf("failed to write vmnet-helper pidfile: %w", err)
113+
}
114+
115+
var info interfaceInfo
116+
if err := json.NewDecoder(stdout).Decode(&info); err != nil {
117+
return fmt.Errorf("failed to decode vmnet interface info: %w", err)
118+
}
119+
120+
log.Infof("Got mac address %q", info.MACAddress)
121+
h.macAddress = info.MACAddress
122+
123+
return nil
124+
}
125+
126+
// GetMACAddress reutuns the mac address assigned by vment framework.
127+
func (h *Helper) GetMACAddress() string {
128+
return h.macAddress
129+
}
130+
131+
// Stop the vmnet-helper child process.
132+
func (h *Helper) Stop() error {
133+
path := h.machinePath(pidfileName)
134+
pid, err := readPidfile(path)
135+
if err != nil {
136+
if errors.Is(err, os.ErrNotExist) {
137+
return nil
138+
}
139+
return err
140+
}
141+
process, err := os.FindProcess(pid)
142+
if err != nil {
143+
return err
144+
}
145+
log.Infof("Terminate vmnet-helper (pid=%v)", pid)
146+
// Terminate sudo, which will terminate vmnet-helper.
147+
if err := process.Signal(syscall.SIGTERM); err != nil {
148+
if err != os.ErrProcessDone {
149+
return err
150+
}
151+
}
152+
os.Remove(path)
153+
return nil
154+
}
155+
156+
// Kill the vmnet-helper child process.
157+
func (h *Helper) Kill() error {
158+
path := h.machinePath(pidfileName)
159+
pid, err := readPidfile(path)
160+
if err != nil {
161+
if errors.Is(err, os.ErrNotExist) {
162+
return nil
163+
}
164+
return err
165+
}
166+
log.Infof("Kill vmnet-helper process group (pgid=%v)", pid)
167+
if err := syscall.Kill(-pid, syscall.SIGKILL); err != nil {
168+
if err != syscall.ESRCH {
169+
return err
170+
}
171+
}
172+
os.Remove(path)
173+
return nil
174+
}
175+
176+
// GetState returns the vment-helper child process state.
177+
func (h *Helper) GetState() (state.State, error) {
178+
path := h.machinePath(pidfileName)
179+
pid, err := readPidfile(path)
180+
if err != nil {
181+
if errors.Is(err, os.ErrNotExist) {
182+
return state.Stopped, nil
183+
}
184+
return state.Error, err
185+
}
186+
if err := checkPid(pid); err != nil {
187+
// No pid, remove pidfile
188+
os.Remove(path)
189+
return state.Stopped, nil
190+
}
191+
return state.Running, nil
192+
}
193+
194+
func (h *Helper) openLogfile() (*os.File, error) {
195+
path := h.machinePath(logfileName)
196+
return os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
197+
}
198+
199+
func (h *Helper) machinePath(fileName string) string {
200+
return filepath.Join(h.StorePath, "machines", h.MachineName, fileName)
201+
}
202+
203+
func writePidfile(path string, pid int) error {
204+
data := fmt.Sprintf("%v", pid)
205+
if err := os.WriteFile(path, []byte(data), 0600); err != nil {
206+
return err
207+
}
208+
return nil
209+
}
210+
211+
func readPidfile(path string) (int, error) {
212+
data, err := os.ReadFile(path)
213+
if err != nil {
214+
return -1, err
215+
}
216+
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
217+
if err != nil {
218+
return -1, err
219+
}
220+
return pid, nil
221+
}
222+
223+
func checkPid(pid int) error {
224+
process, err := os.FindProcess(pid)
225+
if err != nil {
226+
return err
227+
}
228+
return process.Signal(syscall.Signal(0))
229+
}
230+
231+
// uuidFromName generated a random UUID (type 4) from string name. This is
232+
// useful for creating interface ID from virtual machine name to ensure the same
233+
// MAC address in all runs.
234+
func uuidFromName(name string) string {
235+
sum := sha256.Sum256([]byte(name))
236+
uuid := sum[:16]
237+
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
238+
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
239+
return fmt.Sprintf("%4x-%2x-%2x-%2x-%6x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:16])
240+
}
241+
242+
// Apple recommend receive buffer size to be 4 times the size of the send buffer
243+
// size, but send buffer size is not used to allocate a buffer in datagram
244+
// sockets, it only limits the maximum packet size. Must be larger than TSO
245+
// packets size (65550 bytes).
246+
const sendBufferSize = 65 * 1024
247+
248+
// The receive buffer size determine how many packets can be queued by the
249+
// peer. Using bigger receive buffer size make ENOBUFS error less likely for the
250+
// peer and improves throughput.
251+
const recvBufferSize = 4 * 1024 * 1024
252+
253+
// Socketpair returns a pair of connected unix datagram sockets that can be used
254+
// to connect the helper and a vm. Pass one socket to the helper child process
255+
// and the other to the vm child process.
256+
func Socketpair() (*os.File, *os.File, error) {
257+
fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_DGRAM, 0)
258+
if err != nil {
259+
return nil, nil, err
260+
}
261+
// Setting buffer size is an optimization - don't fail on errors.
262+
for _, fd := range fds {
263+
_ = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_SNDBUF, sendBufferSize)
264+
_ = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF, recvBufferSize)
265+
}
266+
return os.NewFile(uintptr(fds[0]), "sock1"), os.NewFile(uintptr(fds[1]), "sock2"), nil
267+
}

0 commit comments

Comments
 (0)