Skip to content

Commit

Permalink
feat(privatevpn): support natively port forwarding
Browse files Browse the repository at this point in the history
  • Loading branch information
qdm12 committed May 18, 2024
1 parent 7872ab9 commit d54a40b
Show file tree
Hide file tree
Showing 11 changed files with 115 additions and 20 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Lightweight swiss-knife-like VPN client to multiple VPN service providers
- [Connect other containers to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-container-to-gluetun.md)
- [Connect LAN devices to it](https://github.com/qdm12/gluetun-wiki/blob/main/setup/connect-a-lan-device-to-gluetun.md)
- Compatible with amd64, i686 (32 bit), **ARM** 64 bit, ARM 32 bit v6 and v7, and even ppc64le 🎆
- [Custom VPN server side port forwarding for Private Internet Access](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/private-internet-access.md#vpn-server-port-forwarding)
- [Custom VPN server side port forwarding for Private Internet Access, ProtonVPN and PrivateVPN](https://github.com/qdm12/gluetun-wiki/blob/main/setup/providers/private-internet-access.md#vpn-server-port-forwarding)
- Possibility of split horizon DNS by selecting multiple DNS over TLS providers
- Unbound subprogram drops root privileges once launched
- Can work as a Kubernetes sidecar container, thanks @rorph
Expand Down
1 change: 1 addition & 0 deletions internal/configuration/settings/portforward.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
}
validProviders := []string{
providers.PrivateInternetAccess,
providers.Privatevpn,
providers.Protonvpn,
}
if err = validate.IsOneOf(providerSelected, validProviders...); err != nil {
Expand Down
13 changes: 10 additions & 3 deletions internal/portforward/service/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package service
import (
"errors"
"fmt"
"net/netip"

"github.com/qdm12/gluetun/internal/constants/providers"
"github.com/qdm12/gosettings"
Expand All @@ -12,9 +13,10 @@ type Settings struct {
Enabled *bool
PortForwarder PortForwarder
Filepath string
Interface string // needed for PIA and ProtonVPN, tun0 for example
ServerName string // needed for PIA
CanPortForward bool // needed for PIA
Interface string // needed for PIA and ProtonVPN, tun0 for example
ServerName string // needed for PIA
ServerIP netip.Addr // needed for PrivateVPN
CanPortForward bool // needed for PIA
ListeningPort uint16
}

Expand All @@ -24,6 +26,7 @@ func (s Settings) Copy() (copied Settings) {
copied.Filepath = s.Filepath
copied.Interface = s.Interface
copied.ServerName = s.ServerName
copied.ServerIP = s.ServerIP
copied.CanPortForward = s.CanPortForward
copied.ListeningPort = s.ListeningPort
return copied
Expand All @@ -35,13 +38,15 @@ func (s *Settings) OverrideWith(update Settings) {
s.Filepath = gosettings.OverrideWithComparable(s.Filepath, update.Filepath)
s.Interface = gosettings.OverrideWithComparable(s.Interface, update.Interface)
s.ServerName = gosettings.OverrideWithComparable(s.ServerName, update.ServerName)
s.ServerIP = gosettings.OverrideWithComparable(s.ServerIP, update.ServerIP)
s.CanPortForward = gosettings.OverrideWithComparable(s.CanPortForward, update.CanPortForward)
s.ListeningPort = gosettings.OverrideWithComparable(s.ListeningPort, update.ListeningPort)
}

var (
ErrPortForwarderNotSet = errors.New("port forwarder not set")
ErrServerNameNotSet = errors.New("server name not set")
ErrServerIPNotSet = errors.New("server ip not set")
ErrFilepathNotSet = errors.New("file path not set")
ErrInterfaceNotSet = errors.New("interface not set")
)
Expand All @@ -66,6 +71,8 @@ func (s *Settings) Validate(forStartup bool) (err error) {
return fmt.Errorf("%w", ErrInterfaceNotSet)
case s.PortForwarder.Name() == providers.PrivateInternetAccess && s.ServerName == "":
return fmt.Errorf("%w", ErrServerNameNotSet)
case s.PortForwarder.Name() == providers.Privatevpn && !s.ServerIP.IsValid():
return fmt.Errorf("%w", ErrServerIPNotSet)
}
return nil
}
1 change: 1 addition & 0 deletions internal/portforward/service/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func (s *Service) Start(ctx context.Context) (runError <-chan error, err error)
Gateway: gateway,
Client: s.client,
ServerName: s.settings.ServerName,
ServerIP: s.settings.ServerIP,
CanPortForward: s.settings.CanPortForward,
}
port, err := s.settings.PortForwarder.PortForward(ctx, obj)
Expand Down
76 changes: 76 additions & 0 deletions internal/provider/privatevpn/portforward.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package privatevpn

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"regexp"
"strconv"

"github.com/qdm12/gluetun/internal/provider/common"
"github.com/qdm12/gluetun/internal/provider/utils"
)

var (
regexPort = regexp.MustCompile(`^[1-9][0-9]{0,4}$`)
)

var (
ErrPortForwardedNotFound = errors.New("port forwarded not found")
)

// PortForward obtains a VPN server side port forwarded from the PrivateVPN API.
// It returns 0 if all ports are to forwarded on a dedicated server IP.
func (p *Provider) PortForward(ctx context.Context, objects utils.PortForwardObjects) (
port uint16, err error) {
url := "https://connect.pvdatanet.com/v3/Api/port?ip[]=" + objects.ServerIP.String()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return 0, fmt.Errorf("creating HTTP request: %w", err)
}

response, err := objects.Client.Do(request)
if err != nil {
return 0, fmt.Errorf("sending HTTP request: %w", err)
}

if response.StatusCode != http.StatusOK {
return 0, fmt.Errorf("%w: %d %s", common.ErrHTTPStatusCodeNotOK,
response.StatusCode, response.Status)
}

defer response.Body.Close()
decoder := json.NewDecoder(response.Body)
var data struct {
Status string `json:"status"`
Supported bool `json:"supported"`
}
err = decoder.Decode(&data)
if err != nil {
return 0, fmt.Errorf("decoding JSON response: %w", err)
} else if !data.Supported {
return 0, fmt.Errorf("%w: for server IP %s",
common.ErrPortForwardNotSupported, objects.ServerIP)
}

portString := regexPort.FindString(data.Status)
if portString == "" {
return 0, fmt.Errorf("%w: in status %q", ErrPortForwardedNotFound, data.Status)
}

const base, bitSize = 10, 16
portUint64, err := strconv.ParseUint(portString, base, bitSize)
if err != nil {
return 0, fmt.Errorf("parsing port %q: %w", portString, err)
}
port = uint16(portUint64)
return port, nil
}

func (p *Provider) KeepPortForward(ctx context.Context,
_ utils.PortForwardObjects) (err error) {
<-ctx.Done()
return ctx.Err()
}
2 changes: 2 additions & 0 deletions internal/provider/utils/portforward.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ type PortForwardObjects struct {
Client *http.Client
// ServerName is used by Private Internet Access for port forwarding.
ServerName string
// ServerIP is used by PrivateVPN for port forwarding.
ServerIP netip.Addr
// CanPortForward is used by Private Internet Access for port forwarding.
CanPortForward bool
}
Expand Down
15 changes: 8 additions & 7 deletions internal/vpn/openvpn.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package vpn
import (
"context"
"fmt"
"net/netip"

"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/openvpn"
Expand All @@ -16,37 +17,37 @@ func setupOpenVPN(ctx context.Context, fw Firewall,
openvpnConf OpenVPN, providerConf provider.Provider,
settings settings.VPN, ipv6Supported bool, starter command.Starter,
logger openvpn.Logger) (runner *openvpn.Runner, serverName string,
canPortForward bool, err error) {
serverIP netip.Addr, canPortForward bool, err error) {
connection, err := providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Supported)
if err != nil {
return nil, "", false, fmt.Errorf("finding a valid server connection: %w", err)
return nil, "", netip.Addr{}, false, fmt.Errorf("finding a valid server connection: %w", err)
}

lines := providerConf.OpenVPNConfig(connection, settings.OpenVPN, ipv6Supported)

if err := openvpnConf.WriteConfig(lines); err != nil {
return nil, "", false, fmt.Errorf("writing configuration to file: %w", err)
return nil, "", netip.Addr{}, false, fmt.Errorf("writing configuration to file: %w", err)
}

if *settings.OpenVPN.User != "" {
err := openvpnConf.WriteAuthFile(*settings.OpenVPN.User, *settings.OpenVPN.Password)
if err != nil {
return nil, "", false, fmt.Errorf("writing auth to file: %w", err)
return nil, "", netip.Addr{}, false, fmt.Errorf("writing auth to file: %w", err)
}
}

if *settings.OpenVPN.KeyPassphrase != "" {
err := openvpnConf.WriteAskPassFile(*settings.OpenVPN.KeyPassphrase)
if err != nil {
return nil, "", false, fmt.Errorf("writing askpass file: %w", err)
return nil, "", netip.Addr{}, false, fmt.Errorf("writing askpass file: %w", err)
}
}

if err := fw.SetVPNConnection(ctx, connection, settings.OpenVPN.Interface); err != nil {
return nil, "", false, fmt.Errorf("allowing VPN connection through firewall: %w", err)
return nil, "", netip.Addr{}, false, fmt.Errorf("allowing VPN connection through firewall: %w", err)
}

runner = openvpn.NewRunner(settings.OpenVPN, starter, logger)

return runner, connection.ServerName, connection.PortForward, nil
return runner, connection.ServerName, connection.IP, connection.PortForward, nil
}
1 change: 1 addition & 0 deletions internal/vpn/portforward.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func (l *Loop) startPortForwarding(data tunnelUpData) (err error) {
PortForwarder: data.portForwarder,
Interface: data.vpnIntf,
ServerName: data.serverName,
ServerIP: data.serverIP,
CanPortForward: data.canPortForward,
},
}
Expand Down
7 changes: 5 additions & 2 deletions internal/vpn/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package vpn

