Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
40812fc
fix(firewall): flush conntrack table after enabling firewall at conta…
qdm12 Feb 21, 2026
ee8d62e
purevpn: switch updater to linux deb local-data and protocol ports
Feb 26, 2026
2a49d27
purevpn: improve resolver fallback and update OpenVPN port defaults
Feb 26, 2026
388e628
purevpn updater: fetch live inventory from app-derived URL
Feb 27, 2026
dbe31bc
purevpn: add hostname-trait server type selection
Feb 27, 2026
32ff15d
purevpn: add deterministic hostname-code location filters
Feb 27, 2026
f81d061
purevpn: add template ingestion and p2p server type support
Feb 27, 2026
6838917
purevpn: merge app local data and add multi-trait filter tests
Feb 27, 2026
2e88ef2
purevpn: switch to city-only filters and match-all tags
Feb 27, 2026
f7b0e54
chore: remove local purevpn test harness scripts from repo
Feb 27, 2026
5a6b7c9
purevpn: add OpenVPN fallback remote lines for port 1194
Feb 27, 2026
57662fc
Merge origin/master into codex/purevpn-deb-updater
Feb 27, 2026
f9ec7ba
chore: remove CSV/TSV PureVPN export tests from repo
Feb 27, 2026
a4afbe4
purevpn: expand city code mappings and tighten inventory-port tests
Feb 27, 2026
eac0f01
purevpn: remove atom secret env override and related tests
Feb 27, 2026
bfe558d
purevpn: remove fallback ports and reseller uid parsing test
Feb 27, 2026
9c63460
chore(deps): tidy purevpn module dependencies
Feb 27, 2026
b89cbbf
purevpn: apply review feedback and split selector features out
Mar 3, 2026
7253ec1
Merge upstream/master into codex/purevpn-deb-updater
Mar 3, 2026
f0247ff
Merge branch 'master' into codex/purevpn-deb-updater
reedog117 Mar 20, 2026
164d724
Merge branch 'master' into codex/purevpn-deb-updater
reedog117 Mar 27, 2026
4ed9152
Merge branch 'master' into codex/purevpn-deb-updater
qdm12 Jun 11, 2026
9475f9c
wip
qdm12 Jun 23, 2026
0cb48ad
wip2
qdm12 Jun 23, 2026
6911705
Merge origin/master into pr-3165
devin-ai-integration[bot] Jul 10, 2026
9713273
fix(purevpn): advance past port_number marker when parsing local-data…
reedog117 Jul 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions cmd/gluetun/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
parallelResolver := resolver.NewParallelResolver(dohDialer)
openvpnFileExtractor := extract.New()
providers := provider.NewProviders(storage, time.Now, updaterLogger,
httpClient, unzipper, parallelResolver, publicIPLooper.Fetcher(),
openvpnFileExtractor, allSettings.Updater)
httpClient, unzipper, parallelResolver, openvpnFileExtractor, allSettings.Updater)

