Compare commits

..

1 Commits

Author SHA1 Message Date
Quentin McGaw
5f3301f3a3 Use Openvpn 2.4 only 2024-11-08 16:41:37 +00:00
26 changed files with 71 additions and 1071 deletions

View File

@@ -85,7 +85,7 @@ ENV VPN_SERVICE_PROVIDER=pia \
OPENVPN_PASSWORD= \
OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user \
OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password \
OPENVPN_VERSION=2.6 \
OPENVPN_VERSION=2.4 \
OPENVPN_VERBOSITY=1 \
OPENVPN_FLAGS= \
OPENVPN_CIPHERS= \
@@ -224,6 +224,9 @@ EXPOSE 8000/tcp 8888/tcp 8388/tcp 8388/udp
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck
ARG TARGETPLATFORM
RUN apk add --no-cache --update -l wget && \
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.12/main" openvpn\~2.4 && \
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.16/main" openssl\~1.1 && \
mv /usr/sbin/openvpn /usr/sbin/openvpn2.4 && \
apk add --no-cache --update -X "https://dl-cdn.alpinelinux.org/alpine/v3.17/main" openvpn\~2.5 && \
mv /usr/sbin/openvpn /usr/sbin/openvpn2.5 && \
apk del openvpn && \

View File

@@ -270,6 +270,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
err = printVersions(ctx, logger, []printVersionElement{
{name: "Alpine", getVersion: alpineConf.Version},
{name: "OpenVPN 2.4", getVersion: ovpnConf.Version24},
{name: "OpenVPN 2.5", getVersion: ovpnConf.Version25},
{name: "OpenVPN 2.6", getVersion: ovpnConf.Version26},
{name: "IPtables", getVersion: firewallConf.Version},

4
go.mod
View File

@@ -8,7 +8,7 @@ require (
github.com/golang/mock v1.6.0
github.com/klauspost/compress v1.17.11
github.com/klauspost/pgzip v1.2.6
github.com/pelletier/go-toml/v2 v2.2.3
github.com/pelletier/go-toml/v2 v2.2.2
github.com/qdm12/dns/v2 v2.0.0-rc8
github.com/qdm12/gosettings v0.4.3
github.com/qdm12/goshutdown v0.3.0
@@ -22,7 +22,7 @@ require (
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
golang.org/x/net v0.30.0
golang.org/x/sys v0.27.0
golang.org/x/sys v0.26.0
golang.org/x/text v0.19.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6

17
go.sum
View File

@@ -4,6 +4,7 @@ github.com/breml/rootcerts v0.2.18 h1:KjZaNT7AX/akUjzpStuwTMQs42YHlPyc6NmdwShVba
github.com/breml/rootcerts v0.2.18/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
@@ -41,8 +42,8 @@ github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE9
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
@@ -73,6 +74,13 @@ github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
@@ -112,8 +120,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -142,6 +150,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=

View File

@@ -19,7 +19,7 @@ import (
// OpenVPN contains settings to configure the OpenVPN client.
type OpenVPN struct {
// Version is the OpenVPN version to run.
// It can only be "2.5" or "2.6".
// It can only be "2.4".
Version string `json:"version"`
// User is the OpenVPN authentication username.
// It cannot be nil in the internal state if OpenVPN is used.
@@ -90,7 +90,7 @@ var ivpnAccountID = regexp.MustCompile(`^(i|ivpn)\-[a-zA-Z0-9]{4}\-[a-zA-Z0-9]{4
func (o OpenVPN) validate(vpnProvider string) (err error) {
// Validate version
validVersions := []string{openvpn.Openvpn25, openvpn.Openvpn26}
validVersions := []string{openvpn.Openvpn24}
if err = validate.IsOneOf(o.Version, validVersions...); err != nil {
return fmt.Errorf("%w: %w", ErrOpenVPNVersionIsNotValid, err)
}
@@ -289,7 +289,7 @@ func (o *OpenVPN) overrideWith(other OpenVPN) {
}
func (o *OpenVPN) setDefaults(vpnProvider string) {
o.Version = gosettings.DefaultComparable(o.Version, openvpn.Openvpn26)
o.Version = gosettings.DefaultComparable(o.Version, openvpn.Openvpn24)
o.User = gosettings.DefaultPointer(o.User, "")
if vpnProvider == providers.Mullvad {
o.Password = gosettings.DefaultPointer(o.Password, "m")

View File

@@ -4,9 +4,7 @@ import (
"fmt"
"net/netip"
"github.com/qdm12/gluetun/internal/configuration/settings/helpers"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/pprof"
"github.com/qdm12/gosettings/reader"
@@ -162,18 +160,6 @@ func (s Settings) Warnings() (warnings []string) {
" so this will likely not work anymore. See https://github.com/qdm12/gluetun/issues/1498.")
}
if helpers.IsOneOf(s.VPN.Provider.Name, providers.SlickVPN) &&
s.VPN.Type == vpn.OpenVPN {
warnings = append(warnings, "OpenVPN 2.5 and 2.6 use OpenSSL 3 "+
"which prohibits the usage of weak security in today's standards. "+
s.VPN.Provider.Name+" uses weak security which is out "+
"of Gluetun's control so the only workaround is to allow such weaknesses "+
`using the OpenVPN option tls-cipher "DEFAULT:@SECLEVEL=0". `+
"You might want to reach to your provider so they upgrade their certificates. "+
"Once this is done, you will have to let the Gluetun maintainers know "+
"by creating an issue, attaching the new certificate and we will update Gluetun.")
}
// TODO remove in v4
if s.DNS.ServerAddress.Unmap().Compare(netip.AddrFrom4([4]byte{127, 0, 0, 1})) != 0 {
warnings = append(warnings, "DNS address is set to "+s.DNS.ServerAddress.String()+

View File

@@ -30,7 +30,7 @@ func Test_Settings_String(t *testing.T) {
| | ├── Protocol: UDP
| | └── Private Internet Access encryption preset: strong
| └── OpenVPN settings:
| ├── OpenVPN version: 2.6
| ├── OpenVPN version: 2.4
| ├── User: [not set]
| ├── Password: [not set]
| ├── Private Internet Access encryption preset: strong

View File

@@ -1,6 +1,5 @@
package openvpn
const (
Openvpn25 = "2.5"
Openvpn26 = "2.6"
Openvpn24 = "2.4"
)

View File

@@ -13,8 +13,7 @@ import (
var ErrVersionUnknown = errors.New("OpenVPN version is unknown")
const (
binOpenvpn25 = "openvpn2.5"
binOpenvpn26 = "openvpn2.6"
binOpenvpn24 = "openvpn2.4"
)
func start(ctx context.Context, starter CmdStarter, version string, flags []string) (
@@ -22,10 +21,8 @@ func start(ctx context.Context, starter CmdStarter, version string, flags []stri
) {
var bin string
switch version {
case openvpn.Openvpn25:
bin = binOpenvpn25
case openvpn.Openvpn26:
bin = binOpenvpn26
case openvpn.Openvpn24:
bin = binOpenvpn24
default:
return nil, nil, nil, fmt.Errorf("%w: %s", ErrVersionUnknown, version)
}

View File

@@ -8,12 +8,8 @@ import (
"strings"
)
func (c *Configurator) Version25(ctx context.Context) (version string, err error) {
return c.version(ctx, binOpenvpn25)
}
func (c *Configurator) Version26(ctx context.Context) (version string, err error) {
return c.version(ctx, binOpenvpn26)
func (c *Configurator) Version24(ctx context.Context) (version string, err error) {
return c.version(ctx, binOpenvpn24)
}
var ErrVersionTooShort = errors.New("version output is too short")

View File

@@ -28,6 +28,8 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
}
switch settings.Version {
case openvpn.Openvpn24:
providerSettings.Ciphers = []string{openvpn.AES256cbc}
case openvpn.Openvpn25, openvpn.Openvpn26:
providerSettings.Ciphers = []string{
openvpn.AES256gcm, openvpn.AES256cbc, openvpn.AES192gcm,

View File

@@ -64,8 +64,8 @@ func Test_modifyConfig(t *testing.T) {
"suppress-timestamps",
"auth-user-pass /etc/openvpn/auth.conf",
"verb 0",
"data-ciphers-fallback cipher",
"data-ciphers cipher",
"cipher cipher", //nolint:dupword
"ncp-ciphers cipher",
"auth sha512",
"mssfix 1000",
"pull-filter ignore \"route-ipv6\"",

View File

@@ -31,11 +31,5 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
},
}
// SlickVPN's certificate is sha1WithRSAEncryption and sha1 is now
// rejected by openssl 3.x.x which is used by OpenVPN >= 2.5.
// We lower the security level to 3 to allow this algorithm,
// see https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_security_level.html
providerSettings.TLSCipher = "DEFAULT:@SECLEVEL=0"
return utils.OpenVPNConfig(providerSettings, connection, settings, ipv6Supported)
}

View File

@@ -10,7 +10,7 @@ func CipherLines(ciphers []string) (lines []string) {
}
return []string{
"data-ciphers-fallback " + ciphers[0],
"data-ciphers " + strings.Join(ciphers, ":"),
"cipher " + ciphers[0],
"ncp-ciphers " + strings.Join(ciphers, ":"),
}
}

View File

@@ -16,16 +16,16 @@ func Test_CipherLines(t *testing.T) {
"empty version": {
ciphers: []string{"AES"},
lines: []string{
"data-ciphers-fallback AES",
"data-ciphers AES",
"cipher AES",
"ncp-ciphers AES",
},
},
"2.5": {
"2.4": {
ciphers: []string{"AES", "CBC"},
version: "2.5",
version: "2.4",
lines: []string{
"data-ciphers-fallback AES",
"data-ciphers AES:CBC",
"cipher AES",
"ncp-ciphers AES:CBC",
},
},
}

View File

@@ -3,11 +3,7 @@ package api
import (
"errors"
"fmt"
"maps"
"net/http"
"net/url"
"regexp"
"slices"
"strings"
)
@@ -20,8 +16,6 @@ const (
IP2Location Provider = "ip2location"
)
const echoipPrefix = "echoip#"
type NameToken struct {
Name string
Token string
@@ -36,19 +30,15 @@ func New(nameTokenPairs []NameToken, client *http.Client) (
if err != nil {
return nil, fmt.Errorf("parsing API name: %w", err)
}
switch {
case provider == Cloudflare:
switch provider {
case Cloudflare:
fetchers[i] = newCloudflare(client)
case provider == IfConfigCo:
const ifConfigCoURL = "https://ifconfig.co"
fetchers[i] = newEchoip(client, ifConfigCoURL)
case provider == IPInfo:
case IfConfigCo:
fetchers[i] = newIfConfigCo(client)
case IPInfo:
fetchers[i] = newIPInfo(client, nameTokenPair.Token)
case provider == IP2Location:
case IP2Location:
fetchers[i] = newIP2Location(client, nameTokenPair.Token)
case strings.HasPrefix(string(provider), echoipPrefix):
url := strings.TrimPrefix(string(provider), echoipPrefix)
fetchers[i] = newEchoip(client, url)
default:
panic("provider not valid: " + provider)
}
@@ -56,88 +46,20 @@ func New(nameTokenPairs []NameToken, client *http.Client) (
return fetchers, nil
}
var regexEchoipURL = regexp.MustCompile(`^http(s|):\/\/.+$`)
var ErrProviderNotValid = errors.New("API name is not valid")
func ParseProvider(s string) (provider Provider, err error) {
possibleProviders := []Provider{
Cloudflare,
IfConfigCo,
IP2Location,
IPInfo,
switch strings.ToLower(s) {
case "cloudflare":
return Cloudflare, nil
case string(IfConfigCo):
return IfConfigCo, nil
case "ipinfo":
return IPInfo, nil
case "ip2location":
return IP2Location, nil
default:
return "", fmt.Errorf(`%w: %q can only be "cloudflare", "ifconfigco", "ip2location" or "ipinfo"`,
ErrProviderNotValid, s)
}
stringToProvider := make(map[string]Provider, len(possibleProviders))
for _, provider := range possibleProviders {
stringToProvider[string(provider)] = provider
}
provider, ok := stringToProvider[strings.ToLower(s)]
if ok {
return provider, nil
}
customPrefixToURLRegex := map[string]*regexp.Regexp{
echoipPrefix: regexEchoipURL,
}
for prefix, urlRegex := range customPrefixToURLRegex {
match, err := checkCustomURL(s, prefix, urlRegex)
if !match {
continue
} else if err != nil {
return "", err
}
return Provider(s), nil
}
providerStrings := make([]string, 0, len(stringToProvider)+len(customPrefixToURLRegex))
for _, providerString := range slices.Sorted(maps.Keys(stringToProvider)) {
providerStrings = append(providerStrings, `"`+providerString+`"`)
}
for _, prefix := range slices.Sorted(maps.Keys(customPrefixToURLRegex)) {
providerStrings = append(providerStrings, "a custom "+prefix+" url")
}
return "", fmt.Errorf(`%w: %q can only be %s`,
ErrProviderNotValid, s, orStrings(providerStrings))
}
var ErrCustomURLNotValid = errors.New("custom URL is not valid")
func checkCustomURL(s, prefix string, regex *regexp.Regexp) (match bool, err error) {
if !strings.HasPrefix(s, prefix) {
return false, nil
}
s = strings.TrimPrefix(s, prefix)
_, err = url.Parse(s)
if err != nil {
return true, fmt.Errorf("%s %w: %w", prefix, ErrCustomURLNotValid, err)
}
if regex.MatchString(s) {
return true, nil
}
return true, fmt.Errorf("%s %w: %q does not match regular expression: %s",
prefix, ErrCustomURLNotValid, s, regex)
}
func orStrings(strings []string) (result string) {
return joinStrings(strings, "or")
}
func joinStrings(strings []string, lastJoin string) (result string) {
if len(strings) == 0 {
return ""
}
result = strings[0]
for i := 1; i < len(strings); i++ {
if i < len(strings)-1 {
result += ", " + strings[i]
} else {
result += " " + lastJoin + " " + strings[i]
}
}
return result
}

View File

@@ -1,68 +0,0 @@
package api
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_ParseProvider(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
s string
provider Provider
errWrapped error
errMessage string
}{
"empty": {
errWrapped: ErrProviderNotValid,
errMessage: `API name is not valid: "" can only be ` +
`"cloudflare", "ifconfigco", "ip2location", "ipinfo" or a custom echoip# url`,
},
"invalid": {
s: "xyz",
errWrapped: ErrProviderNotValid,
errMessage: `API name is not valid: "xyz" can only be ` +
`"cloudflare", "ifconfigco", "ip2location", "ipinfo" or a custom echoip# url`,
},
"ipinfo": {
s: "ipinfo",
provider: IPInfo,
},
"IpInfo": {
s: "IpInfo",
provider: IPInfo,
},
"echoip_url_empty": {
s: "echoip#",
errWrapped: ErrCustomURLNotValid,
errMessage: `echoip# custom URL is not valid: "" ` +
`does not match regular expression: ^http(s|):\/\/.+$`,
},
"echoip_url_invalid": {
s: "echoip#postgres://localhost:3451",
errWrapped: ErrCustomURLNotValid,
errMessage: `echoip# custom URL is not valid: "postgres://localhost:3451" ` +
`does not match regular expression: ^http(s|):\/\/.+$`,
},
"echoip_url_valid": {
s: "echoip#http://localhost:3451",
provider: Provider("echoip#http://localhost:3451"),
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
provider, err := ParseProvider(testCase.s)
assert.Equal(t, testCase.provider, provider)
assert.ErrorIs(t, err, testCase.errWrapped)
if testCase.errWrapped != nil {
assert.EqualError(t, err, testCase.errMessage)
}
})
}
}

View File

@@ -6,45 +6,39 @@ import (
"fmt"
"net/http"
"net/netip"
"strings"
"github.com/qdm12/gluetun/internal/models"
)
type echoip struct {
type ifConfigCo struct {
client *http.Client
url string
}
func newEchoip(client *http.Client, url string) *echoip {
return &echoip{
func newIfConfigCo(client *http.Client) *ifConfigCo {
return &ifConfigCo{
client: client,
url: url,
}
}
func (e *echoip) String() string {
s := e.url
s = strings.TrimPrefix(s, "http://")
s = strings.TrimPrefix(s, "https://")
return s
func (i *ifConfigCo) String() string {
return string(IfConfigCo)
}
func (e *echoip) CanFetchAnyIP() bool {
func (i *ifConfigCo) CanFetchAnyIP() bool {
return true
}
func (e *echoip) Token() string {
func (i *ifConfigCo) Token() string {
return ""
}
// FetchInfo obtains information on the ip address provided
// using the echoip API at the url given. If the ip is the zero value,
// using the ifconfig.co/json API. If the ip is the zero value,
// the public IP address of the machine is used as the IP.
func (e *echoip) FetchInfo(ctx context.Context, ip netip.Addr) (
func (i *ifConfigCo) FetchInfo(ctx context.Context, ip netip.Addr) (
result models.PublicIP, err error,
) {
url := e.url + "/json"
url := "https://ifconfig.co/json"
if ip.IsValid() {
url += "?ip=" + ip.String()
}
@@ -54,7 +48,7 @@ func (e *echoip) FetchInfo(ctx context.Context, ip netip.Addr) (
return result, err
}
response, err := e.client.Do(request)
response, err := i.client.Do(request)
if err != nil {
return result, err
}

View File

@@ -1,86 +0,0 @@
package socks5
import "fmt"
// See https://datatracker.ietf.org/doc/html/rfc1928#section-3
type authMethod byte
const (
authNotRequired authMethod = 0
authGssapi authMethod = 1
authUsernamePassword authMethod = 2
authNotAcceptable authMethod = 255
)
func (a authMethod) String() string {
switch a {
case authNotRequired:
return "no authentication required"
case authGssapi:
return "GSSAPI"
case authUsernamePassword:
return "username/password"
case authNotAcceptable:
return "no acceptable methods"
default:
return fmt.Sprintf("unknown method (%d)", a)
}
}
// Subnegotiation version
// See https://datatracker.ietf.org/doc/html/rfc1929#section-2
const (
authUsernamePasswordSubNegotiation1 byte = 1
)
// SOCKS versions.
const (
socks5Version byte = 5
)
// See https://datatracker.ietf.org/doc/html/rfc1928#section-4
type cmdType byte
const (
connect cmdType = 1
bind cmdType = 2
udpAssociate cmdType = 3
)
func (c cmdType) String() string {
switch c {
case connect:
return "connect"
case bind:
return "bind"
case udpAssociate:
return "UDP associate"
default:
return fmt.Sprintf("unknown command (%d)", c)
}
}
// See https://datatracker.ietf.org/doc/html/rfc1928#section-4 and
// https://datatracker.ietf.org/doc/html/rfc1928#section-5
type addrType byte
const (
ipv4 addrType = 1
domainName addrType = 3
ipv6 addrType = 4
)
// See https://datatracker.ietf.org/doc/html/rfc1928#section-6
type replyCode byte
const (
succeeded replyCode = iota
generalServerFailure
connectionNotAllowedByRuleset
networkUnreachable
hostUnreachable
connectionRefused
ttlExpired
commandNotSupported
addressTypeNotSupported
)

View File

@@ -1,6 +0,0 @@
package socks5
type Logger interface {
Infof(format string, a ...interface{})
Warnf(format string, a ...interface{})
}

View File

@@ -1,103 +0,0 @@
package socks5
import (
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/netip"
)
// See https://datatracker.ietf.org/doc/html/rfc1928#section-6
func (c *socksConn) encodeFailedResponse(writer io.Writer, socksVersion byte, reply replyCode) {
_, err := writer.Write([]byte{
socksVersion,
byte(reply),
0, // RSV byte
// TODO do we need to set the bind addr type to 0??
})
if err != nil {
c.logger.Warnf("failed writing failed response: %s", err)
}
}
// See https://datatracker.ietf.org/doc/html/rfc1928#section-6
func (c *socksConn) encodeSuccessResponse(writer io.Writer, socksVersion byte,
reply replyCode, bindAddrType addrType, bindAddress string,
bindPort uint16) (err error) {
bindData, err := encodeBindData(bindAddrType, bindAddress, bindPort)
if err != nil { // TODO encode with below block if this changes
return err
}
const initialPacketLength = 3
capacity := initialPacketLength + len(bindData)
packet := make([]byte, initialPacketLength, capacity)
packet[0] = socksVersion
packet[1] = byte(reply)
packet[2] = 0 // RSV byte
packet = append(packet, bindData...)
_, err = writer.Write(packet)
if err != nil {
c.logger.Warnf("failed writing success response: %s", err)
}
return nil
}
var (
ErrIPVersionUnexpected = errors.New("ip version is unexpected")
ErrDomainNameTooLong = errors.New("domain name is too long")
)
func encodeBindData(addrType addrType, address string, port uint16) (
data []byte, err error) {
capacity := bindDataLength(addrType, address)
data = make([]byte, 0, capacity)
data = append(data, byte(addrType))
switch addrType {
case ipv4, ipv6:
ip, err := netip.ParseAddr(address)
if err != nil {
return nil, fmt.Errorf("parsing IP address: %w", err)
}
switch {
case addrType == ipv4 && !ip.Is4():
return nil, fmt.Errorf("%w: expected IPv4 for %s", ErrIPVersionUnexpected, ip)
case addrType == ipv6 && !ip.Is6():
return nil, fmt.Errorf("%w: expected IPv6 for %s", ErrIPVersionUnexpected, ip)
}
data = append(data, ip.AsSlice()...)
case domainName:
const maxDomainNameLength = 255
if len(address) > maxDomainNameLength {
return nil, fmt.Errorf("%w: %s", ErrDomainNameTooLong, address)
}
data = append(data, byte(len(address)))
data = append(data, []byte(address)...)
default:
panic(fmt.Sprintf("unsupported address type %d", addrType))
}
data = binary.BigEndian.AppendUint16(data, port)
return data, nil
}
func bindDataLength(addrType addrType, address string) (maxLength int) {
maxLength++ // address type
switch addrType {
case ipv4:
maxLength += net.IPv4len
case domainName:
maxLength++ // domain name length
maxLength += len([]byte(address))
case ipv6:
maxLength += net.IPv6len
default:
panic("unsupported address type: " + fmt.Sprint(addrType))
}
maxLength += 2 // port
return maxLength
}

View File

@@ -1,105 +0,0 @@
package socks5
import (
"context"
"fmt"
"net"
"sync"
"sync/atomic"
)
type Server struct {
username string
password string
address string
logger Logger
// internal fields
listener net.Listener
listening atomic.Bool
socksConnCtx context.Context //nolint:containedctx
socksConnCancel context.CancelFunc
done <-chan struct{}
stopping atomic.Bool
}
func New(settings Settings) *Server {
return &Server{
username: settings.Username,
password: settings.Password,
address: settings.Address,
logger: settings.Logger,
}
}
func (s *Server) Start(_ context.Context) (runErr <-chan error, err error) {
s.listener, err = net.Listen("tcp", s.address)
if err != nil {
return nil, fmt.Errorf("listening on %s: %w", s.address, err)
}
s.listening.Store(true)
s.socksConnCtx, s.socksConnCancel = context.WithCancel(context.Background())
ready := make(chan struct{})
runErrCh := make(chan error)
runErr = runErrCh
done := make(chan struct{})
s.done = done
go s.runServer(ready, runErrCh, done)
<-ready
return runErr, nil
}
func (s *Server) runServer(ready chan<- struct{},
runErrCh chan<- error, done chan<- struct{}) {
close(ready)
defer close(done)
wg := new(sync.WaitGroup)
defer wg.Wait()
dialer := &net.Dialer{}
for {
connection, err := s.listener.Accept()
if err != nil {
if !s.stopping.Load() {
_ = s.Stop()
runErrCh <- fmt.Errorf("accepting connection: %w", err)
}
return
}
wg.Add(1)
go func(ctx context.Context, connection net.Conn,
dialer *net.Dialer, wg *sync.WaitGroup) {
defer wg.Done()
socksConn := &socksConn{
dialer: dialer,
username: s.username,
password: s.password,
clientConn: connection,
logger: s.logger,
}
err := socksConn.run(ctx)
if err != nil {
s.logger.Infof("running socks connection: %s", err)
}
}(s.socksConnCtx, connection, dialer, wg)
}
}
func (s *Server) Stop() (err error) {
s.stopping.Store(true)
s.listening.Store(false)
err = s.listener.Close()
s.socksConnCancel() // stop ongoing socks connections
<-s.done // wait for run goroutine to finish
s.stopping.Store(false)
return err
}
func (s *Server) listeningAddress() net.Addr {
if s.listening.Load() {
return s.listener.Addr()
}
return nil
}

View File

@@ -1,8 +0,0 @@
package socks5
type Settings struct {
Username string
Password string
Address string
Logger Logger
}

View File

@@ -1,283 +0,0 @@
package socks5
import (
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/netip"
"strconv"
"strings"
)
type socksConn struct {
// Injected fields
dialer *net.Dialer
username string
password string
clientConn net.Conn
logger Logger
}
func (c *socksConn) closeClientConn(ctxErr error) {
err := c.clientConn.Close()
if err != nil && ctxErr == nil {
c.logger.Warnf("closing client connection: %s", err)
}
}
func (c *socksConn) run(ctx context.Context) error {
authMethod := authNotRequired
if c.username != "" || c.password != "" {
authMethod = authUsernamePassword
}
err := verifyFirstNegotiation(c.clientConn, authMethod)
if err != nil {
replyMethod := authMethod
if errors.Is(err, ErrNoMethodIdentifiers) || errors.Is(err, ErrNoValidMethodIdentifier) {
replyMethod = authNotAcceptable
}
_, writeErr := c.clientConn.Write([]byte{socks5Version, byte(replyMethod)})
if writeErr != nil {
c.logger.Warnf("failed writing first negotiation reply: %s", writeErr)
}
c.closeClientConn(ctx.Err())
return fmt.Errorf("verifying first negotiation: %w", err)
}
_, err = c.clientConn.Write([]byte{socks5Version, byte(authMethod)})
if err != nil {
c.closeClientConn(ctx.Err())
return fmt.Errorf("writing first negotiation reply: %w", err)
}
switch authMethod {
case authNotRequired, authNotAcceptable:
case authGssapi:
panic("not implemented")
// TODO
case authUsernamePassword:
// See https://datatracker.ietf.org/doc/html/rfc1929#section-2
err = usernamePasswordSubnegotiate(c.clientConn, c.username, c.password)
if err != nil {
// If the server returns a `failure' (STATUS value other than X'00') status,
// it MUST close the connection.
c.closeClientConn(ctx.Err())
return fmt.Errorf("subnegotiating username and password: %w", err)
}
default:
panic(fmt.Sprintf("unimplemented auth method %d", authMethod))
}
err = c.handleRequest(ctx)
c.closeClientConn(ctx.Err())
if err != nil {
return fmt.Errorf("handling request: %w", err)
}
return nil
}
var (
ErrCommandNotSupported = errors.New("command not supported")
)
func (c *socksConn) handleRequest(ctx context.Context) error {
const socksVersion = socks5Version
request, err := decodeRequest(c.clientConn, socksVersion)
if err != nil {
c.encodeFailedResponse(c.clientConn, socksVersion, generalServerFailure)
return err
}
if request.command != connect {
c.encodeFailedResponse(c.clientConn, socksVersion, commandNotSupported)
return fmt.Errorf("%w: %s", ErrCommandNotSupported, request.command)
}
destinationAddress := net.JoinHostPort(request.destination, fmt.Sprint(request.port))
destinationConn, err := c.dialer.DialContext(ctx, "tcp", destinationAddress)
if err != nil {
c.encodeFailedResponse(c.clientConn, socksVersion, generalServerFailure)
return err
}
defer destinationConn.Close()
destinationServerAddress := destinationConn.LocalAddr().String()
destinationAddr, destinationPortStr, err := net.SplitHostPort(destinationServerAddress)
fmt.Println("===", destinationServerAddress)
if err != nil {
return err
}
destinationPort, err := strconv.Atoi(destinationPortStr)
if err != nil {
return err
}
var bindAddrType addrType
if ip := net.ParseIP(destinationAddr); ip != nil {
if ip.To4() != nil {
bindAddrType = ipv4
} else {
bindAddrType = ipv6
}
} else {
bindAddrType = domainName
}
err = c.encodeSuccessResponse(c.clientConn, socksVersion, succeeded, bindAddrType,
destinationAddr, uint16(destinationPort))
if err != nil {
c.encodeFailedResponse(c.clientConn, socksVersion, generalServerFailure)
return fmt.Errorf("writing successful %s response: %w", request.command, err)
}
errc := make(chan error)
go func() {
_, err := io.Copy(c.clientConn, destinationConn)
if err != nil {
err = fmt.Errorf("from backend to client: %w", err)
}
errc <- err
}()
go func() {
_, err := io.Copy(destinationConn, c.clientConn)
if err != nil {
err = fmt.Errorf("from client to backend: %w", err)
}
errc <- err
}()
select {
case err := <-errc:
return err
case <-ctx.Done():
_ = destinationConn.Close()
_ = c.clientConn.Close()
return nil
}
}
var (
ErrVersionNotSupported = errors.New("version not supported")
ErrNoMethodIdentifiers = errors.New("no method identifiers")
)
// See https://datatracker.ietf.org/doc/html/rfc1928#section-3
func verifyFirstNegotiation(reader io.Reader, requiredMethod authMethod) error {
const headerLength = 2 // version + nMethods bytes
header := make([]byte, headerLength)
_, err := io.ReadFull(reader, header[:])
if err != nil {
return fmt.Errorf("reading header: %w", err)
}
if header[0] != socks5Version {
return fmt.Errorf("%w: %d", ErrVersionNotSupported, header[0])
}
nMethods := header[1]
if nMethods == 0 {
return fmt.Errorf("%w", ErrNoMethodIdentifiers)
}
methodIdentifiers := make([]byte, nMethods)
_, err = io.ReadFull(reader, methodIdentifiers)
if err != nil {
return fmt.Errorf("reading method identifiers: %w", err)
}
for _, methodIdentifier := range methodIdentifiers {
if methodIdentifier == byte(requiredMethod) {
return nil
}
}
return makeNoAcceptableMethodError(requiredMethod, methodIdentifiers)
}
var (
ErrNoValidMethodIdentifier = errors.New("no valid method identifier")
)
func makeNoAcceptableMethodError(requiredAuthMethod authMethod, methodIdentifiers []byte) error {
methodNames := make([]string, len(methodIdentifiers))
for i, methodIdentifier := range methodIdentifiers {
methodNames[i] = fmt.Sprintf("%q", authMethod(methodIdentifier))
}
return fmt.Errorf("%w: none of %s matches %s",
ErrNoValidMethodIdentifier, strings.Join(methodNames, ", "),
requiredAuthMethod)
}
// See https://datatracker.ietf.org/doc/html/rfc1928#section-4
type request struct {
command cmdType
destination string
port uint16
addressType addrType
}
var (
ErrRequestSocksVersionMismatch = errors.New("request SOCKS version mismatch")
ErrAddressTypeNotSupported = errors.New("address type not supported")
)
func decodeRequest(reader io.Reader, expectedVersion byte) (req request, err error) {
const headerLength = 4
header := [headerLength]byte{}
_, err = io.ReadFull(reader, header[:])
if err != nil {
return request{}, fmt.Errorf("reading header: %w", err)
}
version := header[0]
if header[0] != expectedVersion {
return request{}, fmt.Errorf("%w: expected %d and got %d",
ErrRequestSocksVersionMismatch, expectedVersion, version)
}
req.command = cmdType(header[1])
// header[2] is RSV byte
req.addressType = addrType(header[3])
switch req.addressType {
case ipv4:
var ip [4]byte
_, err = io.ReadFull(reader, ip[:])
if err != nil {
return request{}, fmt.Errorf("reading IPv4 address: %w", err)
}
req.destination = netip.AddrFrom4(ip).String()
case ipv6:
var ip [16]byte
_, err = io.ReadFull(reader, ip[:])
if err != nil {
return request{}, fmt.Errorf("reading IPv6 address: %w", err)
}
req.destination = netip.AddrFrom16(ip).String()
case domainName:
var header [1]byte
_, err = io.ReadFull(reader, header[:])
if err != nil {
return request{}, fmt.Errorf("reading domain name header: %w", err)
}
domainName := make([]byte, header[0])
_, err = io.ReadFull(reader, domainName)
if err != nil {
return request{}, fmt.Errorf("reading domain name bytes: %w", err)
}
req.destination = string(domainName)
default:
return request{}, fmt.Errorf("%w: %d", ErrAddressTypeNotSupported, req.addressType)
}
var portBytes [2]byte
_, err = io.ReadFull(reader, portBytes[:])
if err != nil {
return request{}, fmt.Errorf("reading port: %w", err)
}
req.port = binary.BigEndian.Uint16(portBytes[:])
return req, nil
}

View File

@@ -1,175 +0,0 @@
package socks5
import (
"context"
"fmt"
"io"
"net"
"testing"
"time"
"github.com/qdm12/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/proxy"
)
func Test(t *testing.T) {
server := New(Settings{
Username: "test",
Password: "test",
Address: ":8000",
Logger: log.New(),
})
runErr, startErr := server.Start(context.Background())
require.NoError(t, startErr)
select {
case err := <-runErr:
require.NoError(t, err)
default:
}
t.Log("SlEEPING")
time.Sleep(15 * time.Second)
t.Log("Done sleeping")
err := server.Stop()
require.NoError(t, err)
}
func backendServer(listener net.Listener) {
conn, err := listener.Accept()
if err != nil {
panic(err)
}
conn.Write([]byte("Test"))
conn.Close()
listener.Close()
}
func TestRead(t *testing.T) {
// backend server which we'll use SOCKS5 to connect to
listener, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
backendServerPort := listener.Addr().(*net.TCPAddr).Port
go backendServer(listener)
// SOCKS5 server
server := New(Settings{
Address: ":0",
})
_, err = server.Start(context.Background())
require.NoError(t, err)
t.Cleanup(func() {
err = server.Stop()
assert.NoError(t, err)
})
socks5Port := server.listeningAddress().(*net.TCPAddr).Port
addr := fmt.Sprintf("localhost:%d", socks5Port)
socksDialer, err := proxy.SOCKS5("tcp", addr, nil, proxy.Direct)
if err != nil {
t.Fatal(err)
}
addr = fmt.Sprintf("localhost:%d", backendServerPort)
conn, err := socksDialer.Dial("tcp", addr)
if err != nil {
t.Fatal(err)
}
buf := make([]byte, 4)
_, err = io.ReadFull(conn, buf)
if err != nil {
t.Fatal(err)
}
if string(buf) != "Test" {
t.Fatalf("got: %q want: Test", buf)
}
err = conn.Close()
if err != nil {
t.Fatal(err)
}
}
func TestReadPassword(t *testing.T) {
// backend server which we'll use SOCKS5 to connect to
ln, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
backendServerPort := ln.Addr().(*net.TCPAddr).Port
go backendServer(ln)
auth := &proxy.Auth{User: "foo", Password: "bar"}
server := Server{
logger: log.New(),
username: auth.User,
password: auth.Password,
address: ":0",
}
_, err = server.Start(context.Background())
require.NoError(t, err)
t.Cleanup(func() {
err = server.Stop()
assert.NoError(t, err)
})
addr := fmt.Sprintf("localhost:%d", server.listeningAddress().(*net.TCPAddr).Port)
if d, err := proxy.SOCKS5("tcp", addr, nil, proxy.Direct); err != nil {
t.Fatal(err)
} else {
if _, err := d.Dial("tcp", addr); err == nil {
t.Fatal("expected no-auth dial error")
}
}
badPwd := &proxy.Auth{User: "foo", Password: "not right"}
if d, err := proxy.SOCKS5("tcp", addr, badPwd, proxy.Direct); err != nil {
t.Fatal(err)
} else {
if _, err := d.Dial("tcp", addr); err == nil {
t.Fatal("expected bad password dial error")
}
}
badUsr := &proxy.Auth{User: "not right", Password: "bar"}
if d, err := proxy.SOCKS5("tcp", addr, badUsr, proxy.Direct); err != nil {
t.Fatal(err)
} else {
if _, err := d.Dial("tcp", addr); err == nil {
t.Fatal("expected bad username dial error")
}
}
socksDialer, err := proxy.SOCKS5("tcp", addr, auth, proxy.Direct)
if err != nil {
t.Fatal(err)
}
addr = fmt.Sprintf("localhost:%d", backendServerPort)
conn, err := socksDialer.Dial("tcp", addr)
if err != nil {
t.Fatal(err)
}
buf := make([]byte, 4)
if _, err := io.ReadFull(conn, buf); err != nil {
t.Fatal(err)
}
if string(buf) != "Test" {
t.Fatalf("got: %q want: Test", buf)
}
if err := conn.Close(); err != nil {
t.Fatal(err)
}
}

View File

@@ -1,69 +0,0 @@
package socks5
import (
"errors"
"fmt"
"io"
)
var (
ErrSubnegotiationVersionNotSupported = errors.New("subnegotiation version not supported")
ErrUsernameNotValid = errors.New("username not valid")
ErrPasswordNotValid = errors.New("password not valid")
)
// See https://datatracker.ietf.org/doc/html/rfc1929#section-2
func usernamePasswordSubnegotiate(conn io.ReadWriter, username, password string) (err error) {
status := byte(1)
const defaultVersion = byte(1)
const headerLength = 2
var header [headerLength]byte
_, err = io.ReadFull(conn, header[:])
if err != nil {
_, _ = conn.Write([]byte{defaultVersion, status})
return fmt.Errorf("reading header: %w", err)
}
if header[0] != authUsernamePasswordSubNegotiation1 {
_, _ = conn.Write([]byte{defaultVersion, status})
return fmt.Errorf("%w: %d", ErrSubnegotiationVersionNotSupported, header[0])
}
version := header[0]
usernameBytes := make([]byte, header[1])
_, err = io.ReadFull(conn, usernameBytes)
if err != nil {
_, _ = conn.Write([]byte{version, status})
return fmt.Errorf("reading username bytes: %w", err)
} else if username != string(usernameBytes) {
_, _ = conn.Write([]byte{version, status})
return fmt.Errorf("%w: %s", ErrUsernameNotValid, string(usernameBytes))
}
const passwordHeaderLength = 1
passwordHeader := make([]byte, passwordHeaderLength)
_, err = io.ReadFull(conn, passwordHeader[:])
if err != nil {
_, _ = conn.Write([]byte{version, status})
return fmt.Errorf("reading password length: %w", err)
}
passwordBytes := make([]byte, passwordHeader[0])
_, err = io.ReadFull(conn, passwordBytes)
if err != nil {
_, _ = conn.Write([]byte{version, status})
return fmt.Errorf("reading password bytes: %w", err)
} else if password != string(passwordBytes) {
_, _ = conn.Write([]byte{version, status})
return fmt.Errorf("%w: %s", ErrPasswordNotValid, string(passwordBytes))
}
status = 0
_, err = conn.Write([]byte{version, status})
if err != nil {
return fmt.Errorf("writing success status: %w", err)
}
return nil
}