Skip to content

Commit

Permalink
feat(protonvpn): feature filters (#2182)
Browse files Browse the repository at this point in the history
- `SECURE_CORE_ONLY`
- `TOR_ONLY`
- `P2P_ONLY`
  • Loading branch information
kvanzuijlen authored Jul 29, 2024
1 parent 2bf2525 commit cb99f90
Show file tree
Hide file tree
Showing 11 changed files with 6,359 additions and 2,285 deletions.
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ ENV VPN_SERVICE_PROVIDER=pia \
SERVER_NAMES= \
# # ProtonVPN only:
FREE_ONLY= \
SECURE_CORE_ONLY= \
TOR_ONLY= \
P2P_ONLY= \
# # Surfshark only:
MULTIHOP_ONLY= \
# # VPN Secure only:
Expand Down
61 changes: 59 additions & 2 deletions internal/configuration/settings/serverselection.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,23 @@ type ServerSelection struct { //nolint:maligned
// TODO extend to providers using FreeOnly.
PremiumOnly *bool `json:"premium_only"`
// StreamOnly is true if VPN servers not for streaming should
// be filtered. This is used with VPNUnlimited.
// be filtered. This is used with ProtonVPN and VPNUnlimited.
StreamOnly *bool `json:"stream_only"`
// MultiHopOnly is true if VPN servers that are not multihop
// should be filtered. This is used with Surfshark.
MultiHopOnly *bool `json:"multi_hop_only"`
// PortForwardOnly is true if VPN servers that don't support
// port forwarding should be filtered. This is used with PIA.
PortForwardOnly *bool `json:"port_forward_only"`
// SecureCoreOnly is true if VPN servers without secure core should
// be filtered. This is used with ProtonVPN.
SecureCoreOnly *bool `json:"secure_core_only"`
// TorOnly is true if VPN servers without tor should
// be filtered. This is used with ProtonVPN.
TorOnly *bool `json:"tor_only"`
// P2POnly is true if VPN servers not for p2p should
// be filtered. This is used with ProtonVPN.
P2POnly *bool `json:"p2p_only"`
// OpenVPN contains settings to select OpenVPN servers
// and the final connection.
OpenVPN OpenVPNSelection `json:"openvpn"`
Expand All @@ -79,6 +88,9 @@ var (
ErrMultiHopOnlyNotSupported = errors.New("multi hop only filter is not supported")
ErrPortForwardOnlyNotSupported = errors.New("port forwarding only filter is not supported")
ErrFreePremiumBothSet = errors.New("free only and premium only filters are both set")
ErrSecureCoreOnlyNotSupported = errors.New("secure core only filter is not supported")
ErrTorOnlyNotSupported = errors.New("tor only filter is not supported")
ErrP2POnlyNotSupported = errors.New("p2p only filter is not supported")
)

func (ss *ServerSelection) validate(vpnServiceProvider string,
Expand Down Expand Up @@ -230,6 +242,12 @@ func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string)
// don't have the port forwarding boolean field. As a consequence, we only allow
// the use of PortForwardOnly for Private Internet Access.
return fmt.Errorf("%w", ErrPortForwardOnlyNotSupported)
case *settings.SecureCoreOnly && vpnServiceProvider != providers.Protonvpn:
return fmt.Errorf("%w", ErrSecureCoreOnlyNotSupported)
case *settings.TorOnly && vpnServiceProvider != providers.Protonvpn:
return fmt.Errorf("%w", ErrTorOnlyNotSupported)
case *settings.P2POnly && vpnServiceProvider != providers.Protonvpn:
return fmt.Errorf("%w", ErrP2POnlyNotSupported)
default:
return nil
}
Expand All @@ -251,6 +269,9 @@ func (ss *ServerSelection) copy() (copied ServerSelection) {
FreeOnly: gosettings.CopyPointer(ss.FreeOnly),
PremiumOnly: gosettings.CopyPointer(ss.PremiumOnly),
StreamOnly: gosettings.CopyPointer(ss.StreamOnly),
SecureCoreOnly: gosettings.CopyPointer(ss.SecureCoreOnly),
TorOnly: gosettings.CopyPointer(ss.TorOnly),
P2POnly: gosettings.CopyPointer(ss.P2POnly),
PortForwardOnly: gosettings.CopyPointer(ss.PortForwardOnly),
MultiHopOnly: gosettings.CopyPointer(ss.MultiHopOnly),
OpenVPN: ss.OpenVPN.copy(),
Expand All @@ -273,6 +294,9 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) {
ss.FreeOnly = gosettings.OverrideWithPointer(ss.FreeOnly, other.FreeOnly)
ss.PremiumOnly = gosettings.OverrideWithPointer(ss.PremiumOnly, other.PremiumOnly)
ss.StreamOnly = gosettings.OverrideWithPointer(ss.StreamOnly, other.StreamOnly)
ss.SecureCoreOnly = gosettings.OverrideWithPointer(ss.SecureCoreOnly, other.SecureCoreOnly)
ss.TorOnly = gosettings.OverrideWithPointer(ss.TorOnly, other.TorOnly)
ss.P2POnly = gosettings.OverrideWithPointer(ss.P2POnly, other.P2POnly)
ss.MultiHopOnly = gosettings.OverrideWithPointer(ss.MultiHopOnly, other.MultiHopOnly)
ss.PortForwardOnly = gosettings.OverrideWithPointer(ss.PortForwardOnly, other.PortForwardOnly)
ss.OpenVPN.overrideWith(other.OpenVPN)
Expand All @@ -286,6 +310,9 @@ func (ss *ServerSelection) setDefaults(vpnProvider string) {
ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false)
ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false)
ss.StreamOnly = gosettings.DefaultPointer(ss.StreamOnly, false)
ss.SecureCoreOnly = gosettings.DefaultPointer(ss.SecureCoreOnly, false)
ss.TorOnly = gosettings.DefaultPointer(ss.TorOnly, false)
ss.P2POnly = gosettings.DefaultPointer(ss.P2POnly, false)
ss.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false)
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, false)
ss.OpenVPN.setDefaults(vpnProvider)
Expand Down Expand Up @@ -354,6 +381,18 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
node.Appendf("Stream only servers: yes")
}

