- Goal was to simplify main.go complexity
- Use common structures and interfaces for all vpn providers
- Moved files around
- Removed some alias models
This commit is contained in:
Quentin McGaw
2020-06-13 14:08:29 -04:00
committed by GitHub
parent 4f502abcf8
commit 7369808b84
47 changed files with 1530 additions and 1693 deletions

View File

@@ -0,0 +1,5 @@
package provider
const (
aes256cbc = "aes-256-cbc"
)

View File

@@ -0,0 +1,135 @@
package provider
import (
"context"
"fmt"
"net"
"strings"
"github.com/qdm12/golibs/files"
"github.com/qdm12/private-internet-access-docker/internal/constants"
"github.com/qdm12/private-internet-access-docker/internal/models"
)
type cyberghost struct {
fileManager files.FileManager
}
func newCyberghost(fileManager files.FileManager) *cyberghost {
return &cyberghost{fileManager: fileManager}
}
func (c *cyberghost) GetOpenVPNConnections(selection models.ServerSelection) (connections []models.OpenVPNConnection, err error) {
var IPs []net.IP
for _, server := range constants.CyberghostServers() {
if strings.EqualFold(server.Region, selection.Region) && strings.EqualFold(server.Group, selection.Group) {
IPs = server.IPs
}
}
if len(IPs) == 0 {
return nil, fmt.Errorf("no IP found for group %q and region %q", selection.Group, selection.Region)
}
if selection.TargetIP != nil {
found := false
for i := range IPs {
if IPs[i].Equal(selection.TargetIP) {
found = true
break
}
}
if !found {
return nil, fmt.Errorf("target IP address %q not found in IP addresses", selection.TargetIP)
}
IPs = []net.IP{selection.TargetIP}
}
for _, IP := range IPs {
connections = append(connections, models.OpenVPNConnection{IP: IP, Port: 1443, Protocol: selection.Protocol})
}
return connections, nil
}
func (c *cyberghost) BuildConf(connections []models.OpenVPNConnection, verbosity, uid, gid int, root bool, cipher, auth string, extras models.ExtraConfigOptions) (err error) {
if len(cipher) == 0 {
cipher = aes256cbc
}
if len(auth) == 0 {
auth = "SHA256"
}
lines := []string{
"client",
"dev tun",
"nobind",
"persist-key",
"persist-tun",
"remote-cert-tls server",
// Cyberghost specific
"resolv-retry infinite",
"redirect-gateway def1",
"ncp-disable",
"ping 5",
"ping-exit 60",
"ping-timer-rem",
"script-security 2",
"route-delay 5",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"auth-retry nointeract",
"remote-random",
"suppress-timestamps",
// Modified variables
fmt.Sprintf("verb %d", verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", string(connections[0].Protocol)),
fmt.Sprintf("cipher %s", cipher),
fmt.Sprintf("auth %s", auth),
}
if strings.HasSuffix(cipher, "-gcm") {
lines = append(lines, "ncp-ciphers AES-256-GCM:AES-256-CBC:AES-128-GCM")
}
if !root {
lines = append(lines, "user nonrootuser")
}
for _, connection := range connections {
lines = append(lines, fmt.Sprintf("remote %s %d", connection.IP.String(), connection.Port))
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.CyberghostCertificate,
"-----END CERTIFICATE-----",
"</ca>",
}...)
lines = append(lines, []string{
"<crt>",
"-----BEGIN CERTIFICATE-----",
constants.CyberghostClientCertificate,
"-----END CERTIFICATE-----",
"</crt>",
}...)
lines = append(lines, []string{
"<key>",
"-----BEGIN PRIVATE KEY-----",
extras.ClientKey,
"-----END PRIVATE KEY-----",
"</key>",
"",
}...)
return c.fileManager.WriteLinesToFile(string(constants.OpenVPNConf), lines, files.Ownership(uid, gid), files.Permissions(0400))
}
func (c *cyberghost) GetPortForward() (port uint16, err error) {
panic("port forwarding is not supported for cyberghost")
}
func (c *cyberghost) WritePortForward(filepath models.Filepath, port uint16, uid, gid int) (err error) {
panic("port forwarding is not supported for cyberghost")
}
func (c *cyberghost) AllowPortForwardFirewall(ctx context.Context, device models.VPNDevice, port uint16) (err error) {
panic("port forwarding is not supported for cyberghost")
}

View File

