Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
purevpn: add template ingestion and p2p server type support
  • Loading branch information
Pat committed Feb 27, 2026
commit f81d06188fca036b42d35e10df200c05999bba9f
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
6 changes: 4 additions & 2 deletions internal/configuration/settings/serverselection.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ type ServerSelection struct {
// and ProtonVPN.
PortForwardOnly *bool `json:"port_forward_only"`
// PureVPNServerType selects PureVPN servers by hostname-inferred traits.
// Allowed values are: regular, portforwarding, quantumresistant, obfuscation.
// Allowed values are: regular, portforwarding, quantumresistant, obfuscation, p2p.
PureVPNServerType string `json:"purevpn_server_type"`
// PureVPNCountryCodes filters PureVPN servers by deterministic
// 2-letter country code parsed from the hostname prefix.
Expand Down Expand Up @@ -299,7 +299,7 @@ func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string)
return fmt.Errorf("%w", ErrPureVPNServerTypeNotSupported)
case settings.PureVPNServerType != "" &&
!helpers.IsOneOf(settings.PureVPNServerType,
"regular", "portforwarding", "quantumresistant", "obfuscation"):
"regular", "portforwarding", "quantumresistant", "obfuscation", "p2p"):
return fmt.Errorf("%w: %q", ErrPureVPNServerTypeNotValid, settings.PureVPNServerType)
case len(settings.PureVPNCountryCodes) > 0 && vpnServiceProvider != providers.Purevpn:
return fmt.Errorf("%w", ErrPureVPNCountryCodesNotSupported)
Expand Down Expand Up @@ -611,6 +611,8 @@ func parsePureVPNServerType(raw string) string {
return "quantumresistant"
case "obfuscation", "obfuscated", "obf":
return "obfuscation"
case "p2p":
return "p2p"
default:
return value
}
Expand Down
14 changes: 14 additions & 0 deletions internal/configuration/settings/serverselection_purevpn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func Test_parsePureVPNServerType(t *testing.T) {
"quantum resistant alias": {raw: "quantum-resistant", expected: "quantumresistant"},
"obf alias": {raw: "obf", expected: "obfuscation"},
"obfuscated alias": {raw: "obfuscated", expected: "obfuscation"},
"p2p": {raw: "p2p", expected: "p2p"},
"unknown": {raw: "fast", expected: "fast"},
}

Expand All @@ -45,6 +46,10 @@ func Test_validateFeatureFilters_PureVPNServerType(t *testing.T) {
provider: providers.Purevpn,
serverType: "obfuscation",
},
"valid p2p with purevpn": {
provider: providers.Purevpn,
serverType: "p2p",
},
"invalid provider": {
provider: providers.Mullvad,
serverType: "regular",
Expand Down Expand Up @@ -138,3 +143,12 @@ func Test_validateFeatureFilters_PureVPNLocationCodeFilters(t *testing.T) {
})
}
}

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

for _, serverType := range []string{"regular", "obfuscation", "p2p"} {
selection := ServerSelection{PureVPNServerType: serverType}.WithDefaults(providers.Purevpn)
assert.Equal(t, "udp", selection.OpenVPN.Protocol)
}
}
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, 15021, 0) //nolint:mnd
defaults := utils.NewConnectionDefaults(80, 15021, 0)
return utils.GetConnection(p.Name(),
p.storage, selection, defaults, ipv6Supported, p.randSource)
}
9 changes: 9 additions & 0 deletions internal/provider/purevpn/openvpnconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ func (p *Provider) OpenVPNConfig(connection models.Connection,
openvpn.AES256gcm,
},
KeyDirection: "1",
UDPLines: []string{
"explicit-exit-notify 2",
},
ExtraLines: []string{
"compress",
"route-method exe",
"route-delay 0",
"script-security 2",
},
CAs: []string{
"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", //nolint:lll
"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", //nolint:lll
Expand Down
53 changes: 53 additions & 0 deletions internal/provider/purevpn/openvpnconf_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package purevpn

import (
"net/netip"
"strings"
"testing"

"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gluetun/internal/models"
"github.com/stretchr/testify/assert"
)

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

p := Provider{}
connection := models.Connection{
IP: netip.MustParseAddr("1.2.3.4"),
Port: 15021,
Protocol: constants.UDP,
Hostname: "us2-udp.ptoserver.com",
}
openvpnSettings := settings.OpenVPN{}.WithDefaults(providers.Purevpn)

lines := p.OpenVPNConfig(connection, openvpnSettings, false)

assert.True(t, hasLineContaining(lines, "remote-cert-tls server"))
assert.True(t, hasLineContaining(lines, "key-direction 1"))
assert.True(t, hasLineContaining(lines, "compress"))
assert.True(t, hasLineContaining(lines, "route-method exe"))
assert.True(t, hasLineContaining(lines, "route-delay 0"))
assert.True(t, hasLineContaining(lines, "script-security 2"))
assert.True(t, hasLineContaining(lines, "explicit-exit-notify 2"))
assert.True(t, hasLineContaining(lines, "<ca>"))
assert.True(t, hasLineContaining(lines, "</ca>"))
assert.True(t, hasLineContaining(lines, "<cert>"))
assert.True(t, hasLineContaining(lines, "</cert>"))
assert.True(t, hasLineContaining(lines, "<key>"))
assert.True(t, hasLineContaining(lines, "</key>"))
assert.True(t, hasLineContaining(lines, "<tls-auth>"))
assert.True(t, hasLineContaining(lines, "</tls-auth>"))
}