if *ss.SecureCoreOnly {
node.Appendf("Secure Core only servers: yes")
}

if *ss.TorOnly {
node.Appendf("Tor only servers: yes")
}

if *ss.P2POnly {
node.Appendf("P2P only servers: yes")
}

if *ss.MultiHopOnly {
node.Appendf("Multi-hop only servers: yes")
}
Expand Down Expand Up @@ -425,12 +464,30 @@ func (ss *ServerSelection) read(r *reader.Reader,
return err
}

// VPNUnlimited only
// VPNUnlimited and ProtonVPN only
ss.StreamOnly, err = r.BoolPtr("STREAM_ONLY")
if err != nil {
return err
}

// ProtonVPN only
ss.SecureCoreOnly, err = r.BoolPtr("SECURE_CORE_ONLY")
if err != nil {
return err
}

// ProtonVPN only
ss.TorOnly, err = r.BoolPtr("TOR_ONLY")
if err != nil {
return err
}

// ProtonVPN only
ss.P2POnly, err = r.BoolPtr("P2P_ONLY")
if err != nil {
return err
}

// PIA only
ss.PortForwardOnly, err = r.BoolPtr("PORT_FORWARD_ONLY")
if err != nil {
Expand Down
7 changes: 5 additions & 2 deletions internal/models/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ type Server struct {
RetroLoc string `json:"retroloc,omitempty"` // TODO remove in v4
MultiHop bool `json:"multihop,omitempty"`
WgPubKey string `json:"wgpubkey,omitempty"`
Free bool `json:"free,omitempty"`
Stream bool `json:"stream,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"`
P2P bool `json:"p2p,omitempty"`
PortForward bool `json:"port_forward,omitempty"`
Keep bool `json:"keep,omitempty"`
IPs []netip.Addr `json:"ips,omitempty"`
Expand Down
6 changes: 6 additions & 0 deletions internal/models/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ func Test_Server_Equal(t *testing.T) {
WgPubKey: "wgpubkey",
Free: true,
Stream: true,
SecureCore: true,
Tor: true,
P2P: false,
PortForward: true,
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
Keep: true,
Expand All @@ -82,6 +85,9 @@ func Test_Server_Equal(t *testing.T) {
WgPubKey: "wgpubkey",
Free: true,
Stream: true,
SecureCore: true,
Tor: true,
P2P: false,
PortForward: true,
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
Keep: true,
Expand Down
1 change: 1 addition & 0 deletions internal/provider/protonvpn/updater/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type logicalServer struct {
Region *string `json:"Region"`
City *string `json:"City"`
Servers []physicalServer `json:"Servers"`
Features uint16 `json:"Features"`
}

type physicalServer struct {
Expand Down
13 changes: 12 additions & 1 deletion internal/provider/protonvpn/updater/iptoserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ import (

type ipToServer map[string]models.Server

type features struct {
secureCore bool
tor bool
p2p bool
stream bool
}

func (its ipToServer) add(country, region, city, name, hostname string,
free bool, entryIP netip.Addr) {
free bool, entryIP netip.Addr, features features) {
key := entryIP.String()

server, ok := its[key]
Expand All @@ -25,6 +32,10 @@ func (its ipToServer) add(country, region, city, name, hostname string,
server.ServerName = name
server.Hostname = hostname
server.Free = free
server.SecureCore = features.secureCore
server.Tor = features.tor
server.P2P = features.p2p
server.Stream = features.stream
server.UDP = true
server.TCP = true
server.IPs = []netip.Addr{entryIP}
Expand Down
14 changes: 13 additions & 1 deletion internal/provider/protonvpn/updater/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) (
// TODO v4 remove `name` field because of
// https://github.com/qdm12/gluetun/issues/1018#issuecomment-1151750179
name := logicalServer.Name

//nolint:lll
// See https://github.com/ProtonVPN/protonvpn-nm-lib/blob/31d5f99fbc89274e4e977a11e7432c0eab5a3ef8/protonvpn_nm_lib/enums.py#L44-L49
featuresBits := logicalServer.Features
features := features{
secureCore: featuresBits&1 != 0,
tor: featuresBits&2 != 0,
p2p: featuresBits&4 != 0,
stream: featuresBits&8 != 0,
// ipv6: featuresBits&16 != 0, - unused.
}

for _, physicalServer := range logicalServer.Servers {
if physicalServer.Status == 0 { // disabled so skip server
u.warner.Warn("ignoring server " + physicalServer.Domain + " with status 0")
Expand All @@ -60,7 +72,7 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) (
u.warner.Warn(warning)
}

ipToServer.add(country, region, city, name, hostname, free, entryIP)
ipToServer.add(country, region, city, name, hostname, free, entryIP, features)
}
}

Expand Down
12 changes: 12 additions & 0 deletions internal/provider/utils/filtering.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ func filterServer(server models.Server,
return true
}

if *selection.SecureCoreOnly && !server.SecureCore {
return true
}

if *selection.TorOnly && !server.Tor {
return true
}

if *selection.P2POnly && !server.P2P {
return true
}

if filterByPossibilities(server.Country, selection.Countries) {
return true
}
Expand Down
39 changes: 39 additions & 0 deletions internal/provider/utils/filtering_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,45 @@ func Test_FilterServers(t *testing.T) {
{Stream: true, VPN: vpn.OpenVPN, UDP: true},
},
},
"filter by secure core only": {
selection: settings.ServerSelection{
SecureCoreOnly: boolPtr(true),
}.WithDefaults(providers.Protonvpn),
servers: []models.Server{
{SecureCore: false, VPN: vpn.OpenVPN, UDP: true},
{SecureCore: true, VPN: vpn.OpenVPN, UDP: true},
{SecureCore: false, VPN: vpn.OpenVPN, UDP: true},
},
filtered: []models.Server{
{SecureCore: true, VPN: vpn.OpenVPN, UDP: true},
},
},
"filter by tor only": {
selection: settings.ServerSelection{
TorOnly: boolPtr(true),
}.WithDefaults(providers.Protonvpn),
servers: []models.Server{
{Tor: false, VPN: vpn.OpenVPN, UDP: true},
{Tor: true, VPN: vpn.OpenVPN, UDP: true},
{Tor: false, VPN: vpn.OpenVPN, UDP: true},
},
filtered: []models.Server{
{Tor: true, VPN: vpn.OpenVPN, UDP: true},
},
},
"filter by P2P only": {
selection: settings.ServerSelection{
P2POnly: boolPtr(true),
}.WithDefaults(providers.Protonvpn),
servers: []models.Server{
{P2P: false, VPN: vpn.OpenVPN, UDP: true},
{P2P: true, VPN: vpn.OpenVPN, UDP: true},
{P2P: false, VPN: vpn.OpenVPN, UDP: true},
},
filtered: []models.Server{
{P2P: true, VPN: vpn.OpenVPN, UDP: true},
},
},
"filter by owned": {
selection: settings.ServerSelection{
OwnedOnly: boolPtr(true),
Expand Down
12 changes: 12 additions & 0 deletions internal/storage/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ func filterServer(server models.Server,
return true
}

if *selection.SecureCoreOnly && !server.SecureCore {
return true
}

if *selection.TorOnly && !server.Tor {
return true
}

if *selection.P2POnly && !server.P2P {
return true
}

if filterByPossibilities(server.Country, selection.Countries) {
return true
}
Expand Down
Loading

0 comments on commit cb99f90

Please sign in to comment.