From 09c47838f18a9166e4dfd32ee1d35af1de922a67 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Fri, 22 May 2026 01:12:24 +0000 Subject: [PATCH] SERVER_DEDICATED option --- Dockerfile | 2 ++ go.mod | 2 +- go.sum | 4 +-- .../configuration/settings/serverselection.go | 18 ++++++++++++ internal/models/server.go | 1 + internal/provider/ovpn/updater/api.go | 18 +++++++----- internal/provider/ovpn/updater/api_test.go | 1 + internal/provider/ovpn/updater/servers.go | 13 ++++++++- .../provider/ovpn/updater/servers_test.go | 28 +++++++++++++++++-- internal/storage/filter.go | 6 ++++ 10 files changed, 80 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index f4aeee10..3307cdde 100644 --- a/Dockerfile +++ b/Dockerfile @@ -192,6 +192,8 @@ ENV VPN_SERVICE_PROVIDER=pia \ PREMIUM_ONLY= \ # # PIA and ProtonVPN only: PORT_FORWARD_ONLY= \ + # # Ovpn only: + SERVER_DEDICATED=no \ # Firewall FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on \ FIREWALL_VPN_INPUT_PORTS= \ diff --git a/go.mod b/go.mod index f5d7ef78..d39554b5 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/mdlayher/netlink v1.9.0 github.com/pelletier/go-toml/v2 v2.2.4 github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a - github.com/qdm12/gluetun-servers v0.1.1-0.20260521235724-060a16d4b34c + github.com/qdm12/gluetun-servers v0.1.1-0.20260522005421-14277e92ce82 github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 github.com/qdm12/gosettings v0.4.4 github.com/qdm12/goshutdown v0.3.0 diff --git a/go.sum b/go.sum index 80034656..0480b4bc 100644 --- a/go.sum +++ b/go.sum @@ -76,8 +76,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a h1:TE157yPQmAbVruH0MWCQzs0vTT/6t96DkoWUXd6PVuc= github.com/qdm12/dns/v2 v2.0.0-rc9.0.20260421173011-9de8e7fdbe3a/go.mod h1:98foWgXJZ+g8gJIuO+fdO+oWpFei5WShMFTeN4Im2lE= -github.com/qdm12/gluetun-servers v0.1.1-0.20260521235724-060a16d4b34c h1:rPgmhMt9uOq6pdPr8k0sXUnZ0fjTBw+yUOaIl0ttFu4= -github.com/qdm12/gluetun-servers v0.1.1-0.20260521235724-060a16d4b34c/go.mod h1:acttuyHyoFDu6GTbf3kAV+QXeiX8oJeh0MBic67/9z8= +github.com/qdm12/gluetun-servers v0.1.1-0.20260522005421-14277e92ce82 h1:tE44IEW7o9yPQaO8HBeoO9RxtTTxqhboIypegrQlVt8= +github.com/qdm12/gluetun-servers v0.1.1-0.20260522005421-14277e92ce82/go.mod h1:acttuyHyoFDu6GTbf3kAV+QXeiX8oJeh0MBic67/9z8= github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978 h1:TRGpCU1l0lNwtogEUSs5U+RFceYxkAJUmrGabno7J5c= github.com/qdm12/goservices v0.1.1-0.20251104135713-6bee97bd4978/go.mod h1:D1Po4CRQLYjccnAR2JsVlN1sBMgQrcNLONbvyuzcdTg= github.com/qdm12/gosettings v0.4.4 h1:SM6tOZDf6k8qbjWU8KWyBF4mWIixfsKCfh9DGRLHlj4= diff --git a/internal/configuration/settings/serverselection.go b/internal/configuration/settings/serverselection.go index c3e6a6d6..f3321b30 100644 --- a/internal/configuration/settings/serverselection.go +++ b/internal/configuration/settings/serverselection.go @@ -63,6 +63,9 @@ type ServerSelection struct { // TorOnly is true if VPN servers without tor should // be filtered. This is used with ProtonVPN. TorOnly *bool `json:"tor_only"` + // Dedicated is true if dedicated VPN servers should be chosen only. + // This is used with OVPN. + Dedicated *bool `json:"dedicated"` // OpenVPN contains settings to select OpenVPN servers // and the final connection. OpenVPN OpenVPNSelection `json:"openvpn"` @@ -272,6 +275,8 @@ func validateFeatureFilters(settings ServerSelection, vpnServiceProvider string) return errors.New("secure core only filter is not supported") case *settings.TorOnly && vpnServiceProvider != providers.Protonvpn: return errors.New("tor only filter is not supported") + case *settings.Dedicated && vpnServiceProvider != providers.Ovpn: + return errors.New("dedicated filter is not supported") default: return nil } @@ -296,6 +301,7 @@ func (ss *ServerSelection) copy() (copied ServerSelection) { TorOnly: gosettings.CopyPointer(ss.TorOnly), PortForwardOnly: gosettings.CopyPointer(ss.PortForwardOnly), MultiHopOnly: gosettings.CopyPointer(ss.MultiHopOnly), + Dedicated: gosettings.CopyPointer(ss.Dedicated), OpenVPN: ss.OpenVPN.copy(), Wireguard: ss.Wireguard.copy(), } @@ -319,6 +325,7 @@ func (ss *ServerSelection) overrideWith(other ServerSelection) { ss.TorOnly = gosettings.OverrideWithPointer(ss.TorOnly, other.TorOnly) ss.MultiHopOnly = gosettings.OverrideWithPointer(ss.MultiHopOnly, other.MultiHopOnly) ss.PortForwardOnly = gosettings.OverrideWithPointer(ss.PortForwardOnly, other.PortForwardOnly) + ss.Dedicated = gosettings.OverrideWithPointer(ss.Dedicated, other.Dedicated) ss.OpenVPN.overrideWith(other.OpenVPN) ss.Wireguard.overrideWith(other.Wireguard) } @@ -335,6 +342,7 @@ func (ss *ServerSelection) setDefaults(vpnProvider string, portForwardingEnabled defaultPortForwardOnly := portForwardingEnabled && helpers.IsOneOf(vpnProvider, providers.PrivateInternetAccess, providers.Protonvpn) ss.PortForwardOnly = gosettings.DefaultPointer(ss.PortForwardOnly, defaultPortForwardOnly) + ss.Dedicated = gosettings.DefaultPointer(ss.Dedicated, false) ss.OpenVPN.setDefaults(vpnProvider) ss.Wireguard.setDefaults() } @@ -410,6 +418,10 @@ func (ss ServerSelection) toLinesNode() (node *gotree.Node) { node.Appendf("Multi-hop only servers: yes") } + if *ss.Dedicated { + node.Appendf("Dedicated servers: yes") + } + if *ss.PortForwardOnly { node.Appendf("Port forwarding only servers: yes") } @@ -501,6 +513,12 @@ func (ss *ServerSelection) read(r *reader.Reader, return err } + // Ovpn only + ss.Dedicated, err = r.BoolPtr("SERVER_DEDICATED") + if err != nil { + return err + } + err = ss.OpenVPN.read(r) if err != nil { return err diff --git a/internal/models/server.go b/internal/models/server.go index 2eb190cc..33656e21 100644 --- a/internal/models/server.go +++ b/internal/models/server.go @@ -34,6 +34,7 @@ type Server struct { SecureCore bool `json:"secure_core,omitempty"` Tor bool `json:"tor,omitempty"` PortForward bool `json:"port_forward,omitempty"` + Dedicated bool `json:"dedicated,omitempty"` Keep bool `json:"keep,omitempty"` IPs []netip.Addr `json:"ips,omitempty"` PortsTCP []uint16 `json:"ports_tcp,omitempty"` diff --git a/internal/provider/ovpn/updater/api.go b/internal/provider/ovpn/updater/api.go index 9a00287e..27d3d661 100644 --- a/internal/provider/ovpn/updater/api.go +++ b/internal/provider/ovpn/updater/api.go @@ -22,13 +22,16 @@ type apiDataCenter struct { } type apiServer struct { - IP netip.Addr `json:"ip"` - Ptr string `json:"ptr"` // hostname - Online bool `json:"online"` - PublicKey string `json:"public_key"` - WireguardPorts []uint16 `json:"wireguard_ports"` - MultiHopOpenvpnPort uint16 `json:"multihop_openvpn_port"` - MultiHopWireguardPort uint16 `json:"multihop_wireguard_port"` + IP netip.Addr `json:"ip"` + Ptr string `json:"ptr"` // hostname + Online bool `json:"online"` + // PublicKey is for the Standard Shared Entry Point + PublicKey string `json:"public_key"` + // PublicKeyIPv4 is for the Public / Dedicated IP Entry Point + PublicKeyIPv4 string `json:"public_key_ipv4"` + WireguardPorts []uint16 `json:"wireguard_ports"` + MultiHopOpenvpnPort uint16 `json:"multihop_openvpn_port"` + MultiHopWireguardPort uint16 `json:"multihop_wireguard_port"` } func fetchAPI(ctx context.Context, client *http.Client) ( @@ -106,6 +109,7 @@ func (a *apiServer) validate() (err error) { {err: "ip address is not set", condition: !a.IP.IsValid()}, {err: "hostname field is not set", condition: a.Ptr == ""}, {err: "public key field is not set", condition: a.PublicKey == ""}, + {err: "public key IPv4 field is not set", condition: a.PublicKeyIPv4 == ""}, {err: "wireguard ports array is not set", condition: len(a.WireguardPorts) == 0}, { err: "wireguard port is not the default 9929", diff --git a/internal/provider/ovpn/updater/api_test.go b/internal/provider/ovpn/updater/api_test.go index 4d7619bf..7a92afcf 100644 --- a/internal/provider/ovpn/updater/api_test.go +++ b/internal/provider/ovpn/updater/api_test.go @@ -76,6 +76,7 @@ func Test_fetchAPI(t *testing.T) { Ptr: "vpn44.prd.vienna.ovpn.com", Online: true, PublicKey: "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=", + PublicKeyIPv4: "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=", WireguardPorts: []uint16{9929}, MultiHopOpenvpnPort: 20044, MultiHopWireguardPort: 30044, diff --git a/internal/provider/ovpn/updater/servers.go b/internal/provider/ovpn/updater/servers.go index 9815975b..f7e16d2d 100644 --- a/internal/provider/ovpn/updater/servers.go +++ b/internal/provider/ovpn/updater/servers.go @@ -56,7 +56,18 @@ func (u *Updater) FetchServers(ctx context.Context, minServers int) ( multiHopWireguardServer := wireguardServer multiHopWireguardServer.MultiHop = true multiHopWireguardServer.PortsUDP = []uint16{apiServer.MultiHopWireguardPort} - servers = append(servers, wireguardServer, multiHopWireguardServer) + dedicatedWireguardServer := wireguardServer + dedicatedWireguardServer.WgPubKey = apiServer.PublicKeyIPv4 + dedicatedWireguardServer.Dedicated = true + dedicatedMultiHopWireguardServer := multiHopWireguardServer + dedicatedMultiHopWireguardServer.WgPubKey = apiServer.PublicKeyIPv4 + dedicatedMultiHopWireguardServer.Dedicated = true + servers = append(servers, + wireguardServer, + multiHopWireguardServer, + dedicatedWireguardServer, + dedicatedMultiHopWireguardServer, + ) } } diff --git a/internal/provider/ovpn/updater/servers_test.go b/internal/provider/ovpn/updater/servers_test.go index eda9d60a..20b1de95 100644 --- a/internal/provider/ovpn/updater/servers_test.go +++ b/internal/provider/ovpn/updater/servers_test.go @@ -55,7 +55,7 @@ func Test_Updater_FetchServers(t *testing.T) { errMessage: "validating data center 1 of 1: data center Vienna: country name is not set", }, "not_enough_servers": { - minServers: 5, + minServers: 7, responseStatus: http.StatusOK, responseBody: `{ "success": true, @@ -69,6 +69,7 @@ func Test_Updater_FetchServers(t *testing.T) { "ptr": "vpn44.prd.vienna.ovpn.com", "online": true, "public_key": "r83LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=", + "public_key_ipv4": "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=", "wireguard_ports": [9929], "multihop_openvpn_port": 20044, "multihop_wireguard_port": 30044 @@ -78,7 +79,9 @@ func Test_Updater_FetchServers(t *testing.T) { ] }`, errWrapped: common.ErrNotEnoughServers, - errMessage: "not enough servers found: 4 and expected at least 5", + // Wireguard + dedicated Wireguard + Wireguard multi-hop + + // dedicated Wireguard multi-hop + OpenVPN + OpenVPN multi-hop + errMessage: "not enough servers found: 6 and expected at least 7", }, "success": { minServers: 4, @@ -114,6 +117,7 @@ func Test_Updater_FetchServers(t *testing.T) { "ptr": "vpn45.prd.vienna.ovpn.com", "online": false, "public_key": "r93LIc0Q2F8s3dY9x5y17Yz8wTADJc7giW1t5eSmoXc=", + "public_key_ipv4": "wGbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=", "wireguard_ports": [9929], "multihop_openvpn_port": 20045, "multihop_wireguard_port": 30045 @@ -163,6 +167,26 @@ func Test_Updater_FetchServers(t *testing.T) { MultiHop: true, PortsUDP: []uint16{30044}, }, + { + Country: "Austria", + City: "Vienna", + Hostname: "vpn44.prd.vienna.ovpn.com", + IPs: []netip.Addr{netip.MustParseAddr("37.120.212.227")}, + VPN: vpn.Wireguard, + WgPubKey: "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=", + Dedicated: true, + }, + { + Country: "Austria", + City: "Vienna", + Hostname: "vpn44.prd.vienna.ovpn.com", + IPs: []netip.Addr{netip.MustParseAddr("37.120.212.227")}, + VPN: vpn.Wireguard, + WgPubKey: "wFbSRyjSXBmkjJodlqz7DoYn3WNDPYFUIXyIUS2QU2A=", + MultiHop: true, + Dedicated: true, + PortsUDP: []uint16{30044}, + }, }, }, } diff --git a/internal/storage/filter.go b/internal/storage/filter.go index c56d0ece..00a99c98 100644 --- a/internal/storage/filter.go +++ b/internal/storage/filter.go @@ -49,6 +49,7 @@ func (s *Storage) FilterServers(provider string, selection settings.ServerSelect return servers, nil } +//nolint:gocognit,gocyclo func filterServer(server models.Server, selection settings.ServerSelection, ) (filtered bool) { @@ -91,6 +92,11 @@ func filterServer(server models.Server, return true } + if (*selection.Dedicated && !server.Dedicated) || + (!*selection.Dedicated && server.Dedicated) { + return false + } + if filterByPossibilities(server.Country, selection.Countries) { return true }