diff --git a/Dockerfile b/Dockerfile index 99de3da6..49992b38 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,6 +34,7 @@ ENV VPNSP=pia \ PROTOCOL=udp \ OPENVPN_VERBOSITY=1 \ OPENVPN_ROOT=no \ + OPENVPN_TARGET_IP= \ TZ= \ # PIA only PASSWORD= \ diff --git a/README.md b/README.md index aecdd1a7..2a4a9a60 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ docker run --rm --network=container:pia alpine:3.11 wget -qO- https://ipinfo.io | `TZ` | | Specify a timezone to use i.e. `Europe/London` | | `OPENVPN_VERBOSITY` | `1` | Openvpn verbosity level from 0 to 6 | | `OPENVPN_ROOT` | `no` | Run OpenVPN as root, `yes` or `no` | +| `OPENVPN_TARGET_IP` | | Specify a target VPN server IP address to use, valid for Mullvad and Private Internet Access | ## Connect to it diff --git a/cmd/main.go b/cmd/main.go index a48159c9..d9fba611 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -130,12 +130,12 @@ func main() { var connections []models.OpenVPNConnection switch allSettings.VPNSP { case "pia": - connections, err = piaConf.GetOpenVPNConnections(allSettings.PIA.Region, allSettings.OpenVPN.NetworkProtocol, allSettings.PIA.Encryption) + connections, err = piaConf.GetOpenVPNConnections(allSettings.PIA.Region, allSettings.OpenVPN.NetworkProtocol, allSettings.PIA.Encryption, allSettings.OpenVPN.TargetIP) e.FatalOnError(err) err = piaConf.BuildConf(connections, allSettings.PIA.Encryption, allSettings.OpenVPN.Verbosity, uid, gid, allSettings.OpenVPN.Root) e.FatalOnError(err) case "mullvad": - connections, err = mullvadConf.GetOpenVPNConnections(allSettings.Mullvad.Country, allSettings.Mullvad.City, allSettings.Mullvad.ISP, allSettings.OpenVPN.NetworkProtocol, allSettings.Mullvad.Port) + connections, err = mullvadConf.GetOpenVPNConnections(allSettings.Mullvad.Country, allSettings.Mullvad.City, allSettings.Mullvad.ISP, allSettings.OpenVPN.NetworkProtocol, allSettings.Mullvad.Port, allSettings.OpenVPN.TargetIP) e.FatalOnError(err) err = mullvadConf.BuildConf(connections, allSettings.OpenVPN.Verbosity, uid, gid, allSettings.OpenVPN.Root) e.FatalOnError(err) diff --git a/docker-compose.yml b/docker-compose.yml index 6b245fd1..28454e81 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,7 @@ services: - PROTOCOL=udp - OPENVPN_VERBOSITY=1 - OPENVPN_ROOT=no + - OPENVPN_TARGET_IP= - TZ= # PIA only diff --git a/internal/mullvad/conf.go b/internal/mullvad/conf.go index 5cc97c56..b0e8c923 100644 --- a/internal/mullvad/conf.go +++ b/internal/mullvad/conf.go @@ -2,13 +2,14 @@ package mullvad import ( "fmt" + "net" "github.com/qdm12/golibs/files" "github.com/qdm12/private-internet-access-docker/internal/constants" "github.com/qdm12/private-internet-access-docker/internal/models" ) -func (c *configurator) GetOpenVPNConnections(country models.MullvadCountry, city models.MullvadCity, provider models.MullvadProvider, protocol models.NetworkProtocol, customPort uint16) (connections []models.OpenVPNConnection, err error) { +func (c *configurator) GetOpenVPNConnections(country models.MullvadCountry, city models.MullvadCity, provider models.MullvadProvider, protocol models.NetworkProtocol, customPort uint16, targetIP net.IP) (connections []models.OpenVPNConnection, err error) { servers := constants.MullvadServerFilter(country, city, provider) if len(servers) == 0 { return nil, fmt.Errorf("no server found for country %q, city %q and ISP %q", country, city, provider) @@ -19,9 +20,18 @@ func (c *configurator) GetOpenVPNConnections(country models.MullvadCountry, city port = customPort } for _, IP := range server.IPs { - connections = append(connections, models.OpenVPNConnection{IP: IP, Port: port, Protocol: protocol}) + if targetIP != nil { + if targetIP.Equal(IP) { + return []models.OpenVPNConnection{{IP: IP, Port: port, Protocol: protocol}}, nil + } + } else { + connections = append(connections, models.OpenVPNConnection{IP: IP, Port: port, Protocol: protocol}) + } } } + if targetIP != nil { + return nil, fmt.Errorf("target IP address %q not found in IP addresses", targetIP) + } return connections, nil } diff --git a/internal/mullvad/mullvad.go b/internal/mullvad/mullvad.go index 05cc3eb9..d5b13c1b 100644 --- a/internal/mullvad/mullvad.go +++ b/internal/mullvad/mullvad.go @@ -1,6 +1,8 @@ package mullvad import ( + "net" + "github.com/qdm12/golibs/files" "github.com/qdm12/golibs/logging" "github.com/qdm12/golibs/network" @@ -11,7 +13,7 @@ const logPrefix = "Mullvad configurator" // Configurator contains methods to download, read and modify the openvpn configuration to connect as a client type Configurator interface { - GetOpenVPNConnections(country models.MullvadCountry, city models.MullvadCity, provider models.MullvadProvider, protocol models.NetworkProtocol, customPort uint16) (connections []models.OpenVPNConnection, err error) + GetOpenVPNConnections(country models.MullvadCountry, city models.MullvadCity, provider models.MullvadProvider, protocol models.NetworkProtocol, customPort uint16, targetIP net.IP) (connections []models.OpenVPNConnection, err error) BuildConf(connections []models.OpenVPNConnection, verbosity, uid, gid int, root bool) (err error) } diff --git a/internal/params/openvpn.go b/internal/params/openvpn.go index fc6078b2..8d4ffbf0 100644 --- a/internal/params/openvpn.go +++ b/internal/params/openvpn.go @@ -1,6 +1,9 @@ package params import ( + "fmt" + "net" + libparams "github.com/qdm12/golibs/params" "github.com/qdm12/private-internet-access-docker/internal/models" ) @@ -45,3 +48,18 @@ func (p *paramsReader) GetOpenVPNVerbosity() (verbosity int, err error) { func (p *paramsReader) GetOpenVPNRoot() (root bool, err error) { return p.envParams.GetYesNo("OPENVPN_ROOT", libparams.Default("no")) } + +// GetTargetIP obtains the IP address to choose from the list of IP addresses +// available for a particular region, from the environment variable +// OPENVPN_TARGET_IP +func (p *paramsReader) GetTargetIP() (ip net.IP, err error) { + s, err := p.envParams.GetEnv("OPENVPN_TARGET_IP") + if len(s) == 0 { + return nil, nil + } + ip = net.ParseIP(s) + if ip == nil { + return nil, fmt.Errorf("target IP address %q is not valid", s) + } + return ip, nil +} diff --git a/internal/params/params.go b/internal/params/params.go index b53d4daf..f310d24e 100644 --- a/internal/params/params.go +++ b/internal/params/params.go @@ -37,6 +37,7 @@ type ParamsReader interface { GetNetworkProtocol() (protocol models.NetworkProtocol, err error) GetOpenVPNVerbosity() (verbosity int, err error) GetOpenVPNRoot() (root bool, err error) + GetTargetIP() (ip net.IP, err error) // PIA getters GetPortForwarding() (activated bool, err error) diff --git a/internal/pia/conf.go b/internal/pia/conf.go index 122901ec..8516d7cd 100644 --- a/internal/pia/conf.go +++ b/internal/pia/conf.go @@ -2,6 +2,7 @@ package pia import ( "fmt" + "net" "strings" "github.com/qdm12/golibs/files" @@ -9,7 +10,7 @@ import ( "github.com/qdm12/private-internet-access-docker/internal/models" ) -func (c *configurator) GetOpenVPNConnections(region models.PIARegion, protocol models.NetworkProtocol, encryption models.PIAEncryption) (connections []models.OpenVPNConnection, err error) { +func (c *configurator) GetOpenVPNConnections(region models.PIARegion, protocol models.NetworkProtocol, encryption models.PIAEncryption, targetIP net.IP) (connections []models.OpenVPNConnection, err error) { geoMapping := constants.PIAGeoToSubdomainMapping() var subdomain string for r, s := range geoMapping { @@ -24,10 +25,24 @@ func (c *configurator) GetOpenVPNConnections(region models.PIARegion, protocol m if err != nil { return nil, err } - IPs, err := c.lookupIP(subdomain + ".privateinternetaccess.com") + hostname := subdomain + ".privateinternetaccess.com" + IPs, err := c.lookupIP(hostname) if err != nil { return nil, err } + if targetIP != nil { + found := false + for i := range IPs { + if IPs[i].Equal(targetIP) { + found = true + break + } + } + if !found { + return nil, fmt.Errorf("target IP address %q not found from IP addresses resolved from %s", targetIP, hostname) + } + IPs = []net.IP{targetIP} + } var port uint16 switch protocol { case constants.TCP: diff --git a/internal/pia/pia.go b/internal/pia/pia.go index 10367627..392c410d 100644 --- a/internal/pia/pia.go +++ b/internal/pia/pia.go @@ -17,7 +17,7 @@ const logPrefix = "PIA configurator" // Configurator contains methods to download, read and modify the openvpn configuration to connect as a client type Configurator interface { GetOpenVPNConnections(region models.PIARegion, protocol models.NetworkProtocol, - encryption models.PIAEncryption) (connections []models.OpenVPNConnection, err error) + encryption models.PIAEncryption, targetIP net.IP) (connections []models.OpenVPNConnection, err error) BuildConf(connections []models.OpenVPNConnection, encryption models.PIAEncryption, verbosity, uid, gid int, root bool) (err error) GetPortForward() (port uint16, err error) WritePortForward(filepath models.Filepath, port uint16) (err error) diff --git a/internal/settings/openvpn.go b/internal/settings/openvpn.go index 78e08262..62b55f98 100644 --- a/internal/settings/openvpn.go +++ b/internal/settings/openvpn.go @@ -2,6 +2,7 @@ package settings import ( "fmt" + "net" "strings" "github.com/qdm12/private-internet-access-docker/internal/models" @@ -13,6 +14,7 @@ type OpenVPN struct { NetworkProtocol models.NetworkProtocol Verbosity int Root bool + TargetIP net.IP } // GetOpenVPNSettings obtains the OpenVPN settings using the params functions @@ -26,6 +28,13 @@ func GetOpenVPNSettings(params params.ParamsReader) (settings OpenVPN, err error return settings, err } settings.Root, err = params.GetOpenVPNRoot() + if err != nil { + return settings, err + } + settings.TargetIP, err = params.GetTargetIP() + if err != nil { + return settings, err + } return settings, nil } @@ -39,6 +48,7 @@ func (o *OpenVPN) String() string { "Network protocol: " + string(o.NetworkProtocol), "Verbosity level: " + fmt.Sprintf("%d", o.Verbosity), "Run as root: " + runAsRoot, + "Target IP address: " + o.TargetIP.String(), } return strings.Join(settingsList, "\n|--") }