Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions cmd/crc/cmd/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"os"
"os/signal"
"regexp"
"runtime"
"syscall"
"time"

Expand All @@ -24,6 +25,7 @@ import (
"github.com/crc-org/crc/v2/pkg/crc/constants"
"github.com/crc-org/crc/v2/pkg/crc/daemonclient"
"github.com/crc-org/crc/v2/pkg/crc/logging"
"github.com/crc-org/crc/v2/pkg/fileserver/fs9p"
"github.com/crc-org/machine/libmachine/drivers"
"github.com/docker/go-units"
"github.com/gorilla/handlers"
Expand Down Expand Up @@ -177,7 +179,7 @@ func run(configuration *types.Configuration) error {
}
}()

ln, err := vn.Listen("tcp", fmt.Sprintf("%s:80", configuration.GatewayIP))
ln, err := vn.Listen("tcp", net.JoinHostPort(configuration.GatewayIP, "80"))
if err != nil {
return err
}
Expand All @@ -193,7 +195,7 @@ func run(configuration *types.Configuration) error {
}
}()

networkListener, err := vn.Listen("tcp", fmt.Sprintf("%s:80", hostVirtualIP))
networkListener, err := vn.Listen("tcp", net.JoinHostPort(hostVirtualIP, "80"))
if err != nil {
return err
}
Expand Down Expand Up @@ -248,6 +250,55 @@ func run(configuration *types.Configuration) error {
}
}()

// 9p home directory sharing
if runtime.GOOS == "windows" && config.Get(crcConfig.EnableSharedDirs).AsBool() {
// 9p over hvsock
listener9pHvsock, err := fs9p.GetHvsockListener(constants.Plan9HvsockGUID)
if err != nil {
return err
}
server9pHvsock, err := fs9p.New9pServer(listener9pHvsock, constants.GetHomeDir())
if err != nil {
return err
}
if err := server9pHvsock.Start(); err != nil {
return err
}
defer func() {
if err := server9pHvsock.Stop(); err != nil {
logging.Warnf("error stopping 9p server (hvsock): %v", err)
}
}()
go func() {
if err := server9pHvsock.WaitForError(); err != nil {
logging.Errorf("9p server (hvsock) error: %v", err)
}
}()

// 9p over TCP (as a backup)
listener9pTCP, err := vn.Listen("tcp", net.JoinHostPort(configuration.GatewayIP, fmt.Sprintf("%d", constants.Plan9TcpPort)))
if err != nil {
return err
}
server9pTCP, err := fs9p.New9pServer(listener9pTCP, constants.GetHomeDir())
if err != nil {
return err
}
if err := server9pTCP.Start(); err != nil {
return err
}
defer func() {
if err := server9pTCP.Stop(); err != nil {
logging.Warnf("error stopping 9p server (tcp): %v", err)
}
}()
go func() {
if err := server9pTCP.WaitForError(); err != nil {
logging.Errorf("9p server (tcp) error: %v", err)
}
}()
}

startupDone()

