Skip to content
Draft
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 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
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
Comment thread
qdm12 marked this conversation as resolved.
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
Comment thread
qdm12 marked this conversation as resolved.
.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": {
Comment thread
qdm12 marked this conversation as resolved.
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)

@qdm12 qdm12 Apr 23, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing as

Suggested change
if testCase.err == nil {
require.NoError(t, err)
return
}
require.Error(t, err)
assert.ErrorIs(t, err, testCase.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
Comment on lines +95 to +97

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not? It should still be applied somehow, otherwise it breaks compatibility.

}

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",
Comment thread
qdm12 marked this conversation as resolved.
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)
Comment thread
qdm12 marked this conversation as resolved.
return utils.GetConnection(p.Name(),
p.storage, selection, defaults, ipv6Supported, p.randSource)
}
Loading