@@ -0,0 +1,115 @@
package provider
import (
"context"
"fmt"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/private-internet-access-docker/internal/constants"
"github.com/qdm12/private-internet-access-docker/internal/models"
)
type mullvad struct {
fileManager files.FileManager
logger logging.Logger
}
func newMullvad(fileManager files.FileManager, logger logging.Logger) *mullvad {
return &mullvad{
fileManager: fileManager,
logger: logger.WithPrefix("Mullvad configurator: "),
}
}
func (m *mullvad) GetOpenVPNConnections(selection models.ServerSelection) (connections []models.OpenVPNConnection, err error) {
servers := constants.MullvadServerFilter(selection.Country, selection.City, selection.ISP)
if len(servers) == 0 {
return nil, fmt.Errorf("no server found for country %q, city %q and ISP %q", selection.Country, selection.City, selection.ISP)
}
for _, server := range servers {
port := server.DefaultPort
if selection.CustomPort > 0 {
port = selection.CustomPort
}
for _, IP := range server.IPs {
if selection.TargetIP != nil {
if selection.TargetIP.Equal(IP) {
return []models.OpenVPNConnection{{IP: IP, Port: port, Protocol: selection.Protocol}}, nil
}
} else {
connections = append(connections, models.OpenVPNConnection{IP: IP, Port: port, Protocol: selection.Protocol})
}
}
}
if selection.TargetIP != nil {
return nil, fmt.Errorf("target IP address %q not found in IP addresses", selection.TargetIP)
}
return connections, nil
}
func (m *mullvad) BuildConf(connections []models.OpenVPNConnection, verbosity, uid, gid int, root bool, cipher, auth string, extras models.ExtraConfigOptions) (err error) {
if len(connections) == 0 {
return fmt.Errorf("at least one connection string is expected")
}
if len(cipher) == 0 {
cipher = aes256cbc
}
lines := []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
// Mullvad specific
"ping 10",
"ping-restart 60",
"sndbuf 524288",
"rcvbuf 524288",
"tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA",
"tun-ipv6",
"fast-io",
// Added constant values
"mute-replay-warnings",
"auth-nocache",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"auth-retry nointeract",
"remote-random",
"suppress-timestamps",
// Modified variables
fmt.Sprintf("verb %d", verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", string(connections[0].Protocol)),
fmt.Sprintf("cipher %s", cipher),
}
if !root {
lines = append(lines, "user nonrootuser")
}
for _, connection := range connections {
lines = append(lines, fmt.Sprintf("remote %s %d", connection.IP.String(), connection.Port))
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.MullvadCertificate,
"-----END CERTIFICATE-----",
"</ca>",
"",
}...)
return m.fileManager.WriteLinesToFile(string(constants.OpenVPNConf), lines, files.Ownership(uid, gid), files.Permissions(0400))
}
func (m *mullvad) GetPortForward() (port uint16, err error) {
panic("port forwarding is not supported for mullvad")
}
func (m *mullvad) WritePortForward(filepath models.Filepath, port uint16, uid, gid int) (err error) {
panic("port forwarding is not supported for mullvad")
}
func (m *mullvad) AllowPortForwardFirewall(ctx context.Context, device models.VPNDevice, port uint16) (err error) {
panic("port forwarding is not supported for mullvad")
}

199
internal/provider/pia.go Normal file
View File