func hasLineContaining(lines []string, needle string) bool {
for _, line := range lines {
if strings.Contains(line, needle) {
return true
}
}
return false
}
82 changes: 82 additions & 0 deletions internal/provider/purevpn/updater/export_enriched_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package updater
import (
"context"
"encoding/csv"
"encoding/json"
"fmt"
"net/http"
"os"
Expand Down Expand Up @@ -106,4 +107,85 @@ func Test_exportEnrichedServersCSV(t *testing.T) {
}

t.Logf("wrote %d enriched rows to %s", len(servers), outPath)

username := strings.TrimSpace(firstNonEmpty(
os.Getenv("PUREVPN_USER"),
os.Getenv("PUREVPN_USERNAME"),
os.Getenv("OPENVPN_USER")))
password := strings.TrimSpace(firstNonEmpty(
os.Getenv("PUREVPN_PASSWORD"),
os.Getenv("OPENVPN_PASSWORD")))
if username == "" || password == "" {
t.Fatalf("PUREVPN credentials are required to export OpenVPN templates")
}

debURL, err := fetchDebURL(ctx, http.DefaultClient)
if err != nil {
t.Fatalf("fetching deb URL for templates: %v", err)
}
debContent, err := fetchURL(ctx, http.DefaultClient, debURL)
if err != nil {
t.Fatalf("fetching deb content for templates: %v", err)
}
asarContent, err := extractAsarFromDeb(debContent)
if err != nil {
t.Fatalf("extracting app.asar for templates: %v", err)
}

endpointsContent, endpointsPath, err := extractFirstFileFromAsar(asarContent,
inventoryEndpointsAsarPath,
"node_modules/atom-sdk/node_modules/inventory/node_modules/utils/lib/constants/end-points.js")
if err != nil {
t.Fatalf("extracting inventory endpoints file from app.asar: %v", err)
}
inventoryURLTemplate, err := parseInventoryURLTemplate(endpointsContent)
if err != nil {
t.Fatalf("parsing inventory URL template from %q: %v", endpointsPath, err)
}

offlineInventoryContent, offlineInventoryPath, err := extractFirstFileFromAsar(asarContent,
inventoryOfflineAsarPath,
"node_modules/atom-sdk/node_modules/inventory/src/offline-data/inventory-data.js")
if err != nil {
t.Fatalf("extracting inventory offline data from app.asar: %v", err)
}
resellerUID, err := parseResellerUIDFromInventoryOffline(offlineInventoryContent)
if err != nil {
t.Fatalf("parsing reseller UID from %q: %v", offlineInventoryPath, err)
}
inventoryURL, err := buildInventoryURL(inventoryURLTemplate, resellerUID)
if err != nil {
t.Fatalf("building inventory URL: %v", err)
}
inventoryContent, err := fetchURL(ctx, http.DefaultClient, inventoryURL)
if err != nil {
t.Fatalf("fetching inventory JSON %q: %v", inventoryURL, err)
}

templates, err := fetchOpenVPNTemplates(ctx, http.DefaultClient, asarContent, inventoryContent, username, password)
if err != nil {
t.Fatalf("fetching OpenVPN templates: %v", err)
}
if len(templates) == 0 {
t.Fatalf("no OpenVPN templates found")
}

const templatesOutPath = "artifacts/purevpn_openvpn_templates.json"
templatesData, err := json.MarshalIndent(templates, "", " ")
if err != nil {
t.Fatalf("marshalling templates artifact: %v", err)
}
if err := os.WriteFile(templatesOutPath, append(templatesData, '\n'), 0o600); err != nil {
t.Fatalf("writing templates artifact: %v", err)
}
t.Logf("wrote %d OpenVPN templates to %s", len(templates), templatesOutPath)
}

