Skip to content

Commit

Permalink
Add support for volume sharing on gvproxy for Windows
Browse files Browse the repository at this point in the history
Specifically, gvproxy will act as a 9p server, serving shares
requested by the caller on HyperV vsocks, to allow the guest VM
to access content on the host.

The vsocks are intended to be managed by the caller in this
model. As such, gvproxy receives the path to be shared and a
vsock port number to share it on via CLI. An arbitrary number of
these are accepted, as each share needs a separate server and
vsock (they will be mounted by the Linux kernel 9p code within
the guest VM, which does not support multiplexing multiple shares
over a single vsock).

Signed-off-by: Matthew Heon <[email protected]>
  • Loading branch information
mheon committed Sep 27, 2023
1 parent 97028a6 commit 2bdf976
Show file tree
Hide file tree
Showing 104 changed files with 14,628 additions and 212 deletions.
27 changes: 27 additions & 0 deletions cmd/gvproxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"syscall"
"time"

"github.com/containers/gvisor-tap-vsock/pkg/fileserver"
"github.com/containers/gvisor-tap-vsock/pkg/net/stdio"
"github.com/containers/gvisor-tap-vsock/pkg/sshclient"
"github.com/containers/gvisor-tap-vsock/pkg/transport"
Expand Down Expand Up @@ -44,6 +45,8 @@ var (
forwardIdentify arrayFlags
sshPort int
pidFile string
shareVolumes arrayFlags
vsockShares map[string]uint64
exitCode int
)

Expand All @@ -70,6 +73,7 @@ func main() {
flag.Var(&forwardUser, "forward-user", "SSH user to use for unix socket forward")
flag.Var(&forwardIdentify, "forward-identity", "Path to SSH identity key for forwarding")
flag.StringVar(&pidFile, "pid-file", "", "Generate a file with the PID in it")
flag.Var(&shareVolumes, "share-volume", "Share a volume to the guest virtual machine over 9p")
flag.Parse()

ctx, cancel := context.WithCancel(context.Background())
Expand Down Expand Up @@ -161,6 +165,24 @@ func main() {
}
}

// Verify syntax of requested volume shares
vsockShares := make(map[string]uint64, len(shareVolumes))
for i := 0; i < len(shareVolumes); i++ {
splitPath := strings.Split(shareVolumes[i], ":")

if len(splitPath) < 2 {
exitWithError(errors.New("Share paths passed to --share-volume must include a port number"))
}

path := strings.Join(splitPath[:len(splitPath)-1], ":")
vsockNum, err := strconv.ParseUint(splitPath[len(splitPath)-1], 10, 64)
if err != nil {
exitWithError(errors.Wrapf(err, "Could not parse port number for --share-volume flag"))
}

vsockShares[path] = vsockNum
}

// Create a PID file if requested
if len(pidFile) > 0 {
f, err := os.Create(pidFile)
Expand All @@ -179,6 +201,11 @@ func main() {
}
}

// Start shares
if err := fileserver.StartShares(vsockShares); err != nil {
exitWithError(err)
}

