diff --git a/cmd/gluetun/main.go b/cmd/gluetun/main.go index b9b1ec33..48fbd95c 100644 --- a/cmd/gluetun/main.go +++ b/cmd/gluetun/main.go @@ -29,7 +29,7 @@ import ( "github.com/qdm12/gluetun/internal/server" "github.com/qdm12/gluetun/internal/shadowsocks" "github.com/qdm12/gluetun/internal/storage" - "github.com/qdm12/gluetun/internal/unix" + "github.com/qdm12/gluetun/internal/tun" "github.com/qdm12/gluetun/internal/updater" "github.com/qdm12/golibs/command" "github.com/qdm12/golibs/logging" @@ -67,14 +67,14 @@ func main() { }) args := os.Args - unix := unix.New() + tun := tun.New() cli := cli.New() env := params.NewEnv() cmder := command.NewCmder() errorCh := make(chan error) go func() { - errorCh <- _main(ctx, buildInfo, args, logger, env, unix, cmder, cli) + errorCh <- _main(ctx, buildInfo, args, logger, env, tun, cmder, cli) }() select { @@ -113,7 +113,7 @@ var ( //nolint:gocognit,gocyclo func _main(ctx context.Context, buildInfo models.BuildInformation, args []string, logger logging.ParentLogger, env params.Env, - unix unix.Unix, cmder command.RunStarter, cli cli.CLIer) error { + tun tun.Interface, cmder command.RunStarter, cli cli.CLIer) error { if len(args) > 1 { // cli operation switch args[1] { case "healthcheck": @@ -135,7 +135,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, alpineConf := alpine.New() ovpnConf := openvpn.NewConfigurator( logger.NewChild(logging.Settings{Prefix: "openvpn configurator: "}), - unix, cmder) + cmder) dnsCrypto := dnscrypto.New(httpClient, "", "") const cacertsPath = "/etc/ssl/certs/ca-certificates.crt" dnsConf := unbound.NewConfigurator(nil, cmder, dnsCrypto, @@ -270,9 +270,9 @@ func _main(ctx context.Context, buildInfo models.BuildInformation, return err } - if err := ovpnConf.CheckTUN(); err != nil { - logger.Warn(err.Error()) - err = ovpnConf.CreateTUN() + if err := tun.Check(constants.TunnelDevice); err != nil { + logger.Info(err.Error() + "; creating it...") + err = tun.Create(constants.TunnelDevice) if err != nil { return err } diff --git a/internal/openvpn/openvpn.go b/internal/openvpn/openvpn.go index 6dbf70e6..4d2dcfeb 100644 --- a/internal/openvpn/openvpn.go +++ b/internal/openvpn/openvpn.go @@ -4,7 +4,6 @@ package openvpn import ( "github.com/qdm12/gluetun/internal/constants" - "github.com/qdm12/gluetun/internal/unix" "github.com/qdm12/golibs/command" "github.com/qdm12/golibs/logging" ) @@ -12,7 +11,6 @@ import ( type Configurator interface { VersionGetter AuthWriter - TUNCheckCreater Starter } @@ -24,18 +22,14 @@ type StarterAuthWriter interface { type configurator struct { logger logging.Logger cmder command.RunStarter - unix unix.Unix authFilePath string - tunDevPath string } -func NewConfigurator(logger logging.Logger, unix unix.Unix, +func NewConfigurator(logger logging.Logger, cmder command.RunStarter) Configurator { return &configurator{ logger: logger, cmder: cmder, - unix: unix, authFilePath: constants.OpenVPNAuthConf, - tunDevPath: constants.TunnelDevice, } } diff --git a/internal/openvpn/tun.go b/internal/openvpn/tun.go deleted file mode 100644 index 70543e46..00000000 --- a/internal/openvpn/tun.go +++ /dev/null @@ -1,61 +0,0 @@ -package openvpn - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/qdm12/gluetun/internal/unix" -) - -type TUNCheckCreater interface { - TUNChecker - TUNCreater -} - -type TUNChecker interface { - CheckTUN() error -} - -// CheckTUN checks the tunnel device is present and accessible. -func (c *configurator) CheckTUN() error { - c.logger.Info("checking for device " + c.tunDevPath) - f, err := os.OpenFile(c.tunDevPath, os.O_RDWR, 0) - if err != nil { - return fmt.Errorf("TUN device is not available: %w", err) - } - if err := f.Close(); err != nil { - c.logger.Warn("Could not close TUN device file: " + err.Error()) - } - return nil -} - -type TUNCreater interface { - CreateTUN() error -} - -func (c *configurator) CreateTUN() error { - c.logger.Info("creating " + c.tunDevPath) - - parentDir := filepath.Dir(c.tunDevPath) - if err := os.MkdirAll(parentDir, 0751); err != nil { //nolint:gomnd - return err - } - - const ( - major = 10 - minor = 200 - ) - dev := c.unix.Mkdev(major, minor) - if err := c.unix.Mknod(c.tunDevPath, unix.S_IFCHR, int(dev)); err != nil { - return err - } - - const readWriteAllPerms os.FileMode = 0666 - file, err := os.OpenFile(c.tunDevPath, os.O_WRONLY, readWriteAllPerms) - if err != nil { - return err - } - - return file.Close() -} diff --git a/internal/tun/check.go b/internal/tun/check.go new file mode 100644 index 00000000..a554f7b2 --- /dev/null +++ b/internal/tun/check.go @@ -0,0 +1,51 @@ +package tun + +import ( + "errors" + "fmt" + "os" + "syscall" +) + +type Checker interface { + Check(path string) error +} + +var ( + ErrTUNNotAvailable = errors.New("TUN device is not available") + ErrTUNStat = errors.New("cannot stat TUN file") + ErrTUNInfo = errors.New("cannot get syscall stat info of TUN file") + ErrTUNBadRdev = errors.New("TUN file has an unexpected rdev") + ErrTUNClose = errors.New("cannot close TUN device") +) + +// 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("%w: %s", ErrTUNNotAvailable, err) + } + defer f.Close() + + info, err := f.Stat() + if err != nil { + return fmt.Errorf("%w: %s", ErrTUNStat, err) + } + + sys, ok := info.Sys().(*syscall.Stat_t) + if !ok { + return ErrTUNInfo + } + + const expectedRdev = 2760 // corresponds to major 10 and minor 200 + if sys.Rdev != expectedRdev { + return fmt.Errorf("%w: %d instead of expected %d", + ErrTUNBadRdev, sys.Rdev, expectedRdev) + } + + if err := f.Close(); err != nil { + return fmt.Errorf("%w: %s", ErrTUNClose, err) + } + + return nil +} diff --git a/internal/tun/create.go b/internal/tun/create.go new file mode 100644 index 00000000..419e6f0b --- /dev/null +++ b/internal/tun/create.go @@ -0,0 +1,38 @@ +package tun + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "golang.org/x/sys/unix" +) + +type Creator interface { + Create(path string) error +} + +var ( + ErrMknod = errors.New("cannot create TUN device file node") +) + +// Create creates a TUN device at the path specified. +func (t *Tun) Create(path string) error { + parentDir := filepath.Dir(path) + if err := os.MkdirAll(parentDir, 0751); err != nil { //nolint:gomnd + return err + } + + const ( + major = 10 + minor = 200 + ) + dev := unix.Mkdev(major, minor) + err := t.mknod(path, unix.S_IFCHR, int(dev)) + if err != nil { + return fmt.Errorf("%w: %s", ErrMknod, err) + } + + return nil +} diff --git a/internal/tun/tun.go b/internal/tun/tun.go new file mode 100644 index 00000000..f9e2f18d --- /dev/null +++ b/internal/tun/tun.go @@ -0,0 +1,20 @@ +package tun + +import "golang.org/x/sys/unix" + +var _ Interface = (*Tun)(nil) + +type Interface interface { + Checker + Creator +} + +type Tun struct { + mknod func(path string, mode uint32, dev int) (err error) +} + +func New() *Tun { + return &Tun{ + mknod: unix.Mknod, + } +} diff --git a/internal/tun/tun_test.go b/internal/tun/tun_test.go new file mode 100644 index 00000000..2b5877e3 --- /dev/null +++ b/internal/tun/tun_test.go @@ -0,0 +1,68 @@ +package tun + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Tun(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) + require.Error(t, err) + expectedMessage := "TUN device is not available: open " + path + ": no such file or directory" + require.Equal(t, expectedMessage, err.Error()) + + // Create simple file + file, err := os.Create(path) + require.NoError(t, err) + err = file.Close() + require.NoError(t, err) + + // Simple file check fail + err = tun.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) + require.Error(t, err) + require.Equal(t, "cannot create TUN device file node: file exists", err.Error()) + + // Remove simple file + err = os.Remove(path) + require.NoError(t, err) + + // Create TUN device success + err = tun.Create(path) + require.NoError(t, err) + + // Check TUN device success + err = tun.Check(path) + require.NoError(t, err) +} + +func getTempPath(t *testing.T) (path string) { + t.Helper() + file, err := os.CreateTemp("", "") + require.NoError(t, err) + path = file.Name() + err = file.Close() + require.NoError(t, err) + err = os.Remove(path) + require.NoError(t, err) + return path +} diff --git a/internal/unix/constants.go b/internal/unix/constants.go deleted file mode 100644 index c6337bd8..00000000 --- a/internal/unix/constants.go +++ /dev/null @@ -1,9 +0,0 @@ -package unix - -import sysunix "golang.org/x/sys/unix" - -// Constants used for convenience so "os" does not have to be imported - -const ( - S_IFCHR = sysunix.S_IFCHR //nolint:revive -) diff --git a/internal/unix/mock_unix/unix.go b/internal/unix/mock_unix/unix.go deleted file mode 100644 index dfea22a0..00000000 --- a/internal/unix/mock_unix/unix.go +++ /dev/null @@ -1,61 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/qdm12/gluetun/internal/unix (interfaces: Unix) - -// Package mock_unix is a generated GoMock package. -package mock_unix - -import ( - gomock "github.com/golang/mock/gomock" - reflect "reflect" -) - -// MockUnix is a mock of Unix interface -type MockUnix struct { - ctrl *gomock.Controller - recorder *MockUnixMockRecorder -} - -// MockUnixMockRecorder is the mock recorder for MockUnix -type MockUnixMockRecorder struct { - mock *MockUnix -} - -// NewMockUnix creates a new mock instance -func NewMockUnix(ctrl *gomock.Controller) *MockUnix { - mock := &MockUnix{ctrl: ctrl} - mock.recorder = &MockUnixMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockUnix) EXPECT() *MockUnixMockRecorder { - return m.recorder -} - -// Mkdev mocks base method -func (m *MockUnix) Mkdev(arg0, arg1 uint32) uint64 { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Mkdev", arg0, arg1) - ret0, _ := ret[0].(uint64) - return ret0 -} - -// Mkdev indicates an expected call of Mkdev -func (mr *MockUnixMockRecorder) Mkdev(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mkdev", reflect.TypeOf((*MockUnix)(nil).Mkdev), arg0, arg1) -} - -// Mknod mocks base method -func (m *MockUnix) Mknod(arg0 string, arg1 uint32, arg2 int) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Mknod", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// Mknod indicates an expected call of Mknod -func (mr *MockUnixMockRecorder) Mknod(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mknod", reflect.TypeOf((*MockUnix)(nil).Mknod), arg0, arg1, arg2) -} diff --git a/internal/unix/unix.go b/internal/unix/unix.go deleted file mode 100644 index 95b6852b..00000000 --- a/internal/unix/unix.go +++ /dev/null @@ -1,26 +0,0 @@ -// Package unix defines interfaces to interact with Unix related objects. -// Its primary use is to be used in tests. -package unix - -import sysunix "golang.org/x/sys/unix" - -//go:generate mockgen -destination=mock_$GOPACKAGE/$GOFILE . Unix - -type Unix interface { - Mkdev(major uint32, minor uint32) uint64 - Mknod(path string, mode uint32, dev int) (err error) -} - -func New() Unix { - return &unix{} -} - -type unix struct{} - -func (u *unix) Mkdev(major uint32, minor uint32) uint64 { - return sysunix.Mkdev(major, minor) -} - -func (u *unix) Mknod(path string, mode uint32, dev int) (err error) { - return sysunix.Mknod(path, mode, dev) -}