From 854bf5811df46ff1f8a35cdaf6398dd45f2a542c Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Tue, 19 May 2026 02:46:40 +0000 Subject: [PATCH] fix(wireguard): skip tun device checks when using kernelspace --- cmd/gluetun/main.go | 25 +------- internal/tun/check.go | 41 ------------- internal/tun/check_unspecified.go | 7 --- internal/tun/create.go | 50 --------------- internal/tun/create_unspecified.go | 8 --- internal/tun/tun.go | 99 +++++++++++++++++++++++++++++- internal/tun/tun_test.go | 14 ++--- internal/tun/tun_unspecified.go | 12 ++++ internal/vpn/amneziawg.go | 6 ++ internal/vpn/openvpn.go | 6 ++ internal/wireguard/run.go | 12 ++++ 11 files changed, 140 insertions(+), 140 deletions(-) delete mode 100644 internal/tun/check.go delete mode 100644 internal/tun/check_unspecified.go delete mode 100644 internal/tun/create.go delete mode 100644 internal/tun/create_unspecified.go create mode 100644 internal/tun/tun_unspecified.go diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index d67d3522..97209889 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "errors" "fmt" "io/fs" "net/http" @@ -43,7 +42,6 @@ import ( "github.com/qdm12/gluetun/internal/server" "github.com/qdm12/gluetun/internal/shadowsocks" "github.com/qdm12/gluetun/internal/storage" - "github.com/qdm12/gluetun/internal/tun" updater "github.com/qdm12/gluetun/internal/updater/loop" "github.com/qdm12/gluetun/internal/updater/resolver" "github.com/qdm12/gluetun/internal/updater/unzip" @@ -80,7 +78,6 @@ func main() { logger := log.New(log.SetLevel(log.LevelInfo)) args := os.Args - tun := tun.New() netLinkDebugLogger := logger.New(log.SetComponent("netlink")) netLinker := netlink.New(netLinkDebugLogger) cli := cli.New() @@ -100,7 +97,7 @@ func main() { errorCh := make(chan error) go func() { - errorCh <- _main(ctx, buildInfo, args, logger, reader, tun, netLinker, cmder, cli) + errorCh <- _main(ctx, buildInfo, args, logger, reader, netLinker, cmder, cli) }() // Wait for OS signal or run error @@ -145,7 +142,7 @@ func main() { //nolint:gocognit,gocyclo,maintidx func _main(ctx context.Context, buildInfo models.BuildInformation, args []string, logger log.LoggerInterface, reader *reader.Reader, - tun Tun, netLinker netLinker, cmder RunStarter, + netLinker netLinker, cmder RunStarter, cli clier, ) error { if len(args) > 1 { // cli operation @@ -342,19 +339,6 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, return fmt.Errorf("adding local rules: %w", err) } - const tunDevice = "/dev/net/tun" - err = tun.Check(tunDevice) - if err != nil { - if !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("checking TUN device: %w (see the Wiki errors/tun page)", err) - } - logger.Info(err.Error() + "; creating it...") - err = tun.Create(tunDevice) - if err != nil { - return fmt.Errorf("creating tun device: %w", err) - } - } - for _, port := range allSettings.Firewall.InputPorts { for _, defaultRoute := range defaultRoutes { err = firewallConf.SetAllowedPort(ctx, port, defaultRoute.NetInterface) @@ -626,11 +610,6 @@ type clier interface { GenKey(args []string) error } -type Tun interface { - Check(tunDevice string) error - Create(tunDevice string) error -} - type RunStarter interface { Run(cmd *exec.Cmd) (output string, err error) Start(cmd *exec.Cmd) (stdoutLines, stderrLines <-chan string, diff --git a/internal/tun/check.go b/internal/tun/check.go deleted file mode 100644 index 14be916c..00000000 --- a/internal/tun/check.go +++ /dev/null @@ -1,41 +0,0 @@ -//go:build linux || darwin - -package tun - -import ( - "errors" - "fmt" - "os" - "syscall" -) - -// Check checks the tunnel device specified by path is present and accessible. -func (t *Tun) Check(path string) error { - f, err := os.OpenFile(path, os.O_RDWR, 0) - if err != nil { - return fmt.Errorf("TUN device is not available: %w", err) - } - defer f.Close() - - info, err := f.Stat() - if err != nil { - return fmt.Errorf("getting stat information for TUN file: %w", err) - } - - sys, ok := info.Sys().(*syscall.Stat_t) - if !ok { - return errors.New("cannot get syscall stat info of TUN file") - } - - const expectedRdev = 2760 // corresponds to major 10 and minor 200 - if sys.Rdev != expectedRdev { - return fmt.Errorf("TUN file has an unexpected rdev: %d instead of expected %d", - sys.Rdev, expectedRdev) - } - - if err := f.Close(); err != nil { - return fmt.Errorf("closing TUN device: %w", err) - } - - return nil -} diff --git a/internal/tun/check_unspecified.go b/internal/tun/check_unspecified.go deleted file mode 100644 index 4ed2f365..00000000 --- a/internal/tun/check_unspecified.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !linux && !darwin - -package tun - -func (t *Tun) Check(path string) error { - panic("not implemented") -} diff --git a/internal/tun/create.go b/internal/tun/create.go deleted file mode 100644 index 5c5c5242..00000000 --- a/internal/tun/create.go +++ /dev/null @@ -1,50 +0,0 @@ -//go:build linux || darwin - -package tun - -import ( - "fmt" - "math" - "os" - "path/filepath" - - "golang.org/x/sys/unix" -) - -// Create creates a TUN device at the path specified. -func (t *Tun) Create(path string) (err error) { - parentDir := filepath.Dir(path) - err = os.MkdirAll(parentDir, 0o751) //nolint:mnd - if err != nil { - return err - } - - const ( - major = 10 - minor = 200 - ) - dev := unix.Mkdev(major, minor) - if dev > math.MaxInt { - panic("dev is too high") - } - err = unix.Mknod(path, unix.S_IFCHR, int(dev)) - if err != nil { - return fmt.Errorf("creating TUN device file node: %w", err) - } - - fd, err := unix.Open(path, 0, 0) - if err != nil { - if err.Error() == "operation not permitted" { - err = fmt.Errorf("%w (did you specify --device /dev/net/tun to your container command?)", err) - } - return fmt.Errorf("unix opening TUN device file: %w", err) - } - - const nonBlocking = true - err = unix.SetNonblock(fd, nonBlocking) - if err != nil { - return fmt.Errorf("setting non block to TUN device file descriptor: %w", err) - } - - return nil -} diff --git a/internal/tun/create_unspecified.go b/internal/tun/create_unspecified.go deleted file mode 100644 index 4378f17d..00000000 --- a/internal/tun/create_unspecified.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build !linux && !darwin - -package tun - -// Create creates a TUN device at the path specified. -func (t *Tun) Create(path string) error { - panic("not implemented") -} diff --git a/internal/tun/tun.go b/internal/tun/tun.go index 6322bc73..9d58ee28 100644 --- a/internal/tun/tun.go +++ b/internal/tun/tun.go @@ -1,7 +1,100 @@ +//go:build linux || darwin + package tun -type Tun struct{} +import ( + "errors" + "fmt" + "math" + "os" + "path/filepath" + "syscall" -func New() *Tun { - return &Tun{} + "golang.org/x/sys/unix" +) + +func Setup() error { + const tunDevice = "/dev/net/tun" + err := check(tunDevice) + switch { + case err == nil: + return nil + case errors.Is(err, os.ErrNotExist): + err = create(tunDevice) + if err != nil { + return fmt.Errorf("creating TUN device: %w", err) + } + return nil + default: + return fmt.Errorf("checking TUN device: %w (see the Wiki errors/tun page)", err) + } +} + +// check checks the tunnel device specified by path is present and accessible. +func check(path string) error { + f, err := os.OpenFile(path, os.O_RDWR, 0) + if err != nil { + return fmt.Errorf("TUN device is not available: %w", err) + } + defer f.Close() + + info, err := f.Stat() + if err != nil { + return fmt.Errorf("getting stat information for TUN file: %w", err) + } + + sys, ok := info.Sys().(*syscall.Stat_t) + if !ok { + return errors.New("cannot get syscall stat info of TUN file") + } + + const expectedRdev = 2760 // corresponds to major 10 and minor 200 + if sys.Rdev != expectedRdev { + return fmt.Errorf("TUN file has an unexpected rdev: %d instead of expected %d", + sys.Rdev, expectedRdev) + } + + if err := f.Close(); err != nil { + return fmt.Errorf("closing TUN device: %w", err) + } + + return nil +} + +// create creates a TUN device at the path specified. +func create(path string) (err error) { + parentDir := filepath.Dir(path) + err = os.MkdirAll(parentDir, 0o751) //nolint:mnd + if err != nil { + return err + } + + const ( + major = 10 + minor = 200 + ) + dev := unix.Mkdev(major, minor) + if dev > math.MaxInt { + panic("dev is too high") + } + err = unix.Mknod(path, unix.S_IFCHR, int(dev)) + if err != nil { + return fmt.Errorf("creating TUN device file node: %w", err) + } + + fd, err := unix.Open(path, 0, 0) + if err != nil { + if err.Error() == "operation not permitted" { + err = fmt.Errorf("%w (did you specify --device /dev/net/tun to your container command?)", err) + } + return fmt.Errorf("unix opening TUN device file: %w", err) + } + + const nonBlocking = true + err = unix.SetNonblock(fd, nonBlocking) + if err != nil { + return fmt.Errorf("setting non block to TUN device file descriptor: %w", err) + } + + return nil } diff --git a/internal/tun/tun_test.go b/internal/tun/tun_test.go index 93565b7f..db7aa4f3 100644 --- a/internal/tun/tun_test.go +++ b/internal/tun/tun_test.go @@ -10,20 +10,18 @@ import ( "github.com/stretchr/testify/require" ) -func Test_Tun(t *testing.T) { +func Test_Setup(t *testing.T) { t.Parallel() path := getTempPath(t) - tun := New() - defer func() { err := os.RemoveAll(path) require.NoError(t, err) }() // No file check fail - err := tun.Check(path) + err := check(path) require.Error(t, err) expectedMessage := "TUN device is not available: open " + path + ": no such file or directory" require.Equal(t, expectedMessage, err.Error()) @@ -35,13 +33,13 @@ func Test_Tun(t *testing.T) { require.NoError(t, err) // Simple file check fail - err = tun.Check(path) + err = check(path) require.Error(t, err) expectedMessage = "TUN file has an unexpected rdev: 0 instead of expected 2760" require.Equal(t, expectedMessage, err.Error()) // Create TUN device fail as file exists - err = tun.Create(path) + err = create(path) require.Error(t, err) require.EqualError(t, err, "creating TUN device file node: file exists") @@ -50,7 +48,7 @@ func Test_Tun(t *testing.T) { require.NoError(t, err) // Create TUN device success - err = tun.Create(path) + err = create(path) if err != nil && strings.HasSuffix(err.Error(), "operation not permitted") { t.Skip("You do not have root privileges to create a TUN device, skipping test") return @@ -58,7 +56,7 @@ func Test_Tun(t *testing.T) { require.NoError(t, err) // Check TUN device success - err = tun.Check(path) + err = check(path) require.NoError(t, err) } diff --git a/internal/tun/tun_unspecified.go b/internal/tun/tun_unspecified.go new file mode 100644 index 00000000..2944a8b3 --- /dev/null +++ b/internal/tun/tun_unspecified.go @@ -0,0 +1,12 @@ +//go:build !linux && !darwin + +package tun + +import ( + "fmt" + "runtime" +) + +func Setup() error { + return fmt.Errorf("not implemented for %s", runtime.GOOS) +} diff --git a/internal/vpn/amneziawg.go b/internal/vpn/amneziawg.go index c13b1125..6b2ce44c 100644 --- a/internal/vpn/amneziawg.go +++ b/internal/vpn/amneziawg.go @@ -9,6 +9,7 @@ import ( "github.com/qdm12/gluetun/internal/models" "github.com/qdm12/gluetun/internal/netlink" "github.com/qdm12/gluetun/internal/provider" + "github.com/qdm12/gluetun/internal/tun" "github.com/qdm12/gluetun/internal/wireguard" "github.com/qdm12/gosettings" ) @@ -19,6 +20,11 @@ func setupAmneziaWg(ctx context.Context, netlinker NetLinker, settings settings.VPN, ipv6SupportLevel netlink.IPv6SupportLevel, logger wireguard.Logger) ( amneziawger *amneziawg.Amneziawg, connection models.Connection, err error, ) { + err = tun.Setup() + if err != nil { + return nil, models.Connection{}, fmt.Errorf("setting up tun device: %w", err) + } + ipv6Internet := ipv6SupportLevel == netlink.IPv6Internet connection, err = providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Internet) if err != nil { diff --git a/internal/vpn/openvpn.go b/internal/vpn/openvpn.go index 7cdcbc0a..307203c3 100644 --- a/internal/vpn/openvpn.go +++ b/internal/vpn/openvpn.go @@ -9,6 +9,7 @@ import ( "github.com/qdm12/gluetun/internal/netlink" "github.com/qdm12/gluetun/internal/openvpn" "github.com/qdm12/gluetun/internal/provider" + "github.com/qdm12/gluetun/internal/tun" ) // setupOpenVPN sets OpenVPN up using the configurators and settings given. @@ -18,6 +19,11 @@ func setupOpenVPN(ctx context.Context, fw Firewall, settings settings.VPN, ipv6SupportLevel netlink.IPv6SupportLevel, starter Cmder, logger openvpn.Logger) (runner *openvpn.Runner, connection models.Connection, err error, ) { + err = tun.Setup() + if err != nil { + return nil, models.Connection{}, fmt.Errorf("setting up tun device: %w", err) + } + ipv6Internet := ipv6SupportLevel == netlink.IPv6Internet connection, err = providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Internet) if err != nil { diff --git a/internal/wireguard/run.go b/internal/wireguard/run.go index deecc756..5e6f238d 100644 --- a/internal/wireguard/run.go +++ b/internal/wireguard/run.go @@ -8,6 +8,7 @@ import ( "github.com/qdm12/gluetun/internal/cleanup" "github.com/qdm12/gluetun/internal/netlink" + gtun "github.com/qdm12/gluetun/internal/tun" "golang.zx2c4.com/wireguard/conn" "golang.zx2c4.com/wireguard/device" "golang.zx2c4.com/wireguard/tun" @@ -27,15 +28,18 @@ func (w *Wireguard) Run(ctx context.Context, waitError chan<- error, ready chan< } setupFunction := setupUserSpace + userspace := false switch w.settings.Implementation { case "auto": //nolint:goconst if !kernelSupported { w.logger.Info("Using userspace implementation since Kernel support does not exist") + userspace = true break } w.logger.Info("Using available kernelspace implementation") setupFunction = setupKernelSpace case "userspace": + userspace = true case "kernelspace": if !kernelSupported { waitError <- errors.New("kernel does not support Wireguard") @@ -46,6 +50,14 @@ func (w *Wireguard) Run(ctx context.Context, waitError chan<- error, ready chan< panic(fmt.Sprintf("unknown implementation %q", w.settings.Implementation)) } + if userspace { + err = gtun.Setup() + if err != nil { + waitError <- fmt.Errorf("setting up userspace tun device: %w", err) + return + } + } + setup := func(ctx context.Context, cleanups *cleanup.Cleanups) ( linkIndex uint32, waitAndCleanup func() error, err error, ) {