feat(settings): parse Wireguard settings from /gluetun/wireguard/wg0.conf (#1120)
This commit is contained in:
1
go.mod
1
go.mod
@@ -24,6 +24,7 @@ require (
|
|||||||
golang.org/x/text v0.11.0
|
golang.org/x/text v0.11.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
|
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230215201556-9c5414ab4bde
|
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
|
inet.af/netaddr v0.0.0-20220811202034-502d2d690317
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
4
go.sum
4
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-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 h1:bge5AL7cjHJMPz+5IOz5yF01q/l8No6+lIEBieA8gMg=
|
||||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6/go.mod h1:6aRbg4Z/bTbm9JfxsGXfWKHi7zsOvPfUTK1S5HuAFKg=
|
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 h1:UYA92yyeDPbmZysIuG65yrpZVPtdIoRmtEHft/AyI38=
|
||||||
github.com/qdm12/gosettings v0.4.0-rc1/go.mod h1:JRV3opOpHvnKlIA29lKQMdYw1WSMVMfHYLLHPHol5ME=
|
github.com/qdm12/gosettings v0.4.0-rc1/go.mod h1:JRV3opOpHvnKlIA29lKQMdYw1WSMVMfHYLLHPHol5ME=
|
||||||
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
|
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 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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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-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-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||||
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
||||||
|
|||||||
3
internal/configuration/sources/files/helpers_test.go
Normal file
3
internal/configuration/sources/files/helpers_test.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package files
|
||||||
|
|
||||||
|
func ptrTo[T any](x T) *T { return &x }
|
||||||
16
internal/configuration/sources/files/provider.go
Normal file
16
internal/configuration/sources/files/provider.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -4,10 +4,15 @@ import (
|
|||||||
"github.com/qdm12/gluetun/internal/configuration/settings"
|
"github.com/qdm12/gluetun/internal/configuration/settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Source struct{}
|
type Source struct {
|
||||||
|
wireguardConfigPath string
|
||||||
|
}
|
||||||
|
|
||||||
func New() *Source {
|
func New() *Source {
|
||||||
return &Source{}
|
const wireguardConfigPath = "/gluetun/wireguard/wg0.conf"
|
||||||
|
return &Source{
|
||||||
|
wireguardConfigPath: wireguardConfigPath,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Source) String() string { return "files" }
|
func (s *Source) String() string { return "files" }
|
||||||
|
|||||||
16
internal/configuration/sources/files/serverselection.go
Normal file
16
internal/configuration/sources/files/serverselection.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -7,10 +7,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (s *Source) readVPN() (vpn settings.VPN, err error) {
|
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()
|
vpn.OpenVPN, err = s.readOpenVPN()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return vpn, fmt.Errorf("OpenVPN: %w", err)
|
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
|
return vpn, nil
|
||||||
}
|
}
|
||||||
|
|||||||
111
internal/configuration/sources/files/wireguard.go
Normal file
111
internal/configuration/sources/files/wireguard.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
273
internal/configuration/sources/files/wireguard_test.go
Normal file
273
internal/configuration/sources/files/wireguard_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
83
internal/configuration/sources/files/wireguardselection.go
Normal file
83
internal/configuration/sources/files/wireguardselection.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
181
internal/configuration/sources/files/wireguardselection_test.go
Normal file
181
internal/configuration/sources/files/wireguardselection_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user