feat(protonvpn): feature filters (#2182)
- `SECURE_CORE_ONLY` - `TOR_ONLY` - `P2P_ONLY`
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -55,7 +55,7 @@ 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.
|
||||
@@ -63,6 +63,15 @@ type ServerSelection struct { //nolint:maligned
|
||||
// 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"`
|
||||
@@ -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,
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(),
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
@@ -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}
|
||||
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user