Maint: tun package to handle tun device operations

- Moved from openvpn package to tun package
- TUN check verifies Rdev value
- TUN create
- Inject as interface to main function
- Add integration test
- Clearer log message for end users if tun device does not exist
- Remove unix package (unneeded for tests)
- Remove tun file opening at the end of tun file creation
- Do not mock unix.Mkdev (no OS operation)
- Remove Tun operations from OpenVPN configurator
This commit is contained in:
Quentin McGaw (desktop)
2021-08-18 15:31:08 +00:00
parent 384a4bae3a
commit 6a545aa088
10 changed files with 186 additions and 172 deletions

View File

@@ -29,7 +29,7 @@ import (
"github.com/qdm12/gluetun/internal/server" "github.com/qdm12/gluetun/internal/server"
"github.com/qdm12/gluetun/internal/shadowsocks" "github.com/qdm12/gluetun/internal/shadowsocks"
"github.com/qdm12/gluetun/internal/storage" "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/gluetun/internal/updater"
"github.com/qdm12/golibs/command" "github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/logging" "github.com/qdm12/golibs/logging"
@@ -67,14 +67,14 @@ func main() {
}) })
args := os.Args args := os.Args
unix := unix.New() tun := tun.New()
cli := cli.New() cli := cli.New()
env := params.NewEnv() env := params.NewEnv()
cmder := command.NewCmder() cmder := command.NewCmder()
errorCh := make(chan error) errorCh := make(chan error)
go func() { go func() {
errorCh <- _main(ctx, buildInfo, args, logger, env, unix, cmder, cli) errorCh <- _main(ctx, buildInfo, args, logger, env, tun, cmder, cli)
}() }()
select { select {
@@ -113,7 +113,7 @@ var (
//nolint:gocognit,gocyclo //nolint:gocognit,gocyclo
func _main(ctx context.Context, buildInfo models.BuildInformation, func _main(ctx context.Context, buildInfo models.BuildInformation,
args []string, logger logging.ParentLogger, env params.Env, 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 if len(args) > 1 { // cli operation
switch args[1] { switch args[1] {
case "healthcheck": case "healthcheck":
@@ -135,7 +135,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
alpineConf := alpine.New() alpineConf := alpine.New()
ovpnConf := openvpn.NewConfigurator( ovpnConf := openvpn.NewConfigurator(
logger.NewChild(logging.Settings{Prefix: "openvpn configurator: "}), logger.NewChild(logging.Settings{Prefix: "openvpn configurator: "}),
unix, cmder) cmder)
dnsCrypto := dnscrypto.New(httpClient, "", "") dnsCrypto := dnscrypto.New(httpClient, "", "")
const cacertsPath = "/etc/ssl/certs/ca-certificates.crt" const cacertsPath = "/etc/ssl/certs/ca-certificates.crt"
dnsConf := unbound.NewConfigurator(nil, cmder, dnsCrypto, dnsConf := unbound.NewConfigurator(nil, cmder, dnsCrypto,
@@ -270,9 +270,9 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
return err return err
} }
if err := ovpnConf.CheckTUN(); err != nil { if err := tun.Check(constants.TunnelDevice); err != nil {
logger.Warn(err.Error()) logger.Info(err.Error() + "; creating it...")
err = ovpnConf.CreateTUN() err = tun.Create(constants.TunnelDevice)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -4,7 +4,6 @@ package openvpn
import ( import (
"github.com/qdm12/gluetun/internal/constants" "github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/unix"
"github.com/qdm12/golibs/command" "github.com/qdm12/golibs/command"
"github.com/qdm12/golibs/logging" "github.com/qdm12/golibs/logging"
) )
@@ -12,7 +11,6 @@ import (
type Configurator interface { type Configurator interface {
VersionGetter VersionGetter
AuthWriter AuthWriter
TUNCheckCreater
Starter Starter
} }
@@ -24,18 +22,14 @@ type StarterAuthWriter interface {
type configurator struct { type configurator struct {
logger logging.Logger logger logging.Logger
cmder command.RunStarter cmder command.RunStarter
unix unix.Unix
authFilePath string authFilePath string
tunDevPath string
} }
func NewConfigurator(logger logging.Logger, unix unix.Unix, func NewConfigurator(logger logging.Logger,
cmder command.RunStarter) Configurator { cmder command.RunStarter) Configurator {
return &configurator{ return &configurator{
logger: logger, logger: logger,
cmder: cmder, cmder: cmder,
unix: unix,
authFilePath: constants.OpenVPNAuthConf, authFilePath: constants.OpenVPNAuthConf,
tunDevPath: constants.TunnelDevice,
} }
} }

View File

@@ -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()
}

51
internal/tun/check.go Normal file
View File

@@ -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
}

38
internal/tun/create.go Normal file
View File

@@ -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
}

20
internal/tun/tun.go Normal file
View File

@@ -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,
}
}

68
internal/tun/tun_test.go Normal file
View File

@@ -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
}

View File

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

View File

@@ -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)
}

View File

@@ -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)
}