@@ -0,0 +1,199 @@
package provider
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"net"
"net/http"
"strings"
"github.com/qdm12/golibs/crypto/random"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/network"
"github.com/qdm12/golibs/verification"
"github.com/qdm12/private-internet-access-docker/internal/constants"
"github.com/qdm12/private-internet-access-docker/internal/firewall"
"github.com/qdm12/private-internet-access-docker/internal/models"
)
type pia struct {
client network.Client
fileManager files.FileManager
firewall firewall.Configurator
random random.Random
verifyPort func(port string) error
lookupIP func(host string) ([]net.IP, error)
}
func newPrivateInternetAccess(client network.Client, fileManager files.FileManager, firewall firewall.Configurator) *pia {
return &pia{
client: client,
fileManager: fileManager,
firewall: firewall,
random: random.NewRandom(),
verifyPort: verification.NewVerifier().VerifyPort,
lookupIP: net.LookupIP}
}
func (p *pia) GetOpenVPNConnections(selection models.ServerSelection) (connections []models.OpenVPNConnection, err error) {
var IPs []net.IP
for _, server := range constants.PIAServers() {
if strings.EqualFold(server.Region, selection.Region) {
IPs = server.IPs
}
}
if len(IPs) == 0 {
return nil, fmt.Errorf("no IP found for region %q", selection.Region)
}
if selection.TargetIP != nil {
found := false
for i := range IPs {
if IPs[i].Equal(selection.TargetIP) {
found = true
break
}
}
if !found {
return nil, fmt.Errorf("target IP address %q not found in IP addresses", selection.TargetIP)
}
IPs = []net.IP{selection.TargetIP}
}
var port uint16
switch selection.Protocol {
case constants.TCP:
switch selection.EncryptionPreset {
case constants.PIAEncryptionPresetNormal:
port = 502
case constants.PIAEncryptionPresetStrong:
port = 501
}
case constants.UDP:
switch selection.EncryptionPreset {
case constants.PIAEncryptionPresetNormal:
port = 1198
case constants.PIAEncryptionPresetStrong:
port = 1197
}
}
if port == 0 {
return nil, fmt.Errorf("combination of protocol %q and encryption %q does not yield any port number", selection.Protocol, selection.EncryptionPreset)
}
for _, IP := range IPs {
connections = append(connections, models.OpenVPNConnection{IP: IP, Port: port, Protocol: selection.Protocol})
}
return connections, nil
}
func (p *pia) BuildConf(connections []models.OpenVPNConnection, verbosity, uid, gid int, root bool, cipher, auth string, extras models.ExtraConfigOptions) (err error) {
var X509CRL, certificate string
if extras.EncryptionPreset == constants.PIAEncryptionPresetNormal {
if len(cipher) == 0 {
cipher = "aes-128-cbc"
}
if len(auth) == 0 {
auth = "sha1"
}
X509CRL = constants.PiaX509CRLNormal
certificate = constants.PIACertificateNormal
} else { // strong encryption
if len(cipher) == 0 {
cipher = aes256cbc
}
if len(auth) == 0 {
auth = "sha256"
}
X509CRL = constants.PiaX509CRLStrong
certificate = constants.PIACertificateStrong
}
lines := []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
// PIA specific
"ping 300", // Ping every 5 minutes to prevent a timeout error
"reneg-sec 0",
"compress", // allow PIA server to choose the compression to use
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"auth-retry nointeract",
"remote-random",
"suppress-timestamps",
// Modified variables
fmt.Sprintf("verb %d", verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", string(connections[0].Protocol)),
fmt.Sprintf("cipher %s", cipher),
fmt.Sprintf("auth %s", auth),
}
if strings.HasSuffix(cipher, "-gcm") {
lines = append(lines, "ncp-disable")
}
if !root {
lines = append(lines, "user nonrootuser")
}
for _, connection := range connections {
lines = append(lines, fmt.Sprintf("remote %s %d", connection.IP.String(), connection.Port))
}
lines = append(lines, []string{
"<crl-verify>",
"-----BEGIN X509 CRL-----",
X509CRL,
"-----END X509 CRL-----",
"</crl-verify>",
}...)
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
certificate,
"-----END CERTIFICATE-----",
"</ca>",
"",
}...)
return p.fileManager.WriteLinesToFile(string(constants.OpenVPNConf), lines, files.Ownership(uid, gid), files.Permissions(0400))
}
func (p *pia) GetPortForward() (port uint16, err error) {
b, err := p.random.GenerateRandomBytes(32)
if err != nil {
return 0, err
}
clientID := hex.EncodeToString(b)
url := fmt.Sprintf("%s/?client_id=%s", constants.PIAPortForwardURL, clientID)
content, status, err := p.client.GetContent(url)
switch {
case err != nil:
return 0, err
case status != http.StatusOK:
return 0, fmt.Errorf("status is %d for %s; does your PIA server support port forwarding?", status, url)
case len(content) == 0:
return 0, fmt.Errorf("port forwarding is already activated on this connection, has expired, or you are not connected to a PIA region that supports port forwarding")
}
body := struct {
Port uint16 `json:"port"`
}{}
if err := json.Unmarshal(content, &body); err != nil {
return 0, fmt.Errorf("port forwarding response: %w", err)
}
return body.Port, nil
}
func (p *pia) WritePortForward(filepath models.Filepath, port uint16, uid, gid int) (err error) {
return p.fileManager.WriteLinesToFile(
string(filepath),
[]string{fmt.Sprintf("%d", port)},
files.Ownership(uid, gid),
files.Permissions(0400))
}
func (p *pia) AllowPortForwardFirewall(ctx context.Context, device models.VPNDevice, port uint16) (err error) {
return p.firewall.AllowInputTrafficOnPort(ctx, device, port)
}

View File