import (
"context"
"net/netip"

"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/vpn"
Expand Down Expand Up @@ -29,16 +30,17 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
Run(ctx context.Context, waitError chan<- error, tunnelReady chan<- struct{})
}
var serverName, vpnInterface string
var serverIP netip.Addr
var canPortForward bool
var err error
subLogger := l.logger.New(log.SetComponent(settings.Type))
if settings.Type == vpn.OpenVPN {
vpnInterface = settings.OpenVPN.Interface
vpnRunner, serverName, canPortForward, err = setupOpenVPN(ctx, l.fw,
vpnRunner, serverName, serverIP, canPortForward, err = setupOpenVPN(ctx, l.fw,
l.openvpnConf, providerConf, settings, l.ipv6Supported, l.starter, subLogger)
} else { // Wireguard
vpnInterface = settings.Wireguard.Interface
vpnRunner, serverName, canPortForward, err = setupWireguard(ctx, l.netLinker, l.fw,
vpnRunner, serverName, serverIP, canPortForward, err = setupWireguard(ctx, l.netLinker, l.fw,
providerConf, settings, l.ipv6Supported, subLogger)
}
if err != nil {
Expand All @@ -47,6 +49,7 @@ func (l *Loop) Run(ctx context.Context, done chan<- struct{}) {
}
tunnelUpData := tunnelUpData{
serverName: serverName,
serverIP: serverIP,
canPortForward: canPortForward,
portForwarder: portForwarder,
vpnIntf: vpnInterface,
Expand Down
6 changes: 4 additions & 2 deletions internal/vpn/tunnelup.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package vpn

import (
"context"
"net/netip"

"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/version"
Expand All @@ -10,8 +11,9 @@ import (
type tunnelUpData struct {
// Port forwarding
vpnIntf string
serverName string // used for PIA
canPortForward bool // used for PIA
serverName string // used for PIA
serverIP netip.Addr // used for PrivateVPN
canPortForward bool // used for PIA
portForwarder PortForwarder
}

Expand Down
11 changes: 6 additions & 5 deletions internal/vpn/wireguard.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package vpn
import (
"context"
"fmt"
"net/netip"

"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/provider"
Expand All @@ -16,10 +17,10 @@ import (
func setupWireguard(ctx context.Context, netlinker NetLinker,
fw Firewall, providerConf provider.Provider,
settings settings.VPN, ipv6Supported bool, logger wireguard.Logger) (
wireguarder *wireguard.Wireguard, serverName string, canPortForward bool, err error) {
wireguarder *wireguard.Wireguard, serverName string, serverIP netip.Addr, canPortForward bool, err error) {
connection, err := providerConf.GetConnection(settings.Provider.ServerSelection, ipv6Supported)
if err != nil {
return nil, "", false, fmt.Errorf("finding a VPN server: %w", err)
return nil, "", netip.Addr{}, false, fmt.Errorf("finding a VPN server: %w", err)
}

wireguardSettings := utils.BuildWireguardSettings(connection, settings.Wireguard, ipv6Supported)
Expand All @@ -30,13 +31,13 @@ func setupWireguard(ctx context.Context, netlinker NetLinker,

wireguarder, err = wireguard.New(wireguardSettings, netlinker, logger)
if err != nil {
return nil, "", false, fmt.Errorf("creating Wireguard: %w", err)
return nil, "", netip.Addr{}, false, fmt.Errorf("creating Wireguard: %w", err)
}

err = fw.SetVPNConnection(ctx, connection, settings.Wireguard.Interface)
if err != nil {
return nil, "", false, fmt.Errorf("setting firewall: %w", err)
return nil, "", netip.Addr{}, false, fmt.Errorf("setting firewall: %w", err)
}

return wireguarder, connection.ServerName, connection.PortForward, nil
return wireguarder, connection.ServerName, connection.IP, connection.PortForward, nil
}

0 comments on commit d54a40b

Please sign in to comment.