if logging.IsDebug() {
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.24.2

require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/DeedleFake/p9 v0.7.1
github.com/Masterminds/semver/v3 v3.4.0
github.com/Microsoft/go-winio v0.6.2
github.com/ProtonMail/go-crypto v1.3.0
Expand All @@ -27,6 +28,7 @@ require (
github.com/klauspost/compress v1.18.1
github.com/klauspost/cpuid/v2 v2.3.0
github.com/kofalt/go-memoize v0.0.0-20220914132407-0b5d6a304579
github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2
github.com/mattn/go-colorable v0.1.14
github.com/mdlayher/vsock v1.2.1
github.com/onsi/ginkgo/v2 v2.27.2
Expand Down Expand Up @@ -130,7 +132,6 @@ require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect
github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DeedleFake/p9 v0.7.1 h1:HS4iPmbxR32pmtUyjNxXIf7eFih07Rxp1E0R6fqA7ik=
github.com/DeedleFake/p9 v0.7.1/go.mod h1:xFOErTOZm4lVysPCTrRtTm1c8J0Pfg0jGEC+re1AnIo=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
Expand Down
6 changes: 6 additions & 0 deletions packaging/windows/product.wxs.template
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@
<RegistryValue Type="string" Name="ElementName" Value="gvisor-tap-vsock" KeyPath="yes"/>
</RegistryKey>
</Component>
<Component Id="Hvsock9pRegistryEntry" Guid="*">
<RegistryKey Root="HKLM" Key="Software\Microsoft\Windows NT\CurrentVersion\Virtualization\GuestCommunicationServices\00009000-FACB-11E6-BD58-64006A7986D3">
<RegistryValue Type="string" Name="ElementName" Value="fs9p-hvsock" KeyPath="yes"/>
</RegistryKey>
</Component>
</Directory>
</Directory>
</Directory>
Expand Down Expand Up @@ -130,6 +135,7 @@
<ComponentRef Id="AddToPath"/>
<ComponentRef Id="AddUserToCrcUsers" />
<ComponentRef Id="VsockRegistryEntry" />
<ComponentRef Id="Hvsock9pRegistryEntry" />
</Feature>
<UI>
<UIRef Id="WixUI_ErrorProgressText"/>
Expand Down
5 changes: 5 additions & 0 deletions pkg/crc/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ const (
OpenShiftIngressHTTPSPort = 443

BackgroundLauncherExecutable = "crc-background-launcher.exe"

Plan9Msize = 1024 * 1024
Plan9TcpPort = 564
Plan9HvsockGUID = "00009000-FACB-11E6-BD58-64006A7986D3"
Plan9HvsockPort = 36864
)

var adminHelperExecutableForOs = map[string]string{
Expand Down
9 changes: 8 additions & 1 deletion pkg/crc/machine/libhvee/driver_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@ func configureShareDirs(machineConfig config.MachineConfig) []drivers.SharedDir
Type: "cifs",
Username: machineConfig.SharedDirUsername,
}
sharedDirs = append(sharedDirs, sharedDir)
sharedDir9p := drivers.SharedDir{
Source: dir,
Target: convertToUnixPath(dir) + "9p", // temporary solution until smb sharing is removed
Tag: "crc-dir0", // same as above
//Tag: fmt.Sprintf("dir%d", i),
Type: "9p",
}
sharedDirs = append(sharedDirs, sharedDir, sharedDir9p)
Comment thread
redbeam marked this conversation as resolved.
}
return sharedDirs
}
15 changes: 15 additions & 0 deletions pkg/crc/machine/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ func configureSharedDirs(vm *virtualMachine, sshRunner *crcssh.Runner) error {
if _, _, err := sshRunner.RunPrivileged(fmt.Sprintf("Mounting %s", mount.Target), "mount", "-o", "context=\"system_u:object_r:container_file_t:s0\"", "-t", mount.Type, mount.Tag, mount.Target); err != nil {
return err
}

case "cifs":
smbUncPath := fmt.Sprintf("//%s/%s", hostVirtualIP, mount.Tag)
if _, _, err := sshRunner.RunPrivate("sudo", "mount", "-o", fmt.Sprintf("rw,uid=core,gid=core,username='%s',password='%s'", mount.Username, mount.Password), "-t", mount.Type, smbUncPath, mount.Target); err != nil {
Expand All @@ -257,6 +258,20 @@ func configureSharedDirs(vm *virtualMachine, sshRunner *crcssh.Runner) error {
}
return fmt.Errorf("Failed to mount CIFS/SMB share '%s' please make sure configured password is correct: %w", mount.Tag, err)
}

case "9p":
// change owner to core user to allow mounting to it as a non-root user
if _, _, err := sshRunner.RunPrivileged("Changing owner of mount directory", "chown", "core:core", mount.Target); err != nil {
return err
}
if _, _, err := sshRunner.Run("9pfs -V -p", fmt.Sprintf("%d", constants.Plan9HvsockPort), "2", mount.Target); err != nil {
logging.Warnf("Failed to connect to 9p server over hvsock: %v", err)
logging.Warnf("Falling back to 9p over TCP")
if _, _, err := sshRunner.Run("9pfs", constants.VSockGateway, mount.Target); err != nil {
return err
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

default:
return fmt.Errorf("Unknown Shared dir type requested: %s", mount.Type)
}
Expand Down
98 changes: 98 additions & 0 deletions pkg/fileserver/fs9p/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package fs9p

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

"github.com/DeedleFake/p9"
"github.com/DeedleFake/p9/proto"
"github.com/crc-org/crc/v2/pkg/crc/constants"
"github.com/sirupsen/logrus"
)

type Server struct {
// Listener this server is bound to
Listener net.Listener

// Plan9 Filesystem type that holds the exposed directory
Filesystem p9.FileSystem

// Directory this server exposes
ExposedDir string

// Errors from the server being started will come out here
ErrChan chan error
}

// New9pServer exposes a single directory (and all children) via the given net.Listener
// and returns the server struct.
// Directory given must be an absolute path and must exist.
func New9pServer(listener net.Listener, exposeDir string) (*Server, 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)
}

fs := p9.FileSystem(p9.Dir(exposeDir))
// set size to 1 making channel buffered to prevent proto.Serve blocking
errChan := make(chan error, 1)

