diff --git a/doc/usage.md b/doc/usage.md index cdf785ff..b86ecfab 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -135,9 +135,14 @@ The `--device virtio-net` option adds a network interface to the virtual machine #### Arguments - `mac`: optional argument to specify the MAC address of the VM. If it's omitted, a random MAC address will be used. +- `fd`: file descriptor to attach to the guest network interface. The file descriptor must be a connected datagram socket. See [VZFileHandleNetworkDeviceAttachment](https://developer.apple.com/documentation/virtualization/vzfilehandlenetworkdeviceattachment?language=objc) for more details. +- `nat`: guest network traffic will be NAT'ed through the host. This is the default. See [VZNATNetworkDeviceAttachment](https://developer.apple.com/documentation/virtualization/vznatnetworkdeviceattachment?language=objc) for more details. +- `unixSocketPath: path to a unix socket to attach to the guest network interface. See See [VZFileHandleNetworkDeviceAttachment](https://developer.apple.com/documentation/virtualization/vzfilehandlenetworkdeviceattachment?language=objc) for more details. + +`fd`, `nat`, `unixSocketPath` are mutually exclusive. #### Example -`--device virtio-net,mac=52:54:00:70:2b:71` +`--device virtio-net,nat,mac=52:54:00:70:2b:71` ### Serial Port @@ -145,9 +150,11 @@ The `--device virtio-net` option adds a network interface to the virtual machine #### Description The `--device virtio-serial` option adds a serial device to the virtual machine. This is useful to redirect text output from the virtual machine to a log file. +The `logFilePath` and `stdio` arguments are mutually exclusive. #### Arguments - `logFilePath`: path where the serial port output should be written. +- `stdio`: uses stdin/stdout for the serial console input/output. #### Example `--device virtio-serial,logFilePath=/Users/virtuser/vfkit.log` diff --git a/go.mod b/go.mod index bab370c2..5adf42d9 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/crc-org/vfkit go 1.17 require ( - github.com/Code-Hex/vz/v3 v3.0.0 + github.com/Code-Hex/vz/v3 v3.0.4 github.com/docker/go-units v0.4.0 github.com/h2non/filetype v1.1.3 github.com/prashantgupta24/mac-sleep-notifier v1.0.1 @@ -19,7 +19,7 @@ require ( github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/kr/pretty v0.2.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/mod v0.7.0 // indirect + golang.org/x/mod v0.9.0 // indirect golang.org/x/sys v0.3.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index eabae40c..e35e5769 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/Code-Hex/vz/v3 v3.0.0 h1:UuYAZz8NF8n+R4MLfn/lpUlepgzZ8BlhKtioQ+q9LXk= -github.com/Code-Hex/vz/v3 v3.0.0/go.mod h1:+xPQOXVzNaZ4OeIIhlJFumtbFhsvRfqKwX7wuZS4dFA= +github.com/Code-Hex/vz/v3 v3.0.4 h1:oB74JSjSdIijjHkoYWG5ws7bHpx69m1w3PopFQdjFSc= +github.com/Code-Hex/vz/v3 v3.0.4/go.mod h1:+xPQOXVzNaZ4OeIIhlJFumtbFhsvRfqKwX7wuZS4dFA= github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -43,8 +43,8 @@ golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= -golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= diff --git a/pkg/config/config.go b/pkg/config/config.go index fba70564..3c053e7f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -2,6 +2,8 @@ package config import ( "fmt" + "os" + "os/exec" "strconv" "strings" ) @@ -75,6 +77,36 @@ func (vm *VirtualMachine) ToCmdLine() ([]string, error) { return args, nil } +func (vm *VirtualMachine) extraFiles() []*os.File { + extraFiles := []*os.File{} + for _, dev := range vm.Devices { + virtioNet, ok := dev.(*VirtioNet) + if !ok { + continue + } + if virtioNet.Socket != nil { + extraFiles = append(extraFiles, virtioNet.Socket) + } + } + + return extraFiles +} + +// Cmd creates an exec.Cmd to start vfkit with the configured devices. +// In particular it will set ExtraFiles appropriately when mapping +// a file with a network interface. +func (vm *VirtualMachine) Cmd(vfkitPath string) (*exec.Cmd, error) { + args, err := vm.ToCmdLine() + if err != nil { + return nil, err + } + + cmd := exec.Command(vfkitPath, args...) + cmd.ExtraFiles = vm.extraFiles() + + return cmd, nil +} + func (vm *VirtualMachine) AddDevicesFromCmdLine(cmdlineOpts []string) error { for _, deviceOpts := range cmdlineOpts { dev, err := deviceFromCmdLine(deviceOpts) diff --git a/pkg/config/virtio.go b/pkg/config/virtio.go index a12f66d5..c67856dc 100644 --- a/pkg/config/virtio.go +++ b/pkg/config/virtio.go @@ -3,6 +3,7 @@ package config import ( "fmt" "net" + "os" "strconv" "strings" ) @@ -42,18 +43,21 @@ type VirtioRng struct { // TODO: Add BridgedNetwork support // https://github.com/Code-Hex/vz/blob/d70a0533bf8ed0fa9ab22fa4d4ca554b7c3f3ce5/network.go#L81-L82 -// TODO: Add FileHandleNetwork support -// https://github.com/Code-Hex/vz/blob/d70a0533bf8ed0fa9ab22fa4d4ca554b7c3f3ce5/network.go#L109-L112 - // VirtioNet configures the virtual machine networking. type VirtioNet struct { Nat bool MacAddress net.HardwareAddr + // file parameter is holding a connected datagram socket. + // see https://github.com/Code-Hex/vz/blob/7f648b6fb9205d6f11792263d79876e3042c33ec/network.go#L113-L155 + Socket *os.File + + UnixSocketPath string } // VirtioSerial configures the virtual machine serial ports. type VirtioSerial struct { - LogFile string + LogFile string + UsesStdio bool } // TODO: Add VirtioBalloon @@ -131,10 +135,31 @@ func VirtioSerialNew(logFilePath string) (VirtioDevice, error) { }, nil } +func VirtioSerialNewStdio() (VirtioDevice, error) { + return &VirtioSerial{ + UsesStdio: true, + }, nil +} + +func (dev *VirtioSerial) validate() error { + if dev.LogFile != "" && dev.UsesStdio { + return fmt.Errorf("'logFilePath' and 'stdio' cannot be set at the same time") + } + if dev.LogFile == "" && !dev.UsesStdio { + return fmt.Errorf("One of 'logFilePath' or 'stdio' must be set") + } + + return nil +} + func (dev *VirtioSerial) ToCmdLine() ([]string, error) { - if dev.LogFile == "" { - return nil, fmt.Errorf("virtio-serial needs the path to the log file") + if err := dev.validate(); err != nil { + return nil, err + } + if dev.UsesStdio { + return []string{"--device", "virtio-serial,stdio"}, nil } + return []string{"--device", fmt.Sprintf("virtio-serial,logFilePath=%s", dev.LogFile)}, nil } @@ -143,16 +168,22 @@ func (dev *VirtioSerial) FromOptions(options []option) error { switch option.key { case "logFilePath": dev.LogFile = option.value + case "stdio": + if option.value != "" { + return fmt.Errorf("Unexpected value for virtio-serial 'stdio' option: %s", option.value) + } + dev.UsesStdio = true default: return fmt.Errorf("Unknown option for virtio-serial devices: %s", option.key) } } - return nil + + return dev.validate() } // VirtioNetNew creates a new network device for the virtual machine. It will // use macAddress as its MAC address. -func VirtioNetNew(macAddress string) (VirtioDevice, error) { +func VirtioNetNew(macAddress string) (*VirtioNet, error) { var hwAddr net.HardwareAddr if macAddress != "" { @@ -167,13 +198,54 @@ func VirtioNetNew(macAddress string) (VirtioDevice, error) { }, nil } +// Set the socket to use for the network communication +// +// This maps the virtual machine network interface to a connected datagram +// socket. This means all network traffic on this interface will go through +// file. +// file must be a connected datagram (SOCK_DGRAM) socket. +func (dev *VirtioNet) SetSocket(file *os.File) { + dev.Socket = file + dev.Nat = false +} + +func (dev *VirtioNet) SetUnixSocketPath(path string) { + dev.UnixSocketPath = path + dev.Nat = false +} + +func (dev *VirtioNet) validate() error { + if dev.Nat && dev.Socket != nil { + return fmt.Errorf("'nat' and 'fd' cannot be set at the same time") + } + if dev.Nat && dev.UnixSocketPath != "" { + return fmt.Errorf("'nat' and 'unixSocketPath' cannot be set at the same time") + } + if dev.Socket != nil && dev.UnixSocketPath != "" { + return fmt.Errorf("'fd' and 'unixSocketPath' cannot be set at the same time") + } + if !dev.Nat && dev.Socket == nil && dev.UnixSocketPath == "" { + return fmt.Errorf("One of 'nat' or 'fd' or 'unixSocketPath' must be set") + } + + return nil +} + func (dev *VirtioNet) ToCmdLine() ([]string, error) { - if !dev.Nat { - return nil, fmt.Errorf("virtio-net only support 'nat' networking") + if err := dev.validate(); err != nil { + return nil, err } + builder := strings.Builder{} builder.WriteString("virtio-net") - builder.WriteString(",nat") + if dev.Nat { + builder.WriteString(",nat") + } else if dev.UnixSocketPath != "" { + fmt.Fprintf(&builder, ",unixSocketPath=%s", dev.UnixSocketPath) + } else { + fmt.Fprintf(&builder, ",fd=%d", dev.Socket.Fd()) + } + if len(dev.MacAddress) != 0 { builder.WriteString(fmt.Sprintf(",mac=%s", dev.MacAddress)) } @@ -195,11 +267,20 @@ func (dev *VirtioNet) FromOptions(options []option) error { return err } dev.MacAddress = macAddress + case "fd": + fd, err := strconv.Atoi(option.value) + if err != nil { + return err + } + dev.Socket = os.NewFile(uintptr(fd), "vfkit virtio-net socket") + case "unixSocketPath": + dev.UnixSocketPath = option.value default: return fmt.Errorf("Unknown option for virtio-net devices: %s", option.key) } } - return nil + + return dev.validate() } // VirtioRngNew creates a new random number generator device to feed entropy diff --git a/pkg/config/virtio_test.go b/pkg/config/virtio_test.go index e2efd3c3..44c7383a 100644 --- a/pkg/config/virtio_test.go +++ b/pkg/config/virtio_test.go @@ -91,6 +91,13 @@ var virtioDevTests = map[string]virtioDevTest{ }, expectedCmdLine: []string{"--device", "virtio-serial,logFilePath=/foo/bar.log"}, }, + "NewVirtioSerialStdio": { + newDev: func() (VirtioDevice, error) { return VirtioSerialNewStdio() }, + expectedDev: &VirtioSerial{ + UsesStdio: true, + }, + expectedCmdLine: []string{"--device", "virtio-serial,stdio"}, + }, "NewVirtioNet": { newDev: func() (VirtioDevice, error) { return VirtioNetNew("") }, expectedDev: &VirtioNet{ @@ -98,6 +105,21 @@ var virtioDevTests = map[string]virtioDevTest{ }, expectedCmdLine: []string{"--device", "virtio-net,nat"}, }, + "NewVirtioNetWithPath": { + newDev: func() (VirtioDevice, error) { + dev, err := VirtioNetNew("") + if err != nil { + return nil, err + } + dev.SetUnixSocketPath("/tmp/unix.sock") + return dev, nil + }, + expectedDev: &VirtioNet{ + Nat: false, + UnixSocketPath: "/tmp/unix.sock", + }, + expectedCmdLine: []string{"--device", "virtio-net,unixSocketPath=/tmp/unix.sock"}, + }, "NewVirtioNetWithMacAddress": { newDev: func() (VirtioDevice, error) { return VirtioNetNew("00:11:22:33:44:55") }, expectedDev: &VirtioNet{ diff --git a/pkg/vf/virtio.go b/pkg/vf/virtio.go index 495a1d6d..a5d95c13 100644 --- a/pkg/vf/virtio.go +++ b/pkg/vf/virtio.go @@ -2,12 +2,16 @@ package vf import ( "fmt" + "net" + "os" "path/filepath" + "syscall" "github.com/crc-org/vfkit/pkg/config" "github.com/Code-Hex/vz/v3" log "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" ) type VirtioBlk config.VirtioBlk @@ -80,17 +84,37 @@ func (dev *VirtioFs) AddToVirtualMachineConfig(vmConfig *vzVirtualMachineConfigu return nil } +func (dev *VirtioNet) connectUnixPath() error { + conn, err := net.Dial("unix", dev.UnixSocketPath) + if err != nil { + return err + } + fd, err := conn.(*net.UnixConn).File() + if err != nil { + return err + } + + dev.Socket = fd + dev.UnixSocketPath = "" + return nil +} + func (dev *VirtioNet) AddToVirtualMachineConfig(vmConfig *vzVirtualMachineConfiguration) error { var ( mac *vz.MACAddress err error ) - if !dev.Nat { - return fmt.Errorf("NAT is the only supported networking mode") - } - log.Infof("Adding virtio-net device (nat: %t macAddress: [%s])", dev.Nat, dev.MacAddress) + if dev.Socket != nil { + log.Infof("Using fd %d", dev.Socket.Fd()) + } + if dev.UnixSocketPath != "" { + log.Infof("Using unix socket %s", dev.UnixSocketPath) + if err := dev.connectUnixPath(); err != nil { + return err + } + } if len(dev.MacAddress) == 0 { mac, err = vz.NewRandomLocallyAdministeredMACAddress() @@ -100,11 +124,16 @@ func (dev *VirtioNet) AddToVirtualMachineConfig(vmConfig *vzVirtualMachineConfig if err != nil { return err } - natAttachment, err := vz.NewNATNetworkDeviceAttachment() + var attachment vz.NetworkDeviceAttachment + if dev.Socket != nil { + attachment, err = vz.NewFileHandleNetworkDeviceAttachment(dev.Socket) + } else { + attachment, err = vz.NewNATNetworkDeviceAttachment() + } if err != nil { return err } - networkConfig, err := vz.NewVirtioNetworkDeviceConfiguration(natAttachment) + networkConfig, err := vz.NewVirtioNetworkDeviceConfiguration(attachment) if err != nil { return err } @@ -129,17 +158,46 @@ func (dev *VirtioRng) AddToVirtualMachineConfig(vmConfig *vzVirtualMachineConfig return nil } +// https://developer.apple.com/documentation/virtualization/running_linux_in_a_virtual_machine?language=objc#:~:text=Configure%20the%20Serial%20Port%20Device%20for%20Standard%20In%20and%20Out +func setRawMode(f *os.File) { + // Get settings for terminal + attr, _ := unix.IoctlGetTermios(int(f.Fd()), unix.TIOCGETA) + + // Put stdin into raw mode, disabling local echo, input canonicalization, + // and CR-NL mapping. + attr.Iflag &^= syscall.ICRNL + attr.Lflag &^= syscall.ICANON | syscall.ECHO + + // Set minimum characters when reading = 1 char + attr.Cc[syscall.VMIN] = 1 + + // set timeout when reading as non-canonical mode + attr.Cc[syscall.VTIME] = 0 + + // reflects the changed settings + unix.IoctlSetTermios(int(f.Fd()), unix.TIOCSETA, attr) +} + func (dev *VirtioSerial) AddToVirtualMachineConfig(vmConfig *vzVirtualMachineConfiguration) error { - if dev.LogFile == "" { - return fmt.Errorf("missing mandatory 'logFile' option for virtio-serial device") + if dev.LogFile != "" { + log.Infof("Adding virtio-serial device (logFile: %s)", dev.LogFile) + } + if dev.UsesStdio { + log.Infof("Adding stdio console") } - log.Infof("Adding virtio-serial device (logFile: %s)", dev.LogFile) - //serialPortAttachment := vz.NewFileHandleSerialPortAttachment(os.Stdin, tty) - serialPortAttachment, err := vz.NewFileSerialPortAttachment(dev.LogFile, false) + var serialPortAttachment vz.SerialPortAttachment + var err error + if dev.UsesStdio { + setRawMode(os.Stdin) + serialPortAttachment, err = vz.NewFileHandleSerialPortAttachment(os.Stdin, os.Stdout) + } else { + serialPortAttachment, err = vz.NewFileSerialPortAttachment(dev.LogFile, false) + } if err != nil { return err } + consoleConfig, err := vz.NewVirtioConsoleDeviceSerialPortConfiguration(serialPortAttachment) if err != nil { return err