@@ -0,0 +1,38 @@
package provider
import (
"context"
"github.com/qdm12/golibs/files"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/network"
"github.com/qdm12/private-internet-access-docker/internal/constants"
"github.com/qdm12/private-internet-access-docker/internal/firewall"
"github.com/qdm12/private-internet-access-docker/internal/models"
)
// Provider contains methods to read and modify the openvpn configuration to connect as a client
type Provider interface {
GetOpenVPNConnections(selection models.ServerSelection) (connections []models.OpenVPNConnection, err error)
BuildConf(connections []models.OpenVPNConnection, verbosity, uid, gid int, root bool, cipher, auth string, extras models.ExtraConfigOptions) (err error)
GetPortForward() (port uint16, err error)
WritePortForward(filepath models.Filepath, port uint16, uid, gid int) (err error)
AllowPortForwardFirewall(ctx context.Context, device models.VPNDevice, port uint16) (err error)
}
func New(provider models.VPNProvider, logger logging.Logger, client network.Client, fileManager files.FileManager, firewall firewall.Configurator) Provider {
switch provider {
case constants.PrivateInternetAccess:
return newPrivateInternetAccess(client, fileManager, firewall)
case constants.Mullvad:
return newMullvad(fileManager, logger)
case constants.Windscribe:
return newWindscribe(fileManager)
case constants.Surfshark:
return newSurfshark(fileManager)
case constants.Cyberghost:
return newCyberghost(fileManager)
default:
return nil // should never occur
}
}

View File

@@ -0,0 +1,136 @@
package provider
import (
"context"
"fmt"
"net"
"strings"
"github.com/qdm12/golibs/files"
"github.com/qdm12/private-internet-access-docker/internal/constants"
"github.com/qdm12/private-internet-access-docker/internal/models"
)
type surfshark struct {
fileManager files.FileManager
lookupIP func(host string) ([]net.IP, error)
}
func newSurfshark(fileManager files.FileManager) *surfshark {
return &surfshark{fileManager, net.LookupIP}
}
func (s *surfshark) GetOpenVPNConnections(selection models.ServerSelection) (connections []models.OpenVPNConnection, err error) {
var IPs []net.IP
for _, server := range constants.SurfsharkServers() {
if strings.EqualFold(server.Region, selection.Region) {
IPs = server.IPs
}
}
if len(IPs) == 0 {
return nil, fmt.Errorf("no IP found for region %q", selection.Region)
}
if selection.TargetIP != nil {
found := false
for i := range IPs {
if IPs[i].Equal(selection.TargetIP) {
found = true
break
}
}
if !found {
return nil, fmt.Errorf("target IP address %q not found in IP addresses", selection.TargetIP)
}
IPs = []net.IP{selection.TargetIP}
}
var port uint16
switch {
case selection.Protocol == constants.TCP:
port = 1443
case selection.Protocol == constants.UDP:
port = 1194
default:
return nil, fmt.Errorf("protocol %q is unknown", selection.Protocol)
}
for _, IP := range IPs {
connections = append(connections, models.OpenVPNConnection{IP: IP, Port: port, Protocol: selection.Protocol})
}
return connections, nil
}
func (s *surfshark) BuildConf(connections []models.OpenVPNConnection, verbosity, uid, gid int, root bool, cipher, auth string, extras models.ExtraConfigOptions) (err error) {
if len(cipher) == 0 {
cipher = aes256cbc
}
if len(auth) == 0 {
auth = "SHA512"
}
lines := []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
// Surfshark specific
"resolv-retry infinite",
"tun-mtu 1500",
"tun-mtu-extra 32",
"mssfix 1450",
"ping 15",
"ping-restart 0",
"ping-timer-rem",
"reneg-sec 0",
"fast-io",
"key-direction 1",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"auth-retry nointeract",
"remote-random",
"suppress-timestamps",
// Modified variables
fmt.Sprintf("verb %d", verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", string(connections[0].Protocol)),
fmt.Sprintf("cipher %s", cipher),
fmt.Sprintf("auth %s", auth),
}
if !root {
lines = append(lines, "user nonrootuser")
}
for _, connection := range connections {
lines = append(lines, fmt.Sprintf("remote %s %d", connection.IP.String(), connection.Port))
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.SurfsharkCertificate,
"-----END CERTIFICATE-----",
"</ca>",
}...)
lines = append(lines, []string{
"<tls-auth>",
"-----BEGIN OpenVPN Static key V1-----",
constants.SurfsharkOpenvpnStaticKeyV1,
"-----END OpenVPN Static key V1-----",
"</tls-auth>",
"",
}...)
return s.fileManager.WriteLinesToFile(string(constants.OpenVPNConf), lines, files.Ownership(uid, gid), files.Permissions(0400))
}
func (s *surfshark) GetPortForward() (port uint16, err error) {
panic("port forwarding is not supported for surfshark")
}
func (s *surfshark) WritePortForward(filepath models.Filepath, port uint16, uid, gid int) (err error) {
panic("port forwarding is not supported for surfshark")
}
func (s *surfshark) AllowPortForwardFirewall(ctx context.Context, device models.VPNDevice, port uint16) (err error) {
panic("port forwarding is not supported for surfshark")
}