toReturn := new(Server)
toReturn.Listener = listener
toReturn.Filesystem = fs
toReturn.ExposedDir = exposeDir
toReturn.ErrChan = errChan

Comment thread
coderabbitai[bot] marked this conversation as resolved.
return toReturn, nil
}

// Start a server created by New9pServer.
func (s *Server) Start() error {
go func() {
s.ErrChan <- proto.Serve(s.Listener, p9.Proto(), p9.FSConnHandler(s.Filesystem, constants.Plan9Msize))
close(s.ErrChan)
}()

// Just before returning, check to see if we got an error off server startup.
select {
case err := <-s.ErrChan:
return fmt.Errorf("starting 9p server: %w", err)
default:
logrus.Infof("started 9p server on %s for directory %s", s.Listener.Addr().String(), s.ExposedDir)
return 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 other than restarting the VM can remove the zombies.
func (s *Server) Stop() error {
if err := s.Listener.Close(); err != nil {
return err
}
logrus.Infof("stopped 9p server for directory %s", s.ExposedDir)
return nil
}
Comment thread
redbeam marked this conversation as resolved.

// WaitForError from a running server.
func (s *Server) WaitForError() error {
err := <-s.ErrChan
// captures "accept tcp: endpoint is in invalid state" errors on exit
var opErr *net.OpError
if errors.As(err, &opErr) && strings.Contains(opErr.Error(), "endpoint is in invalid state") {
return nil
}
return err
}
Comment thread
redbeam marked this conversation as resolved.
15 changes: 15 additions & 0 deletions pkg/fileserver/fs9p/server_fallback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//go:build !windows

package fs9p

import (
"fmt"
"net"
"runtime"
)

// GetHvsockListener returns a net.Listener listening on the specified hvsock.
// The used hvsock must already be defined before this function is called.
func GetHvsockListener(hvsockGUID string) (net.Listener, error) {
return nil, fmt.Errorf("GetHvsockListener() not implemented on %s", runtime.GOOS)
}
29 changes: 29 additions & 0 deletions pkg/fileserver/fs9p/server_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//go:build windows

package fs9p

import (
"fmt"
"net"

"github.com/linuxkit/virtsock/pkg/hvsock"
)

// GetHvsockListener returns a net.Listener listening on the specified hvsock.
// The used hvsock must already be defined before this function is called.
func GetHvsockListener(hvsockGUID string) (net.Listener, error) {
service, err := hvsock.GUIDFromString(hvsockGUID)
if err != nil {
return nil, fmt.Errorf("parsing hvsock guid %s: %w", hvsockGUID, err)
}

listener, err := hvsock.Listen(hvsock.Addr{
VMID: hvsock.GUIDWildcard,
ServiceID: service,
})
if err != nil {
return nil, fmt.Errorf("retrieving listener for hvsock %s: %w", hvsockGUID, err)
}

return listener, nil
}
12 changes: 12 additions & 0 deletions test/e2e/features/9pfs.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@story_9pfs @windows
Feature: Verify 9pfs mount works

Verify 9pfs mount of user's home directory is mounted, accessible
and that basic file operations work. Only relevant on Windows.

Scenario: Test mounted directory functionality
Given directory "/mnt/c/Users/core" exists in VM
And filesystem is mounted
Then listing files in directory "/mnt/c/Users/core" should succeed
And basic file operations in directory "/mnt/c/Users/core" should succeed
And basic directory operations in directory "/mnt/c/Users/core" should succeed
Comment on lines +7 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider parameterizing the hardcoded mount path.

The scenario hardcodes /mnt/c/Users/core throughout. While this may be correct for the current Windows VM configuration, consider:

  1. Making the path configurable via scenario variables or environment-based selection
  2. Updating line 9 to verify the filesystem is mounted at the specific expected path (currently it just checks if any 9pfs filesystem is mounted - see related issue in testsuite.go:1376-1385)

Example improvement for line 9:

-        And filesystem is mounted
+        And filesystem is mounted at "/mnt/c/Users/core"

This would require updating the step definition as suggested in the testsuite.go review.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In test/e2e/features/9pfs.feature around lines 7 to 12, the scenario hardcodes
the mount path "/mnt/c/Users/core" and the "filesystem is mounted" step only
checks for any 9pfs mount; make the path a scenario parameter (replace hardcoded
path with a variable like "<mount_path>") and update the step on line 9 to
assert that a 9pfs filesystem is mounted specifically at that expected path
rather than anywhere; modify the corresponding step definition in testsuite.go
(around lines 1376–1385) to accept the mount path parameter and perform an
exact-path mount check, and ensure the feature file declares the variable or
reads it from the environment.

Loading
Loading