boringPollLogger := logger.New(log.SetComponent("boring poll"))
boringPoll := boringpoll.New(httpClient, boringPollLogger, allSettings.BoringPoll)
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ require (
github.com/klauspost/pgzip v1.2.6
github.com/mdlayher/genetlink v1.4.0
github.com/mdlayher/netlink v1.9.0
github.com/miekg/dns v1.1.62
github.com/pelletier/go-toml/v2 v2.2.4
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a
github.com/qdm12/gluetun-servers v0.1.0
github.com/qdm12/gluetun-servers v0.1.1-0.20260623154528-d1b2c9a591ba
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978
github.com/qdm12/gosettings v0.4.4
github.com/qdm12/goshutdown v0.3.0
Expand Down Expand Up @@ -47,7 +48,6 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/socket v0.6.0 // indirect
github.com/miekg/dns v1.1.62 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a h1:TE157yPQmAbVruH0MWCQzs0vTT/6t96DkoWUXd6PVuc=
github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE=
github.com/qdm12/gluetun-servers v0.1.0 h1:w9JLghKZwI0Gzpp9p5rNANgEYUUZ1dxdxsG6NKIojaY=
github.com/qdm12/gluetun-servers v0.1.0/go.mod h1:acttuyHyoFDu6GTbf3kAV+QXeiX8oJeh0MBic67/9z8=
github.com/qdm12/gluetun-servers v0.1.1-0.20260623154528-d1b2c9a591ba h1:YQf3B6NSRJrWuIsbbHlzh4dpdYmAqMJrKPyvL7cvc54=
github.com/qdm12/gluetun-servers v0.1.1-0.20260623154528-d1b2c9a591ba/go.mod h1:acttuyHyoFDu6GTbf3kAV+QXeiX8oJeh0MBic67/9z8=
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c=
github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg=
github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4=
Expand Down
3 changes: 1 addition & 2 deletions internal/cli/openvpnconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,10 @@ func (c *CLI) OpenvpnConfig(logger OpenvpnConfigLogger, reader *reader.Reader,
client := (*http.Client)(nil)
warner := (Warner)(nil)
parallelResolver := (ParallelResolver)(nil)
ipFetcher := (IPFetcher)(nil)
openvpnFileExtractor := extract.New()

providers := provider.NewProviders(storage, time.Now, warner, client,
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, allSettings.Updater)
unzipper, parallelResolver, openvpnFileExtractor, allSettings.Updater)
providerConf := providers.Get(allSettings.VPN.Provider.Name)
connection, err := providerConf.GetConnection(
allSettings.VPN.Provider.ServerSelection, ipv6SupportLevel == netlink.IPv6Internet)
Expand Down
14 changes: 1 addition & 13 deletions internal/cli/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/openvpn/extract"
"github.com/qdm12/gluetun/internal/provider"
"github.com/qdm12/gluetun/internal/publicip/api"
"github.com/qdm12/gluetun/internal/updater"
"github.com/qdm12/gluetun/internal/updater/resolver"
"github.com/qdm12/gluetun/internal/updater/unzip"
Expand Down Expand Up @@ -114,21 +113,10 @@ func (c *CLI) Update(ctx context.Context, args []string, logger UpdaterLogger) e
httpClient := &http.Client{Timeout: clientTimeout}
unzipper := unzip.New(httpClient)
parallelResolver := resolver.NewParallelResolver(dnsDialer)
nameTokenPairs := []api.NameToken{
{Name: string(api.IPInfo), Token: ipToken},
{Name: string(api.IP2Location)},
{Name: string(api.IfConfigCo)},
}
fetchers, err := api.New(nameTokenPairs, httpClient)
if err != nil {
return fmt.Errorf("creating public IP fetchers: %w", err)
}
ipFetcher := api.NewResilient(fetchers, logger)

openvpnFileExtractor := extract.New()

providers := provider.NewProviders(storage, time.Now, logger, httpClient,
unzipper, parallelResolver, ipFetcher, openvpnFileExtractor, options)
unzipper, parallelResolver, openvpnFileExtractor, options)

updater := updater.New(httpClient, storage, providers, logger, *options.PreferDirectDownload)
err = updater.UpdateServers(ctx, options.Providers, options.MinRatio)
Expand Down
5 changes: 4 additions & 1 deletion internal/configuration/settings/openvpnselection.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
// no custom port allowed
case providers.Expressvpn, providers.Fastestvpn,
providers.Giganews, providers.Ipvanish,
providers.Nordvpn, providers.Purevpn,
providers.Nordvpn,
providers.Surfshark, providers.VPNSecure,
providers.VPNUnlimited, providers.Vyprvpn:
return fmt.Errorf("custom endpoint port is not allowed: for VPN service provider %s", vpnProvider)
Expand Down Expand Up @@ -105,6 +105,9 @@ func (o OpenVPNSelection) validate(vpnProvider string) (err error) {
case providers.Protonvpn:
allowedTCP = []uint16{443, 5995, 8443}
allowedUDP = []uint16{80, 443, 1194, 4569, 5060, 51820}
case providers.Purevpn:
allowedTCP = []uint16{80, 443}
allowedUDP = []uint16{53, 1194, 15021}
case providers.SlickVPN:
allowedTCP = []uint16{443, 8080, 8888}
allowedUDP = []uint16{443, 8080, 8888}
Expand Down
61 changes: 61 additions & 0 deletions internal/configuration/settings/openvpnselection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package settings

import (
"testing"

"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_OpenVPNSelection_validate(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
selection OpenVPNSelection
provider string
errMessage string
}{
"purevpn_default_selection_valid": {
selection: openVPNSelectionForValidation(providers.Purevpn),
provider: providers.Purevpn,
},
"purevpn_TCP_without_custom_port_valid": {
selection: func() OpenVPNSelection {
s := openVPNSelectionForValidation(providers.Purevpn)
s.Protocol = constants.TCP
return s
}(),
provider: providers.Purevpn,
},
"purevpn_custom_port_accepted": {
selection: func() OpenVPNSelection {
s := openVPNSelectionForValidation(providers.Purevpn)
*s.CustomPort = 1194
return s
}(),
provider: providers.Purevpn,
},
}

for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()

err := testCase.selection.validate(testCase.provider)
if testCase.errMessage == "" {
require.NoError(t, err)
} else {
require.Error(t, err)
assert.Contains(t, err.Error(), testCase.errMessage)
}
})
}
}

