Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4755336
feat(cryptostorm): add WireGuard and OpenVPN provider support
Feb 26, 2026
2027c96
fix(docker): remove BUILDPLATFORM default for multi-platform builds
Feb 26, 2026
487fe3a
fix(cryptostorm): add ListeningPort field to PortForwardObjects
Feb 26, 2026
89e9a41
fix(cryptostorm): add full server list to embedded servers.json
Feb 26, 2026
2f6333a
fix(cryptostorm): address PR review feedback
Mar 9, 2026
a7fa18a
fix(cryptostorm): fix port forward response parsing and ListeningPort…
Mar 9, 2026
fd9d379
fix(cryptostorm): add empty servers.json entry for cryptostorm
Mar 9, 2026
634985e
feat(cryptostorm): populate servers.json and resolve DNS at connect time
Mar 9, 2026
747c4ec
debug(cryptostorm): log port forward response body
Mar 9, 2026
911c63c
fix(cryptostorm): parse port forwards from both HTML and plain text
Mar 9, 2026
2ddd82f
fix(cryptostorm): generate random port when ListeningPort is 0
Mar 9, 2026
09f5f73
fix(cryptostorm): require VPN_PORT_FORWARDING_LISTENING_PORT to be set
Mar 9, 2026
b40abfa
feat(cryptostorm): add port persistence and no-op redirect
Mar 10, 2026
74fe72b
fix(cryptostorm): create parent directory for port forward data file
Mar 10, 2026
37022d6
fix(cryptostorm): restrict port forward regex to 30000-65535 range
Mar 10, 2026
e79b2df
fix(cryptostorm): adapt PortForward to return map[uint16]uint16 and u…
Apr 23, 2026
80dea18
fix(cryptostorm): only return the requested port, not all active serv…
Apr 23, 2026
2b09046
feat(cryptostorm): delete stale port forwards on connect and deregist…
Apr 23, 2026
00d257f
fix(cryptostorm): address PR review feedback
May 6, 2026
a405faa
fix(cryptostorm): add empty servers.json skeleton entry
May 6, 2026
704e09a
feat(cryptostorm): populate servers.json with live server data
May 6, 2026
88b961c
Address qdm12 review: TLSCipher, self-redirect guard, setDefaults(vpn…
May 6, 2026
c48e18a
feat(cryptostorm): update servers.json from updater output
May 6, 2026
c9f07b2
feat(cryptostorm): add IPv6 support to port forwarding
May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions internal/configuration/settings/portforward.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ type PortForwarding struct {
// forwarded ports. The redirection is disabled if it is the slice [0],
// which is its default as well. If set and not [0], its length must match
// the PortsCount value, such that each forwarded port is redirected to
// the corresponding listening port.
// the corresponding listening port. For Cryptostorm, ListeningPorts[0]
// also specifies the port to request from the forwarding server (30000–65535).
ListeningPorts []uint16 `json:"listening_port"`
// PortsCount is the number of ports to forward. It is optional for ProtonVPN
// and be between 1 and 5. For other providers, it must be set to 1 if port
Expand Down Expand Up @@ -72,6 +73,7 @@ func (p PortForwarding) Validate(vpnProvider string) (err error) {
providerSelected = *p.Provider
}
validProviders := []string{
providers.Cryptostorm,
providers.Perfectprivacy,
providers.PrivateInternetAccess,
providers.Privatevpn,
Expand Down Expand Up @@ -148,14 +150,21 @@ func (p *PortForwarding) OverrideWith(other PortForwarding) {
p.Password = gosettings.OverrideWithComparable(p.Password, other.Password)
}

func (p *PortForwarding) setDefaults() {
func (p *PortForwarding) setDefaults(vpnProvider string) {
p.Enabled = gosettings.DefaultPointer(p.Enabled, false)
p.Provider = gosettings.DefaultPointer(p.Provider, "")
p.Filepath = gosettings.DefaultPointer(p.Filepath, "/tmp/gluetun/forwarded_port")
p.UpCommand = gosettings.DefaultPointer(p.UpCommand, "")
p.DownCommand = gosettings.DefaultPointer(p.DownCommand, "")
p.ListeningPorts = gosettings.DefaultSlice(p.ListeningPorts, []uint16{0}) // disabled
p.PortsCount = gosettings.DefaultComparable(p.PortsCount, 1)
// For Cryptostorm, default PortsCount to the number of listening ports
// specified, since each listening port maps directly to a requested port.
defaultPortsCount := uint16(1)
if vpnProvider == providers.Cryptostorm &&
len(p.ListeningPorts) > 0 && p.ListeningPorts[0] != 0 {
defaultPortsCount = uint16(len(p.ListeningPorts))
}
p.PortsCount = gosettings.DefaultComparable(p.PortsCount, defaultPortsCount)
}

func (p PortForwarding) String() string {
Expand Down
3 changes: 2 additions & 1 deletion internal/configuration/settings/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func (p *Provider) validate(vpnType string, filterChoicesGetter FilterChoicesGet
case vpn.Wireguard:
validNames = []string{
providers.Airvpn,
providers.Cryptostorm,
providers.Custom,
providers.Fastestvpn,
providers.Ivpn,
Expand Down Expand Up @@ -87,7 +88,7 @@ func (p *Provider) overrideWith(other Provider) {

func (p *Provider) setDefaults() {
p.Name = gosettings.DefaultComparable(p.Name, providers.PrivateInternetAccess)
p.PortForwarding.setDefaults()
p.PortForwarding.setDefaults(p.Name)
p.ServerSelection.setDefaults(p.Name, *p.PortForwarding.Enabled)
}

Expand Down
2 changes: 2 additions & 0 deletions internal/constants/providers/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const (
// Custom is the VPN provider name for custom
// VPN configurations.
Airvpn = "airvpn"
Cryptostorm = "cryptostorm"
Custom = "custom"
Cyberghost = "cyberghost"
Example = "example"
Expand Down Expand Up @@ -34,6 +35,7 @@ const (
func All() []string {
return []string{
Airvpn,
Cryptostorm,
Cyberghost,
Expressvpn,
Fastestvpn,
Expand Down
7 changes: 7 additions & 0 deletions internal/portforward/service/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ func (s *Service) Start(ctx context.Context) (runError <-chan error, err error)
Username: s.settings.Username,
Password: s.settings.Password,
PortsCount: s.settings.PortsCount,
// ListeningPorts is only used for Cryptostorm to request specific ports
// from the forwarding server; for other providers, it is only used to
// redirect forwarded ports to different local ports.
ListeningPorts: s.settings.ListeningPorts,
}
internalToExternalPorts, err := s.settings.PortForwarder.PortForward(ctx, obj)
if err != nil {
Expand Down Expand Up @@ -116,6 +120,9 @@ func (s *Service) onNewPorts(ctx context.Context, internalToExternalPorts map[ui
case userRedirectionEnabled: // precedence over auto redirection
sourcePort = externalToInternalPorts[port]
destinationPort = s.settings.ListeningPorts[i]
if sourcePort == destinationPort {
continue // source and destination are the same; no redirect needed
}
case autoRedirectionNeeded:
sourcePort = externalToInternalPorts[port]
destinationPort = port
Expand Down
88 changes: 88 additions & 0 deletions internal/provider/cryptostorm/connection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package cryptostorm

import (
"context"
"fmt"
"math/rand"
"net"
"time"

"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants"
"github.com/qdm12/gluetun/internal/constants/vpn"
"github.com/qdm12/gluetun/internal/models"
)

// GetConnection selects a server matching the given criteria and resolves
// its hostname via DNS at connection time, rather than relying on
// pre-resolved IP addresses in the server list.
func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) (
connection models.Connection, err error,
) {
servers, err := p.storage.FilterServers(p.Name(), selection)
if err != nil {
return connection, fmt.Errorf("filtering servers: %w", err)
}

// Pick a random server from the filtered list.
server := servers[rand.New(p.randSource).Intn(len(servers))] //nolint:gosec

// Resolve the hostname at connection time.
const resolveTimeout = 10 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), resolveTimeout)
defer cancel()

network := "ip4"
if ipv6Supported {
network = "ip"
}
ips, err := net.DefaultResolver.LookupNetIP(ctx, network, server.Hostname)
if err != nil {
return connection, fmt.Errorf("resolving %s: %w", server.Hostname, err)
}
if len(ips) == 0 {
return connection, fmt.Errorf("no IP addresses found for %s", server.Hostname)
}
ip := ips[rand.New(p.randSource).Intn(len(ips))] //nolint:gosec

// Determine protocol.
protocol := constants.UDP
if selection.VPN == vpn.OpenVPN && selection.OpenVPN.Protocol == constants.TCP {
protocol = constants.TCP
}

// Determine port (cryptostorm accepts any port 1-65535, default 443).
const defaultPort uint16 = 443 //nolint:mnd
port := defaultPort
switch selection.VPN {
case vpn.Wireguard:
if custom := *selection.Wireguard.EndpointPort; custom > 0 {
port = custom
}
default: // OpenVPN
if custom := *selection.OpenVPN.CustomPort; custom > 0 {
port = custom
}
}

// Allow explicit endpoint IP override.
switch selection.VPN {
case vpn.OpenVPN:
if t := selection.OpenVPN.EndpointIP; t.IsValid() && !t.IsUnspecified() {
ip = t
}
case vpn.Wireguard:
if t := selection.Wireguard.EndpointIP; t.IsValid() && !t.IsUnspecified() {
ip = t
}
}

return models.Connection{
Type: selection.VPN,
IP: ip,
Port: port,
Protocol: protocol,
Hostname: server.Hostname,
PubKey: server.WgPubKey,
}, nil
}
22 changes: 22 additions & 0 deletions internal/provider/cryptostorm/openvpnconf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package cryptostorm

import (
"github.com/qdm12/gluetun/internal/configuration/settings"
"github.com/qdm12/gluetun/internal/constants/openvpn"
"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/provider/utils"
)

func (p *Provider) OpenVPNConfig(connection models.Connection,
settings settings.OpenVPN, ipv6Supported bool,
) (lines []string) {
providerSettings := utils.OpenVPNProviderSettings{
RemoteCertTLS: true,
AuthUserPass: true,
Ciphers: []string{openvpn.AES256gcm},
VerifyX509Type: "name",
TLSCrypt: "4875d729589689955012a2ee77f180ecb815c4a336c719c11241a058dafaae00806bbc21d5f1abad085341a3fca4b4f93949151c2979b4ee4390e8d9443acb0061d537f1e9157e45f542c3648f56330505f3eaff97ef82ee063b9d88bb9d5aa0060428455b51a2a4fd929d9af4b94adcb0a4acaa14ff62a9b0f4f9f0b3f01e71fc98a6c60e8584f4deb3de793a5a7bc27014c9369f9724bc810ef0d191b3020478eead725b3ae6aaef2e1030a197e417421f159ed54eb2629afcfb337cf9a0025bf1d5c0d820fffb219d0b4214043d2df27ed367b522945a5dadc748e2ca379e3971789dbdf609b3d9bfe866361b28e3c90589baa925157ad833093a5a7bede5", //nolint:lll
CAs: []string{"MIICCzCCAW2gAwIBAgIUMRTTJ6nuPjmSxaRfbw5f+dZ9d/gwCgYIKoZIzj0EAwQwGTEXMBUGA1UEAwwOY3J5cHRvc3Rvcm0gQ0EwHhcNMTgwOTE3MjAwODU4WhcNMzgwOTE3MjAwODU4WjAZMRcwFQYDVQQDDA5jcnlwdG9zdG9ybSBDQTCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEARKu20PBrr226TP6mQQGtzCqQqBKfGaA05Ml5nrGSV6wzBQDQga4/cPepGrE/tpzRX72KSfZD6nJfQLYen7kdc3PAEvWFBhCovq7e4L6xJ5qV5aMf89QjNhJ/xn//dlxE8Z6UfIx63dJX9q3EHNxateU84lDkbCrqckkckcZF4C1a9Ooo1AwTjAdBgNVHQ4EFgQUdaVDaoi48Yf2RugXqJ4yJ4Z4utgwHwYDVR0jBBgwFoAUdaVDaoi48Yf2RugXqJ4yJ4Z4utgwDAYDVR0TBAUwAwEB/zAKBggqhkjOPQQDBAOBiwAwgYcCQVcCw/8OVpNqltDYczqHmX4sMRsZTY0iIzl1rYY/0/ZPIvzjlMFnouHwb8asJZRMBNECq7u9PCbG3jdu6lYtcCm+AkIB3IYYKuXLKW7ucdttNODBqH2Rail+9oBWTV2ZFKVVwELlKadHx9UvAcpAaV1alkN80CgI2tad2/qVdpSIQpfVvTI="}, //nolint:lll
}
return utils.OpenVPNConfig(providerSettings, connection, settings, ipv6Supported)
}
Loading