diff --git a/Dockerfile b/Dockerfile index 903fe66c6..8ae29b8a0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -134,6 +134,8 @@ ENV VPN_SERVICE_PROVIDER=pia \ MULTIHOP_ONLY= \ # # VPN Secure only: PREMIUM_ONLY= \ + # # PIA only: + PORT_FORWARD_ONLY= \ # Firewall FIREWALL=on \ FIREWALL_VPN_INPUT_PORTS= \ diff --git a/internal/configuration/settings/serverselection.go b/internal/configuration/settings/serverselection.go index 0c03966f1..fb45d678a 100644 --- a/internal/configuration/settings/serverselection.go +++ b/internal/configuration/settings/serverselection.go @@ -57,7 +57,9 @@ type ServerSelection struct { //nolint:maligned // 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"` // OpenVPN contains settings to select OpenVPN servers // and the final connection. OpenVPN OpenVPNSelection `json:"openvpn"` @@ -67,12 +69,13 @@ type ServerSelection struct { //nolint:maligned } var ( - ErrOwnedOnlyNotSupported = errors.New("owned only filter is not supported") - ErrFreeOnlyNotSupported = errors.New("free only filter is not supported") - ErrPremiumOnlyNotSupported = errors.New("premium only filter is not supported") - ErrStreamOnlyNotSupported = errors.New("stream only filter is not supported") - ErrMultiHopOnlyNotSupported = errors.New("multi hop only filter is not supported") - ErrFreePremiumBothSet = errors.New("free only and premium only filters are both set") + ErrOwnedOnlyNotSupported = errors.New("owned only filter is not supported") + ErrFreeOnlyNotSupported = errors.New("free only filter is not supported") + ErrPremiumOnlyNotSupported = errors.New("premium only filter is not supported") + ErrStreamOnlyNotSupported = errors.New("stream 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") + ErrFreePremiumBothSet = errors.New("free only and premium only filters are both set") ) func (ss *ServerSelection) validate(vpnServiceProvider string, @@ -143,6 +146,15 @@ func (ss *ServerSelection) validate(vpnServiceProvider string, 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 { @@ -251,6 +263,7 @@ func (ss *ServerSelection) mergeWith(other ServerSelection) { ss.PremiumOnly = gosettings.MergeWithPointer(ss.PremiumOnly, other.PremiumOnly) ss.StreamOnly = gosettings.MergeWithPointer(ss.StreamOnly, other.StreamOnly) ss.MultiHopOnly = gosettings.MergeWithPointer(ss.MultiHopOnly, other.MultiHopOnly) + ss.PortForwardOnly = gosettings.MergeWithPointer(ss.PortForwardOnly, other.PortForwardOnly) ss.OpenVPN.mergeWith(other.OpenVPN) ss.Wireguard.mergeWith(other.Wireguard) @@ -271,6 +284,7 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) { ss.PremiumOnly = gosettings.OverrideWithPointer(ss.PremiumOnly, other.PremiumOnly) ss.StreamOnly = gosettings.OverrideWithPointer(ss.StreamOnly, other.StreamOnly) ss.MultiHopOnly = gosettings.OverrideWithPointer(ss.MultiHopOnly, other.MultiHopOnly) + ss.PortForwardOnly = gosettings.OverrideWithPointer(ss.PortForwardOnly, other.PortForwardOnly) ss.OpenVPN.overrideWith(other.OpenVPN) ss.Wireguard.overrideWith(other.Wireguard) } @@ -283,6 +297,7 @@ func (ss *ServerSelection) setDefaults(vpnProvider string) { ss.PremiumOnly = gosettings.DefaultPointer(ss.PremiumOnly, false) ss.StreamOnly = gosettings.DefaultPointer(ss.StreamOnly, false) ss.MultiHopOnly = gosettings.DefaultPointer(ss.MultiHopOnly, false) + ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, false) ss.OpenVPN.setDefaults(vpnProvider) ss.Wireguard.setDefaults() } diff --git a/internal/configuration/sources/env/serverselection.go b/internal/configuration/sources/env/serverselection.go index 1f8001a70..4ec2ec6a8 100644 --- a/internal/configuration/sources/env/serverselection.go +++ b/internal/configuration/sources/env/serverselection.go @@ -67,6 +67,12 @@ func (s *Source) readServerSelection(vpnProvider, vpnType string) ( return ss, err } + // PIA only + ss.PortForwardOnly, err = s.env.BoolPtr("PORT_FORWARD_ONLY") + if err != nil { + return ss, err + } + ss.OpenVPN, err = s.readOpenVPNSelection() if err != nil { return ss, err diff --git a/internal/provider/utils/filtering.go b/internal/provider/utils/filtering.go index 7c0059e28..a13de1ca0 100644 --- a/internal/provider/utils/filtering.go +++ b/internal/provider/utils/filtering.go @@ -53,6 +53,10 @@ func filterServer(server models.Server, return true } + if *selection.PortForwardOnly && !server.PortForward { + return true + } + if filterByPossibilities(server.Country, selection.Countries) { return true } diff --git a/internal/provider/utils/filtering_test.go b/internal/provider/utils/filtering_test.go index 3d3ebef6e..5d3f2ae01 100644 --- a/internal/provider/utils/filtering_test.go +++ b/internal/provider/utils/filtering_test.go @@ -127,6 +127,19 @@ func Test_FilterServers(t *testing.T) { {Owned: true, VPN: vpn.OpenVPN, UDP: true}, }, }, + "filter by port forwarding only": { + selection: settings.ServerSelection{ + PortForwardOnly: boolPtr(true), + }.WithDefaults(providers.PrivateInternetAccess), + servers: []models.Server{ + {PortForward: false, VPN: vpn.OpenVPN, UDP: true}, + {PortForward: true, VPN: vpn.OpenVPN, UDP: true}, + {PortForward: false, VPN: vpn.OpenVPN, UDP: true}, + }, + filtered: []models.Server{ + {PortForward: true, VPN: vpn.OpenVPN, UDP: true}, + }, + }, "filter by country": { selection: settings.ServerSelection{ Countries: []string{"b"}, diff --git a/internal/storage/filter.go b/internal/storage/filter.go index fa476e0c3..17f6725c1 100644 --- a/internal/storage/filter.go +++ b/internal/storage/filter.go @@ -74,6 +74,10 @@ func filterServer(server models.Server, return true } + if *selection.PortForwardOnly && !server.PortForward { + return true + } + if filterByPossibilities(server.Country, selection.Countries) { return true }