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).

Also, make a slight change to .gitignore - the ./bin/ ignore
pattern was not ignoring the bin/ directory for me (removing the
leading . fixes things).

Signed-off-by: Matthew Heon <[email protected]>
  • Loading branch information
mheon committed Sep 29, 2023
1 parent 97028a6 commit c22484d
Show file tree
Hide file tree
Showing 104 changed files with 14,336 additions and 213 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
./bin/
/bin/
capture.pcap
tmp/
test/qcon.log
23 changes: 23 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
hvsockShares map[string]string

Check failure on line 49 in cmd/gvproxy/main.go

View workflow job for this annotation

GitHub Actions / lint

var `hvsockShares` is unused (unused)
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,20 @@ func main() {
}
}

// Verify syntax of requested volume shares
hvsockShares := make(map[string]string, 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 vsock guid"))
}

path := strings.Join(splitPath[:len(splitPath)-1], ":")

hvsockShares[path] = splitPath[len(splitPath)-1]
}

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

// Start shares
if err := fileserver.StartShares(hvsockShares); 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.

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

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

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

Check failure on line 11 in pkg/fileserver/plan9/serve.go

View workflow job for this annotation

GitHub Actions / lint

File is not `gofmt`-ed with `-s` (gofmt)
"github.com/pkg/errors"
)

type Plan9Server struct {

Check warning on line 15 in pkg/fileserver/plan9/serve.go

View workflow job for this annotation

GitHub Actions / lint

exported: type name will be used as plan9.Plan9Server by other packages, and that stutters; consider calling this Server (revive)
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, errors.Wrapf(err, "cannot stat path to expose to machine")
}
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, errors.Wrapf(err, "starting 9p server")
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]string) error {

Check warning on line 10 in pkg/fileserver/server_unsupported.go

View workflow job for this annotation

GitHub Actions / lint

unused-parameter: parameter 'mounts' seems to be unused, consider removing or renaming it as _ (revive)
return fmt.Errorf("this platform does not support sharing directories")
}
61 changes: 61 additions & 0 deletions pkg/fileserver/server_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package fileserver

import (
"github.com/containers/gvisor-tap-vsock/pkg/fileserver/plan9"
"github.com/linuxkit/virtsock/pkg/hvsock"
"github.com/sirupsen/logrus"
"github.com/pkg/errors"
)

// 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 GUID.
// 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 accept things
// other than a vsock and support non-Windows OSes.
func StartShares(mounts map[string]string) (defErr error) {
for path, guid := range mounts {
service, err := hvsock.GUIDFromString(guid)
if err != nil {
return errors.Wrapf(err, "parsing vsock guid %s: %w", guid)
}

listener, err := hvsock.Listen(hvsock.Addr{
VMID: hvsock.GUIDWildcard,
ServiceID: service,
})
if err != nil {
return errors.Wrapf(err, "retrieving listener for vsock %s: %w", guid)
}

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

server, err := plan9.New9pServer(listener, path)
if err != nil {
return errors.Wrapf(err, "serving directory %s on vsock %s: %w", path, guid)
}
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 c22484d

Please sign in to comment.