func firstNonEmpty(values ...string) string {
for _, value := range values {
if value != "" {
return value
}
}
return ""
}
7 changes: 5 additions & 2 deletions internal/provider/purevpn/updater/hosttoserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ import (

type hostToServer map[string]models.Server

func (hts hostToServer) add(host string, tcp, udp bool, port uint16) {
func (hts hostToServer) add(host string, tcp, udp bool, port uint16, p2pTagged bool) {
server, ok := hts[host]
if !ok {
server.VPN = vpn.OpenVPN
server.Hostname = host
}
portForward, quantumResistant, obfuscated := inferPureVPNTraits(host)
portForward, quantumResistant, obfuscated, p2pInHost := inferPureVPNTraits(host)
server.PortForward = server.PortForward || portForward
server.QuantumResistant = server.QuantumResistant || quantumResistant
server.Obfuscated = server.Obfuscated || obfuscated
if p2pTagged || p2pInHost {
server.Categories = appendStringIfMissing(server.Categories, "p2p")
}
if tcp {
server.TCP = true
if port != 0 {
Expand Down
42 changes: 42 additions & 0 deletions internal/provider/purevpn/updater/hosttoserver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package updater

import (
"testing"

"github.com/stretchr/testify/assert"
)

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

hts := make(hostToServer)
hts.add("us2-obf-udp.ptoserver.com", false, true, 1210, false)

server := hts["us2-obf-udp.ptoserver.com"]
assert.True(t, server.Obfuscated)
assert.True(t, server.UDP)
assert.False(t, server.TCP)
assert.Nil(t, server.TCPPorts)
assert.Equal(t, []uint16{1210}, server.UDPPorts)
}

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

hts := make(hostToServer)
hts.add("us2-obf-udp.ptoserver.com", true, false, 80, false)

server := hts["us2-obf-udp.ptoserver.com"]
assert.True(t, server.TCP)
assert.Equal(t, []uint16{80}, server.TCPPorts)
}

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

hts := make(hostToServer)
hts.add("us2-udp.ptoserver.com", false, true, 15021, true)

server := hts["us2-udp.ptoserver.com"]
assert.Equal(t, []string{"p2p"}, server.Categories)
}
44 changes: 41 additions & 3 deletions internal/provider/purevpn/updater/inventory.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ type inventoryBody struct {
type inventoryCountry struct {
DataCenters []inventoryDataCenterRef `json:"data_centers"`
Protocols []inventoryProtocol `json:"protocols"`
Features []string `json:"features"`
}

type inventoryDataCenterRef struct {
Expand All @@ -101,8 +102,10 @@ type inventoryProtocolDNS struct {
}

type inventoryDNS struct {
ID int `json:"id"`
Hostname string `json:"hostname"`
ID int `json:"id"`
Hostname string `json:"hostname"`
ConfigurationVersion string `json:"configuration_version"`
Tags []string `json:"tags"`
}

type inventoryDataCenter struct {
Expand All @@ -121,11 +124,13 @@ func parseInventoryJSON(content []byte) (hts hostToServer, hostToFallbackIPs map
}

dnsIDToHostname := make(map[int]string, len(response.Body.DNS))
dnsIDToP2PTagged := make(map[int]bool, len(response.Body.DNS))
for _, dnsEntry := range response.Body.DNS {
if dnsEntry.ID == 0 || dnsEntry.Hostname == "" {
continue
}
dnsIDToHostname[dnsEntry.ID] = strings.TrimSpace(dnsEntry.Hostname)
dnsIDToP2PTagged[dnsEntry.ID] = hasP2PTag(dnsEntry.Tags)
}

dataCenterIDToIP := make(map[int]netip.Addr, len(response.Body.DataCenters))
Expand All @@ -145,6 +150,7 @@ func parseInventoryJSON(content []byte) (hts hostToServer, hostToFallbackIPs map
blocksFound := 0

for _, country := range response.Body.Countries {
countryP2PTagged := hasP2PTag(country.Features)
countryDataCenterIPs := make([]netip.Addr, 0, len(country.DataCenters))
for _, dataCenterRef := range country.DataCenters {
ip, ok := dataCenterIDToIP[dataCenterRef.ID]
Expand Down Expand Up @@ -173,7 +179,8 @@ func parseInventoryJSON(content []byte) (hts hostToServer, hostToFallbackIPs map
if dns.PortNumber > 0 && dns.PortNumber <= 65535 {
port = uint16(dns.PortNumber)
}
hts.add(hostname, tcp, udp, port)
p2pTagged := countryP2PTagged || dnsIDToP2PTagged[dns.DNSID]
hts.add(hostname, tcp, udp, port, p2pTagged)

for _, ip := range countryDataCenterIPs {
hostToFallbackIPs[hostname] = appendIPIfMissing(hostToFallbackIPs[hostname], ip)
Expand All @@ -192,6 +199,37 @@ func parseInventoryJSON(content []byte) (hts hostToServer, hostToFallbackIPs map
return hts, hostToFallbackIPs, nil
}

func parseInventoryConfigurationVersions(content []byte) (versions []string, err error) {
var response inventoryResponse
if err := json.Unmarshal(content, &response); err != nil {
return nil, fmt.Errorf("unmarshalling inventory JSON: %w", err)
}

set := make(map[string]struct{})
for _, dnsEntry := range response.Body.DNS {
version := strings.TrimSpace(dnsEntry.ConfigurationVersion)
if version == "" {
continue
}
if _, exists := set[version]; exists {
continue
}
set[version] = struct{}{}
versions = append(versions, version)
}

return versions, nil
}

func hasP2PTag(tags []string) (p2p bool) {
for _, tag := range tags {
if strings.EqualFold(strings.TrimSpace(tag), "p2p") {
return true
}
}
return false
}

func extractFirstFileFromAsar(asarContent []byte, paths ...string) (content []byte, usedPath string, err error) {
if len(paths) == 0 {
return nil, "", fmt.Errorf("no asar paths provided")
Expand Down
Loading