diff --git a/README.md b/README.md index dabf1168..0a84ba2e 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers - Supports: **AirVPN**, **Cyberghost**, **ExpressVPN**, **FastestVPN**, **HideMyAss**, **IPVanish**, **IVPN**, **Mullvad**, **NordVPN**, **Perfect Privacy**, **Privado**, **Private Internet Access**, **PrivateVPN**, **ProtonVPN**, **PureVPN**, **SlickVPN**, **Surfshark**, **TorGuard**, **VPNSecure.me**, **VPNUnlimited**, **Vyprvpn**, **WeVPN**, **Windscribe** servers - Supports OpenVPN for all providers listed - Supports Wireguard both kernelspace and userspace - - For **AirVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Surfshark** and **Windscribe** + - For **AirVPN**, **Ivpn**, **Mullvad**, **NordVPN**, **Perfect privacy**, **Surfshark** and **Windscribe** - For **ProtonVPN**, **PureVPN**, **Torguard**, **VPN Unlimited** and **WeVPN** using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md) - For custom Wireguard configurations using [the custom provider](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/custom.md) - More in progress, see [#134](https://github.com/qdm12/gluetun/issues/134) diff --git a/internal/configuration/settings/portforward.go b/internal/configuration/settings/portforward.go index 156abdc0..d8b2457b 100644 --- a/internal/configuration/settings/portforward.go +++ b/internal/configuration/settings/portforward.go @@ -50,6 +50,7 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) { providerSelected = *p.Provider } validProviders := []string{ + providers.Perfectprivacy, providers.PrivateInternetAccess, providers.Protonvpn, } diff --git a/internal/portforward/interfaces.go b/internal/portforward/interfaces.go index 6b9eee0e..c3c610ae 100644 --- a/internal/portforward/interfaces.go +++ b/internal/portforward/interfaces.go @@ -13,6 +13,7 @@ type Service interface { type Routing interface { VPNLocalGatewayIP(vpnInterface string) (gateway netip.Addr, err error) + AssignedIP(interfaceName string, family int) (ip netip.Addr, err error) } type PortAllower interface { diff --git a/internal/portforward/service/interfaces.go b/internal/portforward/service/interfaces.go index 7cf7d4c1..9a1f7c04 100644 --- a/internal/portforward/service/interfaces.go +++ b/internal/portforward/service/interfaces.go @@ -16,6 +16,7 @@ type PortAllower interface { type Routing interface { VPNLocalGatewayIP(vpnInterface string) (gateway netip.Addr, err error) + AssignedIP(interfaceName string, family int) (ip netip.Addr, err error) } type Logger interface { diff --git a/internal/portforward/service/start.go b/internal/portforward/service/start.go index 4f308375..b6bc7726 100644 --- a/internal/portforward/service/start.go +++ b/internal/portforward/service/start.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/qdm12/gluetun/internal/netlink" "github.com/qdm12/gluetun/internal/provider/utils" ) @@ -22,9 +23,19 @@ func (s *Service) Start(ctx context.Context) (runError <-chan error, err error) return nil, fmt.Errorf("getting VPN local gateway IP: %w", err) } + family := netlink.FamilyV4 + if gateway.Is6() { + family = netlink.FamilyV6 + } + internalIP, err := s.routing.AssignedIP(s.settings.Interface, family) + if err != nil { + return nil, fmt.Errorf("getting VPN assigned IP address: %w", err) + } + obj := utils.PortForwardObjects{ Logger: s.logger, Gateway: gateway, + InternalIP: internalIP, Client: s.client, ServerName: s.settings.ServerName, CanPortForward: s.settings.CanPortForward, diff --git a/internal/provider/perfectprivacy/portforward.go b/internal/provider/perfectprivacy/portforward.go new file mode 100644 index 00000000..db4bbb9d --- /dev/null +++ b/internal/provider/perfectprivacy/portforward.go @@ -0,0 +1,43 @@ +package perfectprivacy + +import ( + "context" + "net/netip" + + "github.com/qdm12/gluetun/internal/provider/utils" +) + +// PortForward calculates and returns the VPN server side ports forwarded. +func (p *Provider) PortForward(_ context.Context, + objects utils.PortForwardObjects) (ports []uint16, err error) { + if !objects.InternalIP.IsValid() { + panic("internal ip is not set") + } + + return internalIPToPorts(objects.InternalIP), nil +} + +func (p *Provider) KeepPortForward(ctx context.Context, + _ utils.PortForwardObjects) (err error) { + <-ctx.Done() + return ctx.Err() +} + +// See https://www.perfect-privacy.com/en/faq section +// How are the default forwarding ports being calculated? +func internalIPToPorts(internalIP netip.Addr) (ports []uint16) { + internalIPBytes := internalIP.AsSlice() + // Convert the internal IP address to a bit string + // and keep only the last 12 bits + last16Bits := internalIPBytes[len(internalIPBytes)-2:] + last12Bits := []byte{ + last16Bits[0] & 0b00001111, // only keep 4 bits + last16Bits[1], + } + basePort := uint16(last12Bits[0])<<8 + uint16(last12Bits[1]) //nolint:gomnd + return []uint16{ + 10000 + basePort, + 20000 + basePort, + 30000 + basePort, + } +} diff --git a/internal/provider/perfectprivacy/portforward_test.go b/internal/provider/perfectprivacy/portforward_test.go new file mode 100644 index 00000000..9aae7daa --- /dev/null +++ b/internal/provider/perfectprivacy/portforward_test.go @@ -0,0 +1,33 @@ +package perfectprivacy + +import ( + "net/netip" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_internalIPToPorts(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + internalIP netip.Addr + ports []uint16 + }{ + "example_case": { + internalIP: netip.AddrFrom4([4]byte{10, 0, 203, 88}), + ports: []uint16{12904, 22904, 32904}, + }, + } + + for name, testCase := range testCases { + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + ports := internalIPToPorts(testCase.internalIP) + + assert.Equal(t, testCase.ports, ports) + }) + } +} diff --git a/internal/provider/utils/portforward.go b/internal/provider/utils/portforward.go index 0dd2a8f5..6320f3e1 100644 --- a/internal/provider/utils/portforward.go +++ b/internal/provider/utils/portforward.go @@ -13,6 +13,8 @@ type PortForwardObjects struct { // Gateway is the VPN gateway IP address, used by Private Internet Access // and ProtonVPN. Gateway netip.Addr + // InternalIP is the VPN internal IP address assigned, used by Perfect Privacy. + InternalIP netip.Addr // Client is used to query the VPN gateway for Private Internet Access. Client *http.Client // ServerName is used by Private Internet Access for port forwarding. diff --git a/internal/routing/default.go b/internal/routing/default.go index 0209f379..8ceb5d11 100644 --- a/internal/routing/default.go +++ b/internal/routing/default.go @@ -53,7 +53,7 @@ func (r *Routing) DefaultRoutes() (defaultRoutes []DefaultRoute, err error) { if route.Gw.Is4() { family = netlink.FamilyV4 } - defaultRoute.AssignedIP, err = r.assignedIP(defaultRoute.NetInterface, family) + defaultRoute.AssignedIP, err = r.AssignedIP(defaultRoute.NetInterface, family) if err != nil { return nil, fmt.Errorf("getting assigned IP of %s: %w", defaultRoute.NetInterface, err) } diff --git a/internal/routing/ip.go b/internal/routing/ip.go index 99da450e..a80c04e8 100644 --- a/internal/routing/ip.go +++ b/internal/routing/ip.go @@ -23,7 +23,7 @@ func ipMatchesFamily(ip netip.Addr, family int) bool { (family == netlink.FamilyV6 && ip.Is6()) } -func (r *Routing) assignedIP(interfaceName string, family int) (ip netip.Addr, err error) { +func (r *Routing) AssignedIP(interfaceName string, family int) (ip netip.Addr, err error) { iface, err := net.InterfaceByName(interfaceName) if err != nil { return ip, fmt.Errorf("network interface %s not found: %w", interfaceName, err) diff --git a/internal/routing/local.go b/internal/routing/local.go index 0b7ba84d..d463d9dc 100644 --- a/internal/routing/local.go +++ b/internal/routing/local.go @@ -70,7 +70,7 @@ func (r *Routing) LocalNetworks() (localNetworks []LocalNetwork, err error) { if localNet.IPNet.Addr().Is4() { family = netlink.FamilyV4 } - ip, err := r.assignedIP(localNet.InterfaceName, family) + ip, err := r.AssignedIP(localNet.InterfaceName, family) if err != nil { return localNetworks, err }