Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(protonvpn): Added ProtonVPN feature selection #2182

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
150 changes: 124 additions & 26 deletions internal/configuration/settings/serverselection.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,26 @@ 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"`
// IPv6Only is true if VPN servers without IPv6 support should
// be filtered. This is used with ProtonVPN.
IPv6Only *bool `json:"ipv6_only"`
Comment on lines +75 to +77
Copy link
Owner

Choose a reason for hiding this comment

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

What does this filter do? 🤔
Because AFAIK there are only IPv4 addresses to reach Protonvpn servers.

Copy link
Author

Choose a reason for hiding this comment

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

Not sure! It's indeed not a supported feature by any current server, but I just wanted to implement all features listed in protonvpn-nm-lib

// OpenVPN contains settings to select OpenVPN servers
// and the final connection.
OpenVPN OpenVPNSelection `json:"openvpn"`
Expand All @@ -79,6 +91,10 @@ 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")
ErrIPv6OnlyNotSupported = errors.New("IPv6 only filter is not supported")
)

func (ss *ServerSelection) validate(vpnServiceProvider string,
Expand Down Expand Up @@ -107,6 +123,11 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
return fmt.Errorf("for VPN service provider %s: %w", vpnServiceProvider, err)
}

err = validateFeatureFilters(ss, vpnServiceProvider)
if err != nil {
return err
}

if *ss.OwnedOnly &&
vpnServiceProvider != providers.Mullvad {
return fmt.Errorf("%w: for VPN service provider %s",
Expand Down Expand Up @@ -134,30 +155,6 @@ func (ss *ServerSelection) validate(vpnServiceProvider string,
return fmt.Errorf("%w", ErrFreePremiumBothSet)
}

if *ss.StreamOnly &&
!helpers.IsOneOf(vpnServiceProvider,
providers.Protonvpn,
providers.VPNUnlimited,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrStreamOnlyNotSupported, vpnServiceProvider)
}

if *ss.MultiHopOnly &&
vpnServiceProvider != providers.Surfshark {
return fmt.Errorf("%w: for VPN service provider %s",
ErrMultiHopOnlyNotSupported, vpnServiceProvider)
}

if *ss.PortForwardOnly &&
vpnServiceProvider != providers.PrivateInternetAccess {
// ProtonVPN also supports port forwarding, but on all their servers, so these
// 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: for VPN service provider %s",
ErrPortForwardOnlyNotSupported, vpnServiceProvider)
}