config := types.Configuration{
Debug: debug,
CaptureFile: captureFile(),
Expand Down
9 changes: 6 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ require (
github.com/coreos/stream-metadata-go v0.4.3
github.com/dustin/go-humanize v1.0.1
github.com/google/gopacket v1.1.19
github.com/insomniacslk/dhcp v0.0.0-20220504074936-1ca156eafb9f
github.com/hugelgupf/p9 v0.3.1-0.20230822151754-54f5c5530921
github.com/insomniacslk/dhcp v0.0.0-20230731140434-0f9eb93a696c
github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2
github.com/mdlayher/vsock v1.2.1
github.com/miekg/dns v1.1.56
Expand All @@ -34,11 +35,13 @@ require (
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/pierrec/lz4/v4 v4.1.18 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/text v0.13.0 // indirect
Expand Down
79 changes: 32 additions & 47 deletions go.sum

Large diffs are not rendered by default.

94 changes: 94 additions & 0 deletions pkg/fileserver/plan9/serve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package plan9

import (
"fmt"
"net"
"os"
"path/filepath"

"github.com/hugelgupf/p9/fsimpl/localfs"
"github.com/hugelgupf/p9/p9"
"github.com/sirupsen/logrus"
)

type Plan9Server struct {
server *p9.Server
// TODO: Once server has a proper Close() we don't need this.
// This is basically just a short-circuit to actually close the server
// without that ability.
listener net.Listener
// Errors from the server being started will come out here.
errChan chan error
}

// Expose a single directory (and all children) via the given net.Listener.
// Directory given must be an absolute path and must exist.
func New9pServer(listener net.Listener, exposeDir string) (*Plan9Server, error) {
// Verify that exposeDir makes sense.
if !filepath.IsAbs(exposeDir) {
return nil, fmt.Errorf("path to expose to machine must be absolute: %s", exposeDir)
}
stat, err := os.Stat(exposeDir)
if err != nil {
return nil, fmt.Errorf("cannot stat path to expose to machine: %w", err)
}
if !stat.IsDir() {
return nil, fmt.Errorf("path to expose to machine must be a directory: %s", exposeDir)
}

server := p9.NewServer(localfs.Attacher(exposeDir), []p9.ServerOpt{}...)
if server == nil {
return nil, fmt.Errorf("p9.NewServer returned nil")
}

errChan := make(chan error)

// TODO: Use a channel to pass back this if it occurs within a
// reasonable timeframe.
go func() {
errChan <- server.Serve(listener)
close(errChan)
}()

toReturn := new(Plan9Server)
toReturn.listener = listener
toReturn.server = server
toReturn.errChan = errChan

// Just before returning, check to see if we got an error off server
// startup.
select {
case err := <-errChan:
return nil, fmt.Errorf("starting 9p server: %w", err)
default:
logrus.Infof("Successfully started 9p server for directory %s", exposeDir)
}

return toReturn, nil
}

// Stop a running server.
// Please note that this does *BAD THINGS* to clients if they are still running
// when the server stops. Processes get stuck in I/O deep sleep and zombify, and
// nothing I do save restarting the VM can remove the zombies.
func (s *Plan9Server) Stop() error {
if s.server != nil {
if err := s.listener.Close(); err != nil {
return err
}
s.server = nil
}

return nil
}

// Wait for an error from a running server.
func (s *Plan9Server) WaitForError() error {
if s.server != nil {
err := <-s.errChan
return err
}

// Server already down, return nil
return nil
}
12 changes: 12 additions & 0 deletions pkg/fileserver/server_unsupported.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//go:build !windows
// +build !windows

package fileserver

import (
"fmt"
)

func StartShares(mounts map[string]uint64) error {
return fmt.Errorf("this platform does not support sharing directories")
}
59 changes: 59 additions & 0 deletions pkg/fileserver/server_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package fileserver

import (
"fmt"

"github.com/containers/gvisor-tap-vsock/pkg/vsock"
"github.com/containers/gvisor-tap-vsock/pkg/fileserver/plan9"
"github.com/sirupsen/logrus"
)

// Start serving the given shares on Windows HVSocks for use by a Hyper-V VM.
// Mounts is formatted as a map of directory to be shared to vsock port number.
// The vsocks used must already be defined before StartShares is called; it's
// expected that the vsocks will be created and torn down by the program calling
// gvproxy.
// TODO: The map here probably doesn't make sense.
// In the future, possibly accept a struct instead, so we can map to things
// other than a vsock.
func StartShares(mounts map[string]uint64) (defErr error) {
for path, vsockNum := range mounts {
hvsock, err := vsock.LoadHVSockRegistryEntry(vsockNum)
if err != nil {
return fmt.Errorf("retrieving vsock %d for share %s: %w", vsockNum, path, err)
}

listener, err := hvsock.Listener()
if err != nil {
return fmt.Errorf("retrieving listener for vsock %d (share %s): %w", vsockNum, path, err)
}

logrus.Debugf("Going to serve directory %s on vsock port %d", path, vsockNum)

server, err := plan9.New9pServer(listener, path)
if err != nil {
return fmt.Errorf("serving directory %s on vsock %d: %w", path, vsockNum, err)
}
defer func() {
if defErr != nil {
if err := server.Stop(); err != nil {
logrus.Errorf("Error stopping 9p server: %v", err)
}
}
}()

serverDir := path

go func() {
if err := server.WaitForError(); err != nil {
logrus.Errorf("Error from 9p server for %s: %v", path, err)
} else {
// We do not expect server exits - this should
// run until the program exits.
logrus.Warnf("9p server for %s exited without error", serverDir)
}
}()
}

return nil
}
Loading

0 comments on commit 2bdf976

Please sign in to comment.