View File

@@ -0,0 +1,133 @@
package provider
import (
"context"
"fmt"
"net"
"strings"
"github.com/qdm12/golibs/files"
"github.com/qdm12/private-internet-access-docker/internal/constants"
"github.com/qdm12/private-internet-access-docker/internal/models"
)
type windscribe struct {
fileManager files.FileManager
}
func newWindscribe(fileManager files.FileManager) *windscribe {
return &windscribe{fileManager: fileManager}
}
func (w *windscribe) GetOpenVPNConnections(selection models.ServerSelection) (connections []models.OpenVPNConnection, err error) {
var IPs []net.IP
for _, server := range constants.WindscribeServers() {
if strings.EqualFold(server.Region, selection.Region) {
IPs = server.IPs
}
}
if len(IPs) == 0 {
return nil, fmt.Errorf("no IP found for region %q", selection.Region)
}
if selection.TargetIP != nil {
found := false
for i := range IPs {
if IPs[i].Equal(selection.TargetIP) {
found = true
break
}
}
if !found {
return nil, fmt.Errorf("target IP address %q not found in IP addresses", selection.TargetIP)
}
IPs = []net.IP{selection.TargetIP}
}
var port uint16
switch {
case selection.CustomPort > 0:
port = selection.CustomPort
case selection.Protocol == constants.TCP:
port = 1194
case selection.Protocol == constants.UDP:
port = 443
default:
return nil, fmt.Errorf("protocol %q is unknown", selection.Protocol)
}
for _, IP := range IPs {
connections = append(connections, models.OpenVPNConnection{IP: IP, Port: port, Protocol: selection.Protocol})
}
return connections, nil
}
func (w *windscribe) BuildConf(connections []models.OpenVPNConnection, verbosity, uid, gid int, root bool, cipher, auth string, extras models.ExtraConfigOptions) (err error) {
if len(cipher) == 0 {
cipher = aes256cbc
}
if len(auth) == 0 {
auth = "sha512"
}
lines := []string{
"client",
"dev tun",
"nobind",
"persist-key",
"remote-cert-tls server",
// Windscribe specific
"resolv-retry infinite",
"comp-lzo",
"key-direction 1",
// Added constant values
"auth-nocache",
"mute-replay-warnings",
"pull-filter ignore \"auth-token\"", // prevent auth failed loops
"auth-retry nointeract",
"remote-random",
"suppress-timestamps",
// Modified variables
fmt.Sprintf("verb %d", verbosity),
fmt.Sprintf("auth-user-pass %s", constants.OpenVPNAuthConf),
fmt.Sprintf("proto %s", string(connections[0].Protocol)),
fmt.Sprintf("cipher %s", cipher),
fmt.Sprintf("auth %s", auth),
}
if strings.HasSuffix(cipher, "-gcm") {
lines = append(lines, "ncp-ciphers AES-256-GCM:AES-256-CBC:AES-128-GCM")
}
if !root {
lines = append(lines, "user nonrootuser")
}
for _, connection := range connections {
lines = append(lines, fmt.Sprintf("remote %s %d", connection.IP.String(), connection.Port))
}
lines = append(lines, []string{
"<ca>",
"-----BEGIN CERTIFICATE-----",
constants.WindscribeCertificate,
"-----END CERTIFICATE-----",
"</ca>",
}...)
lines = append(lines, []string{
"<tls-auth>",
"-----BEGIN OpenVPN Static key V1-----",
constants.WindscribeOpenvpnStaticKeyV1,
"-----END OpenVPN Static key V1-----",
"</tls-auth>",
"",
}...)
return w.fileManager.WriteLinesToFile(string(constants.OpenVPNConf), lines, files.Ownership(uid, gid), files.Permissions(0400))
}
func (w *windscribe) GetPortForward() (port uint16, err error) {
panic("port forwarding is not supported for windscribe")
}
func (w *windscribe) WritePortForward(filepath models.Filepath, port uint16, uid, gid int) (err error) {
panic("port forwarding is not supported for windscribe")
}
func (w *windscribe) AllowPortForwardFirewall(ctx context.Context, device models.VPNDevice, port uint16) (err error) {
panic("port forwarding is not supported for windscribe")
}