Skip to content

Commit

Permalink
Initial addition of 9p code to Podman
Browse files Browse the repository at this point in the history
Server logic lives in gvisor-tap-vsock (specifically gvproxy),
where it will live through the entire life of the VM, unlike any
of our `podman machine` commands. Client logic lives in a new,
hidden Podman command (`podman system client9`) that makes a
connection to the server and then uses the kernel 9p mount code
to complete the mount. Unfortunately we can't just directly call
mount, as it doesn't support the connection type we're using;
instead we need to make the connection ourselves and mount using
FDs.

The initial PR only works over Hyper-V vsocks, but the actual 9p
protocol should work over just about anything so this can easily
be generalized to work elsewhere if necessary.

The client code (which is staying in Podman) is unfortunately a
lot more gross than I want it to be, but I don't see an easy way
around that. At the very least, it does avoid the need for us to
maintain our own FUSE filesystem by using the kernel's 9p mount
code (which looks like it's the same code QEMU uses, which is
promising in terms of maintenance).

Signed-off-by: Matthew Heon <matthew.heon@pm.me>
  • Loading branch information
mheon committed Sep 29, 2023
1 parent 5e8c75e commit 334e213
Show file tree
Hide file tree
Showing 39 changed files with 3,903 additions and 3 deletions.
42 changes: 42 additions & 0 deletions cmd/podman/system/9client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package system

import (
"fmt"
"strconv"

"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v4/cmd/podman/registry"
"github.com/spf13/cobra"
)

