Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 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
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
4 changes: 4 additions & 0 deletions .env.purevpn.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
PUREVPN_USER=your-username
PUREVPN_PASSWORD=your-password
# Optional timezone for container logs
TZ=UTC
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
scratch.txt
.env.purevpn
.DS_Store
62 changes: 62 additions & 0 deletions internal/configuration/settings/openvpnselection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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
err error
}{
"purevpn default selection is valid": {
selection: openVPNSelectionForValidation(providers.Purevpn),
provider: providers.Purevpn,
},
"purevpn TCP without custom port is valid": {
selection: func() OpenVPNSelection {
s := openVPNSelectionForValidation(providers.Purevpn)
s.Protocol = constants.TCP
return s
}(),
provider: providers.Purevpn,
},
"purevpn custom port is rejected": {
selection: func() OpenVPNSelection {
s := openVPNSelectionForValidation(providers.Purevpn)
*s.CustomPort = 1194
return s
}(),
provider: providers.Purevpn,
err: ErrOpenVPNCustomPortNotAllowed,
},
}

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

err := testCase.selection.validate(testCase.provider)
if testCase.err == nil {
require.NoError(t, err)
return
}
require.Error(t, err)
assert.ErrorIs(t, err, testCase.err)
})
}
}

func openVPNSelectionForValidation(provider string) OpenVPNSelection {
selection := OpenVPNSelection{}
selection.setDefaults(provider)
return selection
}
4 changes: 4 additions & 0 deletions internal/configuration/settings/serverselection.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
*ss = nordvpnRetroRegion(*ss, filterChoices.Regions, filterChoices.Countries)
case providers.Surfshark:
*ss = surfsharkRetroRegion(*ss)
case providers.Purevpn:
// Keep parsing SERVER_REGIONS for retro-compatibility, but
// do not apply it to PureVPN filtering.
ss.Regions = nil
}

err = validateServerFilters(*ss, filterChoices, vpnServiceProvider, warner)
Expand Down
50 changes: 27 additions & 23 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"`
}

var (
Expand Down
92 changes: 48 additions & 44 deletions internal/models/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,52 +43,56 @@ 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,
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,
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
2 changes: 1 addition & 1 deletion internal/provider/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func NewProviders(storage Storage, timeNow func() time.Time,
providers.PrivateInternetAccess: privateinternetaccess.New(storage, randSource, timeNow, client),
providers.Privatevpn: privatevpn.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
providers.Protonvpn: protonvpn.New(storage, randSource, client, updaterWarner, *credentials.ProtonEmail, *credentials.ProtonPassword),
providers.Purevpn: purevpn.New(storage, randSource, ipFetcher, unzipper, updaterWarner, parallelResolver),
providers.Purevpn: purevpn.New(storage, randSource, client, ipFetcher, unzipper, updaterWarner, parallelResolver),
providers.SlickVPN: slickvpn.New(storage, randSource, client, updaterWarner, parallelResolver),
providers.Surfshark: surfshark.New(storage, randSource, client, unzipper, updaterWarner, parallelResolver),
providers.Torguard: torguard.New(storage, randSource, unzipper, updaterWarner, parallelResolver),
Expand Down
2 changes: 1 addition & 1 deletion internal/provider/purevpn/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) (
connection models.Connection, err error,
) {
defaults := utils.NewConnectionDefaults(80, 53, 0) //nolint:mnd
defaults := utils.NewConnectionDefaults(80, 15021, 0)
return utils.GetConnection(p.Name(),
p.storage, selection, defaults, ipv6Supported, p.randSource)
}
Loading