if ss.VPN == vpn.OpenVPN {
err = ss.OpenVPN.validate(vpnServiceProvider)
if err != nil {
Expand Down Expand Up @@ -235,6 +232,55 @@ func validateServerFilters(settings ServerSelection, filterChoices models.Filter
return nil
}

// validateFeatureFilters validates filters for features.
func validateFeatureFilters(settings *ServerSelection, vpnServiceProvider string) (err error) {
if *settings.StreamOnly &&
!helpers.IsOneOf(vpnServiceProvider,
providers.Protonvpn,
providers.VPNUnlimited,
) {
return fmt.Errorf("%w: for VPN service provider %s",
ErrStreamOnlyNotSupported, vpnServiceProvider)
}

if *settings.MultiHopOnly &&
vpnServiceProvider != providers.Surfshark {
return fmt.Errorf("%w: for VPN service provider %s",
ErrMultiHopOnlyNotSupported, vpnServiceProvider)
}

if *settings.PortForwardOnly &&
vpnServiceProvider != providers.PrivateInternetAccess {
// ProtonVPN also supports port forwarding, but on all their servers, so these
// 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: for VPN service provider %s",
ErrPortForwardOnlyNotSupported, vpnServiceProvider)
}

if *settings.SecureCoreOnly && vpnServiceProvider != providers.Protonvpn {
return fmt.Errorf("%w: for VPN service provider %s",
ErrSecureCoreOnlyNotSupported, vpnServiceProvider)
}

if *settings.TorOnly && vpnServiceProvider != providers.Protonvpn {
return fmt.Errorf("%w: for VPN service provider %s",
ErrTorOnlyNotSupported, vpnServiceProvider)
}

if *settings.P2POnly && vpnServiceProvider != providers.Protonvpn {
return fmt.Errorf("%w: for VPN service provider %s",
ErrP2POnlyNotSupported, vpnServiceProvider)
}

if *settings.IPv6Only && vpnServiceProvider != providers.Protonvpn {
return fmt.Errorf("%w: for VPN service provider %s",
ErrIPv6OnlyNotSupported, vpnServiceProvider)
}

return nil
}

func (ss *ServerSelection) copy() (copied ServerSelection) {
return ServerSelection{
VPN: ss.VPN,
Expand All @@ -251,6 +297,10 @@ 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),
IPv6Only: gosettings.CopyPointer(ss.IPv6Only),
PortForwardOnly: gosettings.CopyPointer(ss.PortForwardOnly),
MultiHopOnly: gosettings.CopyPointer(ss.MultiHopOnly),
OpenVPN: ss.OpenVPN.copy(),
Expand All @@ -273,6 +323,10 @@ 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.IPv6Only = gosettings.OverrideWithPointer(ss.IPv6Only, other.IPv6Only)
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 +340,10 @@ 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.IPv6Only = gosettings.DefaultPointer(ss.IPv6Only, 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 +412,22 @@ 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.IPv6Only {
node.Appendf("IPv6 only servers: yes")
}

if *ss.MultiHopOnly {
node.Appendf("Multi-hop only servers: yes")
}
Expand Down Expand Up @@ -425,12 +499,36 @@ 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
}

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

// PIA only
ss.PortForwardOnly, err = r.BoolPtr("PORT_FORWARD_ONLY")
if err != nil {
Expand Down
6 changes: 5 additions & 1 deletion internal/models/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ type Server struct {
MultiHop bool `json:"multihop,omitempty"`
WgPubKey string `json:"wgpubkey,omitempty"`
Free bool `json:"free,omitempty"`
Stream bool `json:"stream,omitempty"`
Stream bool `json:"stream,omitempty"` // TODO consider moving to a Features struct in v4
SecureCore bool `json:"secure_core,omitempty"` // TODO consider moving to a Features struct in v4
Tor bool `json:"tor,omitempty"` // TODO consider moving to a Features struct in v4
P2P bool `json:"p2p,omitempty"` // TODO consider moving to a Features struct in v4
IPv6 bool `json:"ipv6,omitempty"` // TODO consider moving to a Features struct in v4
Premium bool `json:"premium,omitempty"`
PortForward bool `json:"port_forward,omitempty"`
Keep bool `json:"keep,omitempty"`
Expand Down
8 changes: 8 additions & 0 deletions internal/models/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ func Test_Server_Equal(t *testing.T) {
WgPubKey: "wgpubkey",
Free: true,
Stream: true,
SecureCore: true,
Tor: true,
P2P: false,
IPv6: false,
PortForward: true,
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
Keep: true,
Expand All @@ -82,6 +86,10 @@ func Test_Server_Equal(t *testing.T) {
WgPubKey: "wgpubkey",
Free: true,
Stream: true,
SecureCore: true,
Tor: true,
P2P: false,
IPv6: 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
15 changes: 14 additions & 1 deletion internal/provider/protonvpn/updater/iptoserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,16 @@ import (

type ipToServer map[string]models.Server

type features struct {
SecureCore bool
Tor bool
P2P bool
Stream bool
IPv6 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 +33,11 @@ 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.IPv6 = features.IPv6
server.UDP = true
server.TCP = true
server.IPs = []netip.Addr{entryIP}
Expand Down
20 changes: 19 additions & 1 deletion internal/provider/protonvpn/updater/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ 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

featuresBits := logicalServer.Features

// FROM https://github.com/ProtonVPN/protonvpn-nm-lib/blob/31d5f99fbc89274e4e977a11e7432c0eab5a3ef8/protonvpn_nm_lib/enums.py#L44-L49
features := &features{}
switch {
case featuresBits&1 != 0:
features.SecureCore = true
case featuresBits&2 != 0:
features.Tor = true
case featuresBits&4 != 0:
features.P2P = true
case featuresBits&8 != 0:
features.Stream = true
case featuresBits&16 != 0:
features.IPv6 = true
}

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 +78,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
16 changes: 16 additions & 0 deletions internal/provider/utils/filtering.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,22 @@ 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 *selection.IPv6Only && !server.IPv6 {
return true
}

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