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= \
|
SERVER_NAMES= \
|
||||||
# # ProtonVPN only:
|
# # ProtonVPN only:
|
||||||
FREE_ONLY= \
|
FREE_ONLY= \
|
||||||
|
SECURE_CORE_ONLY= \
|
||||||
|
TOR_ONLY= \
|
||||||
|
P2P_ONLY= \
|
||||||
# # Surfshark only:
|
# # Surfshark only:
|
||||||
MULTIHOP_ONLY= \
|
MULTIHOP_ONLY= \
|
||||||
# # VPN Secure only:
|
# # VPN Secure only:
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ type ServerSelection struct { //nolint:maligned
|
|||||||
// TODO extend to providers using FreeOnly.
|
// TODO extend to providers using FreeOnly.
|
||||||
PremiumOnly *bool `json:"premium_only"`
|
PremiumOnly *bool `json:"premium_only"`
|
||||||
// StreamOnly is true if VPN servers not for streaming should
|
// 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"`
|
StreamOnly *bool `json:"stream_only"`
|
||||||
// MultiHopOnly is true if VPN servers that are not multihop
|
// MultiHopOnly is true if VPN servers that are not multihop
|
||||||
// should be filtered. This is used with Surfshark.
|
// 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
|
// PortForwardOnly is true if VPN servers that don't support
|
||||||
// port forwarding should be filtered. This is used with PIA.
|
// port forwarding should be filtered. This is used with PIA.
|
||||||
PortForwardOnly *bool `json:"port_forward_only"`
|
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
|
// OpenVPN contains settings to select OpenVPN servers
|
||||||
// and the final connection.
|
// and the final connection.
|
||||||
OpenVPN OpenVPNSelection `json:"openvpn"`
|
OpenVPN OpenVPNSelection `json:"openvpn"`
|
||||||
@@ -79,6 +88,9 @@ var (
|
|||||||
ErrMultiHopOnlyNotSupported = errors.New("multi hop only filter is not supported")
|
ErrMultiHopOnlyNotSupported = errors.New("multi hop only filter is not supported")
|
||||||
ErrPortForwardOnlyNotSupported = errors.New("port forwarding 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")
|
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,
|
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
|
// don't have the port forwarding boolean field. As a consequence, we only allow
|
||||||
// the use of PortForwardOnly for Private Internet Access.
|
// the use of PortForwardOnly for Private Internet Access.
|
||||||
return fmt.Errorf("%w", ErrPortForwardOnlyNotSupported)
|
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:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -251,6 +269,9 @@ func (ss *ServerSelection) copy() (copied ServerSelection) {
|
|||||||
FreeOnly: gosettings.CopyPointer(ss.FreeOnly),
|
FreeOnly: gosettings.CopyPointer(ss.FreeOnly),
|
||||||
PremiumOnly: gosettings.CopyPointer(ss.PremiumOnly),
|
PremiumOnly: gosettings.CopyPointer(ss.PremiumOnly),
|
||||||
StreamOnly: gosettings.CopyPointer(ss.StreamOnly),
|
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),
|
PortForwardOnly: gosettings.CopyPointer(ss.PortForwardOnly),
|
||||||
MultiHopOnly: gosettings.CopyPointer(ss.MultiHopOnly),
|
MultiHopOnly: gosettings.CopyPointer(ss.MultiHopOnly),
|
||||||
OpenVPN: ss.OpenVPN.copy(),
|
OpenVPN: ss.OpenVPN.copy(),
|
||||||
@@ -273,6 +294,9 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) {
|
|||||||
ss.FreeOnly = gosettings.OverrideWithPointer(ss.FreeOnly, other.FreeOnly)
|
ss.FreeOnly = gosettings.OverrideWithPointer(ss.FreeOnly, other.FreeOnly)
|
||||||
ss.PremiumOnly = gosettings.OverrideWithPointer(ss.PremiumOnly, other.PremiumOnly)
|
ss.PremiumOnly = gosettings.OverrideWithPointer(ss.PremiumOnly, other.PremiumOnly)
|
||||||
ss.StreamOnly = gosettings.OverrideWithPointer(ss.StreamOnly, other.StreamOnly)
|
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.MultiHopOnly = gosettings.OverrideWithPointer(ss.MultiHopOnly, other.MultiHopOnly)
|
||||||
ss.PortForwardOnly = gosettings.OverrideWithPointer(ss.PortForwardOnly, other.PortForwardOnly)
|
ss.PortForwardOnly = gosettings.OverrideWithPointer(ss.PortForwardOnly, other.PortForwardOnly)
|
||||||
ss.OpenVPN.overrideWith(other.OpenVPN)
|
ss.OpenVPN.overrideWith(other.OpenVPN)
|
||||||
@@ -286,6 +310,9 @@ func (ss *ServerSelection) setDefaults(vpnProvider string) {
|
|||||||
ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false)
|
ss.FreeOnly = gosettings.DefaultPointer(ss.FreeOnly, false)
|
||||||
ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false)
|
ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false)
|
||||||
ss.StreamOnly = gosettings.DefaultPointer(ss.StreamOnly, 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.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false)
|
||||||
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, false)
|
ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, false)
|
||||||
ss.OpenVPN.setDefaults(vpnProvider)
|
ss.OpenVPN.setDefaults(vpnProvider)
|
||||||
@@ -354,6 +381,18 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) {
|
|||||||
node.Appendf("Stream only servers: yes")
|
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 {
|
if *ss.MultiHopOnly {
|
||||||
node.Appendf("Multi-hop only servers: yes")
|
node.Appendf("Multi-hop only servers: yes")
|
||||||
}
|
}
|
||||||
@@ -425,12 +464,30 @@ func (ss *ServerSelection) read(r *reader.Reader,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// VPNUnlimited only
|
// VPNUnlimited and ProtonVPN only
|
||||||
ss.StreamOnly, err = r.BoolPtr("STREAM_ONLY")
|
ss.StreamOnly, err = r.BoolPtr("STREAM_ONLY")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
// PIA only
|
||||||
ss.PortForwardOnly, err = r.BoolPtr("PORT_FORWARD_ONLY")
|
ss.PortForwardOnly, err = r.BoolPtr("PORT_FORWARD_ONLY")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -28,9 +28,12 @@ type Server struct {
|
|||||||
RetroLoc string `json:"retroloc,omitempty"` // TODO remove in v4
|
RetroLoc string `json:"retroloc,omitempty"` // TODO remove in v4
|
||||||
MultiHop bool `json:"multihop,omitempty"`
|
MultiHop bool `json:"multihop,omitempty"`
|
||||||
WgPubKey string `json:"wgpubkey,omitempty"`
|
WgPubKey string `json:"wgpubkey,omitempty"`
|
||||||
Free bool `json:"free,omitempty"`
|
Free bool `json:"free,omitempty"` // TODO v4 create a SubscriptionTier struct
|
||||||
Stream bool `json:"stream,omitempty"`
|
|
||||||
Premium bool `json:"premium,omitempty"`
|
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"`
|
PortForward bool `json:"port_forward,omitempty"`
|
||||||
Keep bool `json:"keep,omitempty"`
|
Keep bool `json:"keep,omitempty"`
|
||||||
IPs []netip.Addr `json:"ips,omitempty"`
|
IPs []netip.Addr `json:"ips,omitempty"`
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ func Test_Server_Equal(t *testing.T) {
|
|||||||
WgPubKey: "wgpubkey",
|
WgPubKey: "wgpubkey",
|
||||||
Free: true,
|
Free: true,
|
||||||
Stream: true,
|
Stream: true,
|
||||||
|
SecureCore: true,
|
||||||
|
Tor: true,
|
||||||
|
P2P: false,
|
||||||
PortForward: true,
|
PortForward: true,
|
||||||
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
|
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
|
||||||
Keep: true,
|
Keep: true,
|
||||||
@@ -82,6 +85,9 @@ func Test_Server_Equal(t *testing.T) {
|
|||||||
WgPubKey: "wgpubkey",
|
WgPubKey: "wgpubkey",
|
||||||
Free: true,
|
Free: true,
|
||||||
Stream: true,
|
Stream: true,
|
||||||
|
SecureCore: true,
|
||||||
|
Tor: true,
|
||||||
|
P2P: false,
|
||||||
PortForward: true,
|
PortForward: true,
|
||||||
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
|
IPs: []netip.Addr{netip.AddrFrom4([4]byte{1, 2, 3, 4})},
|
||||||
Keep: true,
|
Keep: true,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type logicalServer struct {
|
|||||||
Region *string `json:"Region"`
|
Region *string `json:"Region"`
|
||||||
City *string `json:"City"`
|
City *string `json:"City"`
|
||||||
Servers []physicalServer `json:"Servers"`
|
Servers []physicalServer `json:"Servers"`
|
||||||
|
Features uint16 `json:"Features"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type physicalServer struct {
|
type physicalServer struct {
|
||||||
|
|||||||
@@ -9,8 +9,15 @@ import (
|
|||||||
|
|
||||||
type ipToServer map[string]models.Server
|
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,
|
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()
|
key := entryIP.String()
|
||||||
|
|
||||||
server, ok := its[key]
|
server, ok := its[key]
|
||||||
@@ -25,6 +32,10 @@ func (its ipToServer) add(country, region, city, name, hostname string,
|
|||||||
server.ServerName = name
|
server.ServerName = name
|
||||||
server.Hostname = hostname
|
server.Hostname = hostname
|
||||||
server.Free = free
|
server.Free = free
|
||||||
|
server.SecureCore = features.secureCore
|
||||||
|
server.Tor = features.tor
|
||||||
|
server.P2P = features.p2p
|
||||||
|
server.Stream = features.stream
|
||||||
server.UDP = true
|
server.UDP = true
|
||||||
server.TCP = true
|
server.TCP = true
|
||||||
server.IPs = []netip.Addr{entryIP}
|
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
|
// TODO v4 remove `name` field because of
|
||||||
// https://github.com/qdm12/gluetun/issues/1018#issuecomment-1151750179
|
// https://github.com/qdm12/gluetun/issues/1018#issuecomment-1151750179
|
||||||
name := logicalServer.Name
|
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 {
|
for _, physicalServer := range logicalServer.Servers {
|
||||||
if physicalServer.Status == 0 { // disabled so skip server
|
if physicalServer.Status == 0 { // disabled so skip server
|
||||||
u.warner.Warn("ignoring server " + physicalServer.Domain + " with status 0")
|
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)
|
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
|
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) {
|
if filterByPossibilities(server.Country, selection.Countries) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,45 @@ func Test_FilterServers(t *testing.T) {
|
|||||||
{Stream: true, VPN: vpn.OpenVPN, UDP: true},
|
{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": {
|
"filter by owned": {
|
||||||
selection: settings.ServerSelection{
|
selection: settings.ServerSelection{
|
||||||
OwnedOnly: boolPtr(true),
|
OwnedOnly: boolPtr(true),
|
||||||
|
|||||||
@@ -79,6 +79,18 @@ func filterServer(server models.Server,
|
|||||||
return true
|
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) {
|
if filterByPossibilities(server.Country, selection.Countries) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user