From 0ebfe534d3dd66f180c17914734232f212a281e3 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Sat, 22 Jul 2023 17:25:30 +0200 Subject: [PATCH] feat(settings): parse Wireguard settings from `/gluetun/wireguard/wg0.conf` (#1120) --- go.mod | 1 + go.sum | 4 +- .../sources/files/helpers_test.go | 3 + .../configuration/sources/files/provider.go | 16 + .../configuration/sources/files/reader.go | 9 +- .../sources/files/serverselection.go | 16 + internal/configuration/sources/files/vpn.go | 10 + .../configuration/sources/files/wireguard.go | 111 +++++++ .../sources/files/wireguard_test.go | 273 ++++++++++++++++++ .../sources/files/wireguardselection.go | 83 ++++++ .../sources/files/wireguardselection_test.go | 181 ++++++++++++ 11 files changed, 703 insertions(+), 4 deletions(-) create mode 100644 internal/configuration/sources/files/helpers_test.go create mode 100644 internal/configuration/sources/files/provider.go create mode 100644 internal/configuration/sources/files/serverselection.go create mode 100644 internal/configuration/sources/files/wireguard.go create mode 100644 internal/configuration/sources/files/wireguard_test.go create mode 100644 internal/configuration/sources/files/wireguardselection.go create mode 100644 internal/configuration/sources/files/wireguardselection_test.go diff --git a/go.mod b/go.mod index f30a0523..4953d763 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( golang.org/x/text v0.11.0 golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde + gopkg.in/ini.v1 v1.67.0 inet.af/netaddr v0.0.0-20220811202034-502d2d690317 ) diff --git a/go.sum b/go.sum index b8f4794c..55008742 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,6 @@ github.com/qdm12/golibs v0.0.0-20210603202746-e5494e9c2ebb/go.mod h1:15RBzkun0i8 github.com/qdm12/golibs v0.0.0-20210723175634-a75ca7fd74c2/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg= github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg= github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg= -github.com/qdm12/gosettings v0.3.0-rc13 h1:fag+/hFPBUcNk3a5ifUbwNS2VgXFpxindkl8mQNk76U= -github.com/qdm12/gosettings v0.3.0-rc13/go.mod h1:JRV3opOpHvnKlIA29lKQMdYw1WSMVMfHYLLHPHol5ME= github.com/qdm12/gosettings v0.4.0-rc1 h1:UYA92yyeDPbmZysIuG65yrpZVPtdIoRmtEHft/AyI38= github.com/qdm12/gosettings v0.4.0-rc1/go.mod h1:JRV3opOpHvnKlIA29lKQMdYw1WSMVMfHYLLHPHol5ME= github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM= @@ -228,6 +226,8 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde/go.mod h1:m gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= diff --git a/internal/configuration/sources/files/helpers_test.go b/internal/configuration/sources/files/helpers_test.go new file mode 100644 index 00000000..a2658d7d --- /dev/null +++ b/internal/configuration/sources/files/helpers_test.go @@ -0,0 +1,3 @@ +package files + +func ptrTo[T any](x T) *T { return &x } diff --git a/internal/configuration/sources/files/provider.go b/internal/configuration/sources/files/provider.go new file mode 100644 index 00000000..5fd2cbe6 --- /dev/null +++ b/internal/configuration/sources/files/provider.go @@ -0,0 +1,16 @@ +package files + +import ( + "fmt" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func (s *Source) readProvider() (provider settings.Provider, err error) { + provider.ServerSelection, err = s.readServerSelection() + if err != nil { + return provider, fmt.Errorf("server selection: %w", err) + } + + return provider, nil +} diff --git a/internal/configuration/sources/files/reader.go b/internal/configuration/sources/files/reader.go index ed593de3..378b5805 100644 --- a/internal/configuration/sources/files/reader.go +++ b/internal/configuration/sources/files/reader.go @@ -4,10 +4,15 @@ import ( "github.com/qdm12/gluetun/internal/configuration/settings" ) -type Source struct{} +type Source struct { + wireguardConfigPath string +} func New() *Source { - return &Source{} + const wireguardConfigPath = "/gluetun/wireguard/wg0.conf" + return &Source{ + wireguardConfigPath: wireguardConfigPath, + } } func (s *Source) String() string { return "files" } diff --git a/internal/configuration/sources/files/serverselection.go b/internal/configuration/sources/files/serverselection.go new file mode 100644 index 00000000..89f0cd4c --- /dev/null +++ b/internal/configuration/sources/files/serverselection.go @@ -0,0 +1,16 @@ +package files + +import ( + "fmt" + + "github.com/qdm12/gluetun/internal/configuration/settings" +) + +func (s *Source) readServerSelection() (selection settings.ServerSelection, err error) { + selection.Wireguard, err = s.readWireguardSelection() + if err != nil { + return selection, fmt.Errorf("wireguard: %w", err) + } + + return selection, nil +} diff --git a/internal/configuration/sources/files/vpn.go b/internal/configuration/sources/files/vpn.go index 48d8e156..12d1425f 100644 --- a/internal/configuration/sources/files/vpn.go +++ b/internal/configuration/sources/files/vpn.go @@ -7,10 +7,20 @@ import ( ) func (s *Source) readVPN() (vpn settings.VPN, err error) { + vpn.Provider, err = s.readProvider() + if err != nil { + return vpn, fmt.Errorf("provider: %w", err) + } + vpn.OpenVPN, err = s.readOpenVPN() if err != nil { return vpn, fmt.Errorf("OpenVPN: %w", err) } + vpn.Wireguard, err = s.readWireguard() + if err != nil { + return vpn, fmt.Errorf("wireguard: %w", err) + } + return vpn, nil } diff --git a/internal/configuration/sources/files/wireguard.go b/internal/configuration/sources/files/wireguard.go new file mode 100644 index 00000000..5ed3a812 --- /dev/null +++ b/internal/configuration/sources/files/wireguard.go @@ -0,0 +1,111 @@ +package files + +import ( + "fmt" + "net/netip" + "regexp" + "strings" + + "github.com/qdm12/gluetun/internal/configuration/settings" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + "gopkg.in/ini.v1" +) + +var ( + regexINISectionNotExist = regexp.MustCompile(`^section ".+" does not exist$`) + regexINIKeyNotExist = regexp.MustCompile(`key ".*" not exists$`) +) + +func (s *Source) readWireguard() (wireguard settings.Wireguard, err error) { + fileStringPtr, err := ReadFromFile(s.wireguardConfigPath) + if err != nil { + return wireguard, fmt.Errorf("reading file: %w", err) + } + + if fileStringPtr == nil { + return wireguard, nil + } + + rawData := []byte(*fileStringPtr) + iniFile, err := ini.Load(rawData) + if err != nil { + return wireguard, fmt.Errorf("loading ini from reader: %w", err) + } + + interfaceSection, err := iniFile.GetSection("Interface") + if err == nil { + err = parseWireguardInterfaceSection(interfaceSection, &wireguard) + if err != nil { + return wireguard, fmt.Errorf("parsing interface section: %w", err) + } + } else if !regexINISectionNotExist.MatchString(err.Error()) { + // can never happen + return wireguard, fmt.Errorf("getting interface section: %w", err) + } + + return wireguard, nil +} + +func parseWireguardInterfaceSection(interfaceSection *ini.Section, + wireguard *settings.Wireguard) (err error) { + wireguard.PrivateKey, err = parseINIWireguardKey(interfaceSection, "PrivateKey") + if err != nil { + return err // error is already wrapped correctly + } + + wireguard.PreSharedKey, err = parseINIWireguardKey(interfaceSection, "PreSharedKey") + if err != nil { + return err // error is already wrapped correctly + } + + wireguard.Addresses, err = parseINIWireguardAddress(interfaceSection) + if err != nil { + return err // error is already wrapped correctly + } + + return nil +} + +func parseINIWireguardKey(section *ini.Section, keyName string) ( + key *string, err error) { + iniKey, err := section.GetKey(keyName) + if err != nil { + if regexINIKeyNotExist.MatchString(err.Error()) { + return nil, nil //nolint:nilnil + } + // can never happen + return nil, fmt.Errorf("getting %s key: %w", keyName, err) + } + + key = new(string) + *key = iniKey.String() + _, err = wgtypes.ParseKey(*key) + if err != nil { + return nil, fmt.Errorf("parsing %s: %s: %w", keyName, *key, err) + } + return key, nil +} + +func parseINIWireguardAddress(section *ini.Section) ( + addresses []netip.Prefix, err error) { + addressKey, err := section.GetKey("Address") + if err != nil { + if regexINIKeyNotExist.MatchString(err.Error()) { + return nil, nil + } + // can never happen + return nil, fmt.Errorf("getting Address key: %w", err) + } + + addressStrings := strings.Split(addressKey.String(), ",") + addresses = make([]netip.Prefix, len(addressStrings)) + for i, addressString := range addressStrings { + addressString = strings.TrimSpace(addressString) + addresses[i], err = netip.ParsePrefix(addressString) + if err != nil { + return nil, fmt.Errorf("parsing address: %w", err) + } + } + + return addresses, nil +} diff --git a/internal/configuration/sources/files/wireguard_test.go b/internal/configuration/sources/files/wireguard_test.go new file mode 100644 index 00000000..c74aab43 --- /dev/null +++ b/internal/configuration/sources/files/wireguard_test.go @@ -0,0 +1,273 @@ +package files + +import ( + "net/netip" + "os" + "path/filepath" + "testing" + + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/ini.v1" +) + +func Test_Source_readWireguard(t *testing.T) { + t.Parallel() + + t.Run("fail reading from file", func(t *testing.T) { + t.Parallel() + + dirPath := t.TempDir() + source := &Source{ + wireguardConfigPath: dirPath, + } + wireguard, err := source.readWireguard() + assert.Equal(t, settings.Wireguard{}, wireguard) + assert.Error(t, err) + assert.Regexp(t, `reading file: read .+: is a directory`, err.Error()) + }) + + t.Run("no file", func(t *testing.T) { + t.Parallel() + + noFile := filepath.Join(t.TempDir(), "doesnotexist") + source := &Source{ + wireguardConfigPath: noFile, + } + wireguard, err := source.readWireguard() + assert.Equal(t, settings.Wireguard{}, wireguard) + assert.NoError(t, err) + }) + + testCases := map[string]struct { + fileContent string + wireguard settings.Wireguard + errMessage string + }{ + "ini load error": { + fileContent: "invalid", + errMessage: "loading ini from reader: key-value delimiter not found: invalid", + }, + "empty file": {}, + "interface section parsing error": { + fileContent: ` +[Interface] +PrivateKey = x +`, + errMessage: "parsing interface section: parsing PrivateKey: " + + "x: wgtypes: failed to parse base64-encoded key: " + + "illegal base64 data at input byte 0", + }, + "success": { + fileContent: ` +[Interface] +PrivateKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8= +PreSharedKey = YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g= +Address = 10.38.22.35/32 +DNS = 193.138.218.74 + +[Peer] +`, + wireguard: settings.Wireguard{ + PrivateKey: ptrTo("QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8="), + PreSharedKey: ptrTo("YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g="), + Addresses: []netip.Prefix{ + netip.PrefixFrom(netip.AddrFrom4([4]byte{10, 38, 22, 35}), 32), + }, + }, + }, + } + + for testName, testCase := range testCases { + testCase := testCase + t.Run(testName, func(t *testing.T) { + t.Parallel() + + configFile := filepath.Join(t.TempDir(), "wg.conf") + err := os.WriteFile(configFile, []byte(testCase.fileContent), 0600) + require.NoError(t, err) + + source := &Source{ + wireguardConfigPath: configFile, + } + + wireguard, err := source.readWireguard() + + assert.Equal(t, testCase.wireguard, wireguard) + if testCase.errMessage != "" { + assert.EqualError(t, err, testCase.errMessage) + } else { + assert.NoError(t, err) + } + }) + } +} + +func Test_parseWireguardInterfaceSection(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + iniData string + wireguard settings.Wireguard + errMessage string + }{ + "private key error": { + iniData: `[Interface] +PrivateKey = x`, + errMessage: "parsing PrivateKey: x: " + + "wgtypes: failed to parse base64-encoded key: " + + "illegal base64 data at input byte 0", + }, + "pre shared key error": { + iniData: `[Interface] +PreSharedKey = x +`, + errMessage: "parsing PreSharedKey: x: " + + "wgtypes: failed to parse base64-encoded key: " + + "illegal base64 data at input byte 0", + }, + "address error": { + iniData: `[Interface] +Address = x +`, + errMessage: "parsing address: netip.ParsePrefix(\"x\"): no '/'", + }, + "success": { + iniData: ` +[Interface] +PrivateKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8= +PreSharedKey = YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g= +Address = 10.38.22.35/32 +`, + wireguard: settings.Wireguard{ + PrivateKey: ptrTo("QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8="), + PreSharedKey: ptrTo("YJ680VN+dGrdsWNjSFqZ6vvwuiNhbq502ZL3G7Q3o3g="), + Addresses: []netip.Prefix{ + netip.PrefixFrom(netip.AddrFrom4([4]byte{10, 38, 22, 35}), 32), + }, + }, + }, + } + + for testName, testCase := range testCases { + testCase := testCase + t.Run(testName, func(t *testing.T) { + t.Parallel() + + iniFile, err := ini.Load([]byte(testCase.iniData)) + require.NoError(t, err) + iniSection, err := iniFile.GetSection("Interface") + require.NoError(t, err) + + var wireguard settings.Wireguard + err = parseWireguardInterfaceSection(iniSection, &wireguard) + + assert.Equal(t, testCase.wireguard, wireguard) + if testCase.errMessage != "" { + assert.EqualError(t, err, testCase.errMessage) + } else { + assert.NoError(t, err) + } + }) + } +} + +func Test_parseINIWireguardKey(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fileContent string + keyName string + key *string + errMessage string + }{ + "key does not exist": { + fileContent: `[Interface]`, + keyName: "PrivateKey", + }, + "bad Wireguard key": { + fileContent: `[Interface] +PrivateKey = x`, + keyName: "PrivateKey", + errMessage: "parsing PrivateKey: x: " + + "wgtypes: failed to parse base64-encoded key: " + + "illegal base64 data at input byte 0", + }, + "success": { + fileContent: `[Interface] +PrivateKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=`, + keyName: "PrivateKey", + key: ptrTo("QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8="), + }, + } + + for testName, testCase := range testCases { + testCase := testCase + t.Run(testName, func(t *testing.T) { + t.Parallel() + + iniFile, err := ini.Load([]byte(testCase.fileContent)) + require.NoError(t, err) + iniSection, err := iniFile.GetSection("Interface") + require.NoError(t, err) + + key, err := parseINIWireguardKey(iniSection, testCase.keyName) + + assert.Equal(t, testCase.key, key) + if testCase.errMessage != "" { + assert.EqualError(t, err, testCase.errMessage) + } else { + assert.NoError(t, err) + } + }) + } +} + +func Test_parseINIWireguardAddress(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fileContent string + addresses []netip.Prefix + errMessage string + }{ + "key does not exist": { + fileContent: `[Interface]`, + }, + "bad address": { + fileContent: `[Interface] +Address = x`, + errMessage: "parsing address: netip.ParsePrefix(\"x\"): no '/'", + }, + "success": { + fileContent: `[Interface] +Address = 1.2.3.4/32, 5.6.7.8/32`, + addresses: []netip.Prefix{ + netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 32), + netip.PrefixFrom(netip.AddrFrom4([4]byte{5, 6, 7, 8}), 32), + }, + }, + } + + for testName, testCase := range testCases { + testCase := testCase + t.Run(testName, func(t *testing.T) { + t.Parallel() + + iniFile, err := ini.Load([]byte(testCase.fileContent)) + require.NoError(t, err) + iniSection, err := iniFile.GetSection("Interface") + require.NoError(t, err) + + addresses, err := parseINIWireguardAddress(iniSection) + + assert.Equal(t, testCase.addresses, addresses) + if testCase.errMessage != "" { + assert.EqualError(t, err, testCase.errMessage) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/internal/configuration/sources/files/wireguardselection.go b/internal/configuration/sources/files/wireguardselection.go new file mode 100644 index 00000000..f08f3cc2 --- /dev/null +++ b/internal/configuration/sources/files/wireguardselection.go @@ -0,0 +1,83 @@ +package files + +import ( + "errors" + "fmt" + "net" + "net/netip" + + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/qdm12/govalid/port" + "gopkg.in/ini.v1" +) + +var ( + ErrEndpointHostNotIP = errors.New("endpoint host is not an IP") +) + +func (s *Source) readWireguardSelection() (selection settings.WireguardSelection, err error) { + fileStringPtr, err := ReadFromFile(s.wireguardConfigPath) + if err != nil { + return selection, fmt.Errorf("reading file: %w", err) + } + + if fileStringPtr == nil { + return selection, nil + } + + rawData := []byte(*fileStringPtr) + iniFile, err := ini.Load(rawData) + if err != nil { + return selection, fmt.Errorf("loading ini from reader: %w", err) + } + + peerSection, err := iniFile.GetSection("Peer") + if err == nil { + err = parseWireguardPeerSection(peerSection, &selection) + if err != nil { + return selection, fmt.Errorf("parsing peer section: %w", err) + } + } else if !regexINISectionNotExist.MatchString(err.Error()) { + // can never happen + return selection, fmt.Errorf("getting peer section: %w", err) + } + + return selection, nil +} + +func parseWireguardPeerSection(peerSection *ini.Section, + selection *settings.WireguardSelection) (err error) { + publicKeyPtr, err := parseINIWireguardKey(peerSection, "PublicKey") + if err != nil { + return err // error is already wrapped correctly + } else if publicKeyPtr != nil { + selection.PublicKey = *publicKeyPtr + } + + endpointKey, err := peerSection.GetKey("Endpoint") + if err == nil { + endpoint := endpointKey.String() + host, portString, err := net.SplitHostPort(endpoint) + if err != nil { + return fmt.Errorf("splitting endpoint: %w", err) + } + + ip, err := netip.ParseAddr(host) + if err != nil { + return fmt.Errorf("%w: %w", ErrEndpointHostNotIP, err) + } + + endpointPort, err := port.Validate(portString) + if err != nil { + return fmt.Errorf("port from Endpoint key: %w", err) + } + + selection.EndpointIP = ip + selection.EndpointPort = &endpointPort + } else if !regexINIKeyNotExist.MatchString(err.Error()) { + // can never happen + return fmt.Errorf("getting endpoint key: %w", err) + } + + return nil +} diff --git a/internal/configuration/sources/files/wireguardselection_test.go b/internal/configuration/sources/files/wireguardselection_test.go new file mode 100644 index 00000000..4bedce5c --- /dev/null +++ b/internal/configuration/sources/files/wireguardselection_test.go @@ -0,0 +1,181 @@ +package files + +import ( + "net/netip" + "os" + "path/filepath" + "testing" + + "github.com/qdm12/gluetun/internal/configuration/settings" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/ini.v1" +) + +func uint16Ptr(n uint16) *uint16 { return &n } + +func Test_Source_readWireguardSelection(t *testing.T) { + t.Parallel() + + t.Run("fail reading from file", func(t *testing.T) { + t.Parallel() + + dirPath := t.TempDir() + source := &Source{ + wireguardConfigPath: dirPath, + } + wireguard, err := source.readWireguardSelection() + assert.Equal(t, settings.WireguardSelection{}, wireguard) + assert.Error(t, err) + assert.Regexp(t, `reading file: read .+: is a directory`, err.Error()) + }) + + t.Run("no file", func(t *testing.T) { + t.Parallel() + + noFile := filepath.Join(t.TempDir(), "doesnotexist") + source := &Source{ + wireguardConfigPath: noFile, + } + wireguard, err := source.readWireguardSelection() + assert.Equal(t, settings.WireguardSelection{}, wireguard) + assert.NoError(t, err) + }) + + testCases := map[string]struct { + fileContent string + selection settings.WireguardSelection + errMessage string + }{ + "ini load error": { + fileContent: "invalid", + errMessage: "loading ini from reader: key-value delimiter not found: invalid", + }, + "empty file": {}, + "peer section parsing error": { + fileContent: ` +[Peer] +PublicKey = x +`, + errMessage: "parsing peer section: parsing PublicKey: " + + "x: wgtypes: failed to parse base64-encoded key: " + + "illegal base64 data at input byte 0", + }, + "success": { + fileContent: ` +[Peer] +PublicKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8= +Endpoint = 1.2.3.4:51820 +`, + selection: settings.WireguardSelection{ + PublicKey: "QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=", + EndpointIP: netip.AddrFrom4([4]byte{1, 2, 3, 4}), + EndpointPort: uint16Ptr(51820), + }, + }, + } + + for testName, testCase := range testCases { + testCase := testCase + t.Run(testName, func(t *testing.T) { + t.Parallel() + + configFile := filepath.Join(t.TempDir(), "wg.conf") + err := os.WriteFile(configFile, []byte(testCase.fileContent), 0600) + require.NoError(t, err) + + source := &Source{ + wireguardConfigPath: configFile, + } + + wireguard, err := source.readWireguardSelection() + + assert.Equal(t, testCase.selection, wireguard) + if testCase.errMessage != "" { + assert.EqualError(t, err, testCase.errMessage) + } else { + assert.NoError(t, err) + } + }) + } +} + +func Test_parseWireguardPeerSection(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + iniData string + selection settings.WireguardSelection + errMessage string + }{ + "public key error": { + iniData: `[Peer] +PublicKey = x`, + errMessage: "parsing PublicKey: x: " + + "wgtypes: failed to parse base64-encoded key: " + + "illegal base64 data at input byte 0", + }, + "public key set": { + iniData: `[Peer] +PublicKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=`, + selection: settings.WireguardSelection{ + PublicKey: "QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=", + }, + }, + "missing port in endpoint": { + iniData: `[Peer] +Endpoint = x`, + errMessage: "splitting endpoint: address x: missing port in address", + }, + "endpoint host is not IP": { + iniData: `[Peer] +Endpoint = website.com:51820`, + errMessage: "endpoint host is not an IP: ParseAddr(\"website.com\"): unexpected character (at \"website.com\")", + }, + "endpoint port is not valid": { + iniData: `[Peer] +Endpoint = 1.2.3.4:518299`, + errMessage: "port from Endpoint key: port cannot be higher than 65535: 518299", + }, + "valid endpoint": { + iniData: `[Peer] +Endpoint = 1.2.3.4:51820`, + selection: settings.WireguardSelection{ + EndpointIP: netip.AddrFrom4([4]byte{1, 2, 3, 4}), + EndpointPort: uint16Ptr(51820), + }, + }, + "all set": { + iniData: `[Peer] +PublicKey = QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8= +Endpoint = 1.2.3.4:51820`, + selection: settings.WireguardSelection{ + PublicKey: "QOlCgyA/Sn/c/+YNTIEohrjm8IZV+OZ2AUFIoX20sk8=", + EndpointIP: netip.AddrFrom4([4]byte{1, 2, 3, 4}), + EndpointPort: uint16Ptr(51820), + }, + }, + } + + for testName, testCase := range testCases { + testCase := testCase + t.Run(testName, func(t *testing.T) { + t.Parallel() + + iniFile, err := ini.Load([]byte(testCase.iniData)) + require.NoError(t, err) + iniSection, err := iniFile.GetSection("Peer") + require.NoError(t, err) + + var selection settings.WireguardSelection + err = parseWireguardPeerSection(iniSection, &selection) + + assert.Equal(t, testCase.selection, selection) + if testCase.errMessage != "" { + assert.EqualError(t, err, testCase.errMessage) + } else { + assert.NoError(t, err) + } + }) + } +}