func openVPNSelectionForValidation(provider string) OpenVPNSelection {
selection := OpenVPNSelection{}
selection.setDefaults(provider)
return selection
}
72 changes: 45 additions & 27 deletions internal/models/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,33 @@ import (
type Server struct {
VPN string `json:"vpn,omitempty"`
// Surfshark: country is also used for multi-hop
Country string `json:"country,omitempty"`
Region string `json:"region,omitempty"`
City string `json:"city,omitempty"`
ISP string `json:"isp,omitempty"`
Categories []string `json:"categories,omitempty"`
Owned bool `json:"owned,omitempty"`
Number uint16 `json:"number,omitempty"`
ServerName string `json:"server_name,omitempty"`
Hostname string `json:"hostname,omitempty"`
TCP bool `json:"tcp,omitempty"`
UDP bool `json:"udp,omitempty"`
OvpnX509 string `json:"x509,omitempty"`
RetroLoc string `json:"retroloc,omitempty"` // TODO remove in v4
MultiHop bool `json:"multihop,omitempty"`
WgPubKey string `json:"wgpubkey,omitempty"`
Free bool `json:"free,omitempty"` // TODO v4 create a SubscriptionTier struct
Premium bool `json:"premium,omitempty"`
Stream bool `json:"stream,omitempty"` // TODO v4 create a Features struct
SecureCore bool `json:"secure_core,omitempty"`
Tor bool `json:"tor,omitempty"`
PortForward bool `json:"port_forward,omitempty"`
Keep bool `json:"keep,omitempty"`
IPs []netip.Addr `json:"ips,omitempty"`
Country string `json:"country,omitempty"`
Region string `json:"region,omitempty"`
City string `json:"city,omitempty"`
ISP string `json:"isp,omitempty"`
Categories []string `json:"categories,omitempty"`
Owned bool `json:"owned,omitempty"`
Number uint16 `json:"number,omitempty"`
ServerName string `json:"server_name,omitempty"`
Hostname string `json:"hostname,omitempty"`
TCP bool `json:"tcp,omitempty"`
UDP bool `json:"udp,omitempty"`
TCPPorts []uint16 `json:"tcp_ports,omitempty"`
UDPPorts []uint16 `json:"udp_ports,omitempty"`
OvpnX509 string `json:"x509,omitempty"`
RetroLoc string `json:"retroloc,omitempty"` // TODO remove in v4
MultiHop bool `json:"multihop,omitempty"`
WgPubKey string `json:"wgpubkey,omitempty"`
Free bool `json:"free,omitempty"` // TODO v4 create a SubscriptionTier struct
Premium bool `json:"premium,omitempty"`
Stream bool `json:"stream,omitempty"` // TODO v4 create a Features struct
SecureCore bool `json:"secure_core,omitempty"`
Tor bool `json:"tor,omitempty"`
PortForward bool `json:"port_forward,omitempty"`
QuantumResistant bool `json:"quantum_resistant,omitempty"`
Obfuscated bool `json:"obfuscated,omitempty"`
Keep bool `json:"keep,omitempty"`
IPs []netip.Addr `json:"ips,omitempty"`
}

func (s *Server) HasMinimumInformation() (err error) {
Expand All @@ -56,25 +60,39 @@ func (s *Server) HasMinimumInformation() (err error) {
}

func (s *Server) Equal(other Server) (equal bool) {
if !ipsAreEqual(s.IPs, other.IPs) {
if !comparablesAreEqualNoOrder(s.IPs, other.IPs) ||
!comparablesAreEqualNoOrder(s.TCPPorts, other.TCPPorts) ||
!comparablesAreEqualNoOrder(s.UDPPorts, other.UDPPorts) {
return false
}

serverCopy := *s
serverCopy.IPs = nil
serverCopy.TCPPorts = nil
serverCopy.UDPPorts = nil
other.IPs = nil
other.TCPPorts = nil
other.UDPPorts = nil
return reflect.DeepEqual(serverCopy, other)
}

func ipsAreEqual(a, b []netip.Addr) (equal bool) {
func comparablesAreEqualNoOrder[T comparable](a, b []T) bool {
if len(a) != len(b) {
return false
}

for i := range a {
if a[i].Compare(b[i]) != 0 {
// Track element frequencies
counts := make(map[T]int, len(a))
for _, v := range a {
counts[v]++
}

// Decrement counts and check for mismatches
for _, v := range b {
if counts[v] == 0 {
return false
}
counts[v]--
}

return true
Expand Down
96 changes: 52 additions & 44 deletions internal/models/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,52 +43,60 @@ func Test_Server_Equal(t *testing.T) {
},
"all fields equal": {
a: &Server{
VPN: "vpn",
Country: "country",
Region: "region",
City: "city",
ISP: "isp",
Owned: true,
Number: 1,
ServerName: "server_name",
Hostname: "hostname",
TCP: true,
UDP: true,
OvpnX509: "x509",
RetroLoc: "retroloc",
MultiHop: true,
WgPubKey: "wgpubkey",
Free: true,
Stream: true,
SecureCore: true,
Tor: true,
PortForward: true,
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
Keep: true,
VPN: "vpn",
Country: "country",
Region: "region",
City: "city",
ISP: "isp",
Owned: true,
Number: 1,
ServerName: "server_name",
Hostname: "hostname",
TCP: true,
UDP: true,
TCPPorts: []uint16{123, 456},
UDPPorts: []uint16{678, 901},
OvpnX509: "x509",
RetroLoc: "retroloc",
MultiHop: true,
WgPubKey: "wgpubkey",
Free: true,
Stream: true,
SecureCore: true,
Tor: true,
PortForward: true,
QuantumResistant: true,
Obfuscated: true,
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
Keep: true,
},
b: Server{
VPN: "vpn",
Country: "country",
Region: "region",
City: "city",
ISP: "isp",
Owned: true,
Number: 1,
ServerName: "server_name",
Hostname: "hostname",
TCP: true,
UDP: true,
OvpnX509: "x509",
RetroLoc: "retroloc",
MultiHop: true,
WgPubKey: "wgpubkey",
Free: true,
Stream: true,
SecureCore: true,
Tor: true,
PortForward: true,
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
Keep: true,
VPN: "vpn",
Country: "country",
Region: "region",
City: "city",
ISP: "isp",
Owned: true,
Number: 1,
ServerName: "server_name",
Hostname: "hostname",
TCP: true,
UDP: true,
TCPPorts: []uint16{456, 123},
UDPPorts: []uint16{901, 678},
OvpnX509: "x509",
RetroLoc: "retroloc",
MultiHop: true,
WgPubKey: "wgpubkey",
Free: true,
Stream: true,
SecureCore: true,
Tor: true,
PortForward: true,
QuantumResistant: true,
Obfuscated: true,
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
Keep: true,
},
equal: true,
},
Expand Down
6 changes: 3 additions & 3 deletions internal/provider/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ type Extractor interface {

func NewProviders(storage Storage, timeNow func() time.Time,
updaterWarner common.Warner, client *http.Client, unzipper common.Unzipper,
parallelResolver common.ParallelResolver, ipFetcher common.IPFetcher,
extractor custom.Extractor, credentials settings.Updater,
parallelResolver common.ParallelResolver, extractor custom.Extractor,
credentials settings.Updater,
) *Providers {
//nolint:lll
providerNameToProvider := map[string]Provider{
Expand All @@ -72,7 +72,7 @@ func NewProviders(storage Storage, timeNow func() time.Time,
providers.PrivateInternetAccess: privateinternetaccess.New(storage, timeNow, client),
providers.Privatevpn: privatevpn.New(storage, unzipper, updaterWarner, parallelResolver),
providers.Protonvpn: protonvpn.New(storage, client, updaterWarner, *credentials.ProtonEmail, *credentials.ProtonPassword),
providers.Purevpn: purevpn.New(storage, ipFetcher, unzipper, updaterWarner, parallelResolver),
providers.Purevpn: purevpn.New(storage, client, unzipper, updaterWarner, parallelResolver),
providers.SlickVPN: slickvpn.New(storage, client, updaterWarner, parallelResolver),
providers.Surfshark: surfshark.New(storage, client, unzipper, updaterWarner, parallelResolver),
providers.Torguard: torguard.New(storage, unzipper, updaterWarner, parallelResolver),
Expand Down
Loading