var (
client9Command = &cobra.Command{
Args: cobra.ExactArgs(2),
Use: "client9",
Short: "Connect to a 9p + vsock connection",
Long: "Connect to a 9p + vsock connection.",
RunE: remoteDirClient,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman system client9`,
}
)

func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: client9Command,
Parent: systemCmd,
})
}

func remoteDirClient(cmd *cobra.Command, args []string) error {
port, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("error parsing port number: %w", err)
}

if err := client9p(uint32(port), args[1]); err != nil {
return err
}

return nil
}
100 changes: 100 additions & 0 deletions cmd/podman/system/9p_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package system

import (
"fmt"
"bytes"
"os"
"os/exec"
"path/filepath"

"github.com/mdlayher/vsock"
"github.com/sirupsen/logrus"
)

// This is Linux-only as we only intend for this function to be used inside the
// `podman machine` VM, which is guaranteed to be Linux.
func client9p(portNum uint32, mountPath string) error {
cleanPath, err := filepath.Abs(mountPath)
if err != nil {
return fmt.Errorf("absolute path for %s: %w", mountPath, err)
}
mountPath = cleanPath

// Mountpath needs to exist and be a directory
stat, err := os.Stat(mountPath)
if err != nil {
return fmt.Errorf("stat %s: %w", mountPath, err)
}
if !stat.IsDir() {
return fmt.Errorf("path %s is not a directory", mountPath)
}

logrus.Infof("Going to mount 9p on vsock port %d to directory %s", portNum, mountPath)

// Host connects to non-hypervisor processes on the host running the VM.
conn, err := vsock.Dial(vsock.Host, portNum, nil)
if err != nil {
return fmt.Errorf("dialing vsock port %d: %w", portNum, err)
}
defer func() {
if err := conn.Close(); err != nil {
logrus.Errorf("Error closing vsock: %w", err)
}
}()

// vsock doesn't give us direct access to the underlying FD. That's kind
// of inconvenient, because we have to pass it off to mount.
// However, it does give us the ability to get a syscall.RawConn, which
// has a method that allows us to run a function that takes the FD
// number as an argument.
// Which ought to be good enough? Probably?
// Overall, this is gross and I hate it, but I don't see a better way.
rawConn, err := conn.SyscallConn()
if err != nil {
return fmt.Errorf("getting vsock raw conn: %w", err)
}
errChan := make(chan error)
runMount := func(fd uintptr) {
var (
stdout bytes.Buffer
stderr bytes.Buffer
vsock *os.File
)

vsock = os.NewFile(fd, "vsock")
if vsock == nil {
errChan <- fmt.Errorf("could not convert vsock fd to os.File")
return
}

// This is ugly, but it lets us use real kernel mount code,
// instead of maintaining our own FUSE 9p implementation.
cmd := exec.Command("mount", "-t", "9p", "-o", "trans=fd,rfdno=3,wfdno=3,version=9p2000.L", "9p", mountPath)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.ExtraFiles = []*os.File{vsock}

err := cmd.Run()
logrus.Debugf("Mount stdout: %s", stdout.String())
if err != nil {
logrus.Debugf("Mount stderr: %s", stderr.String())
logrus.Infof("Mounted directory %s using 9p", mountPath)
} else {
logrus.Errorf("Error mounting directory %s. Mount stderr: %s", mountPath, stderr.String())
}

errChan <- err
close(errChan)
}
if err := rawConn.Control(runMount); err != nil {
return fmt.Errorf("running mount function for dir %s: %w", mountPath, err)
}

if err := <-errChan; err != nil {
return fmt.Errorf("mounting filesystem %s: %w", mountPath, err)
}

logrus.Infof("Mount of filesystem %s successful", mountPath)

return nil
}
12 changes: 12 additions & 0 deletions cmd/podman/system/9p_unsupported.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//go+build !linux
// +build !linux

package system

import (
"fmt"
)

func client9p(socketNum uint32, mountPath string) error {
return fmt.Errorf("unsupported on this OS")
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ require (
github.com/json-iterator/go v1.1.12
github.com/mattn/go-shellwords v1.0.12
github.com/mattn/go-sqlite3 v1.14.17
github.com/mdlayher/vsock v1.2.1
github.com/moby/term v0.5.0
github.com/nxadm/tail v1.4.8
github.com/onsi/ginkgo/v2 v2.12.0
Expand Down Expand Up @@ -144,6 +145,7 @@ require (
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,10 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ=
github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE=
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
Expand Down
109 changes: 109 additions & 0 deletions pkg/machine/hyperv/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"path/filepath"
"time"

"github.com/Microsoft/go-winio"
"github.com/containers/common/pkg/config"
"github.com/containers/libhvee/pkg/hypervctl"
"github.com/containers/podman/v4/pkg/machine"
Expand Down Expand Up @@ -64,6 +65,9 @@ type HyperVMachine struct {
LastUp time.Time
// GVProxy will write its PID here
GvProxyPid machine.VMFile
// MountVsocks contains the currently-active vsocks, mapped to the
// directory they should be mounted on.
MountVsocks map[string]uint64
}

// addNetworkAndReadySocketsToRegistry adds the Network and Ready sockets to the
Expand Down Expand Up @@ -354,6 +358,11 @@ func (m *HyperVMachine) Remove(_ string, opts machine.RemoveOptions) (string, fu
}
}

// Tear down vsocks
if err := m.removeShares(); err != nil {
logrus.Errorf("Error removing vsock: %w", err)
}

vm, err = vmm.GetMachine(m.Name)
if err != nil {
return "", nil, err
Expand Down Expand Up @@ -453,6 +462,16 @@ func (m *HyperVMachine) SSH(name string, opts machine.SSHOptions) error {
}

func (m *HyperVMachine) Start(name string, opts machine.StartOptions) error {
// Start 9p shares
shares, err := m.createShares()
if err != nil {
return err
}
m.MountVsocks = shares
if err := m.writeConfig(); err != nil {
return err
}

vmm := hypervctl.NewVirtualMachineManager()
vm, err := vmm.GetMachine(m.Name)
if err != nil {
Expand All @@ -473,6 +492,10 @@ func (m *HyperVMachine) Start(name string, opts machine.StartOptions) error {
return err
}

if err := m.startShares(); err != nil {
return err
}

if m.HostUser.Modified {
if machine.UpdatePodmanDockerSockService(m, name, m.UID, m.Rootful) == nil {
// Reset modification state if there are no errors, otherwise ignore errors
Expand Down Expand Up @@ -627,6 +650,15 @@ func (m *HyperVMachine) startHostNetworking() (string, machine.APIForwardingStat
cmd = append(cmd, []string{"-ssh-port", fmt.Sprintf("%d", m.Port)}...)
cmd = append(cmd, []string{"-listen", fmt.Sprintf("vsock://%s", m.NetworkHVSock.KeyName)}...)
cmd = append(cmd, "-pid-file", m.GvProxyPid.GetPath())
// Add all shares
for mountpoint, vsock := range m.MountVsocks {
for _, mount := range m.Mounts {
if mount.Target == mountpoint {
cmd = append(cmd, "-share-volume", fmt.Sprintf("%s:%s", mount.Source, winio.VsockServiceID(uint32(vsock))))
break
}
}
}

cmd, forwardSock, state = m.setupAPIForwarding(cmd)
if logrus.GetLevel() == logrus.DebugLevel {
Expand Down Expand Up @@ -692,3 +724,80 @@ func (m *HyperVMachine) setRootful(rootful bool) error {
m.HostUser.Modified = true
return nil
}

func (m *HyperVMachine) createShares() (_ map[string]uint64, defErr error) {
toReturn := make(map[string]uint64)

for _, mount := range m.Mounts {
var vsock *HVSockRegistryEntry

vsockNum, ok := m.MountVsocks[mount.Target]
if ok {
// Ignore errors here, we'll just try and recreate the
// vsock below.
testVsock, err := LoadHVSockRegistryEntry(vsockNum)
if err == nil {
vsock = testVsock
}
}
if vsock == nil {
testVsock, err := NewHVSockRegistryEntry(m.Name, Fileserver)
if err != nil {
return nil, err
}
defer func() {
if defErr != nil {
if err := testVsock.Remove(); err != nil {
logrus.Errorf("Removing vsock: %v", err)
}
}
}()
vsock = testVsock
}

toReturn[mount.Target] = vsock.Port
}

return toReturn, nil
}

func (m *HyperVMachine) removeShares() error {
var removalErr error

for _, mount := range m.Mounts {
vsockNum, ok := m.MountVsocks[mount.Target]
if !ok {
// Mount doesn't have a valid vsock, no need to tear down
continue
}

vsock, err := LoadHVSockRegistryEntry(vsockNum)
if err != nil {
logrus.Debugf("Vsock %d for mountpoint %s does not have a valid registry entry, skipping removal", vsockNum, mount.Target)
continue
}

if err := vsock.Remove(); err != nil {
if removalErr != nil {
logrus.Errorf("Error removing vsock: %w", removalErr)
}
removalErr = fmt.Errorf("removing vsock %d for mountpoint %s: %w", vsockNum, mount.Target, err)
}
}

return removalErr
}

func (m *HyperVMachine) startShares() error {
for mountpoint, sockNum := range m.MountVsocks {
sshOpts := machine.SSHOptions{
Args: []string{"-q", "--", "sudo", "podman", "system", "client9", fmt.Sprintf("%d", sockNum), mountpoint},
}

if err := m.SSH(m.Name, sshOpts); err != nil {
return err
}
}

return nil
}
Loading

0 comments on commit 334e213

Please sign in to comment.