Skip to content

feat(cryptostorm): add WireGuard and OpenVPN provider support#3164

Open
mpatton125 wants to merge 24 commits into
passteque:masterfrom
mpatton125:claude/crazy-brahmagupta
Open

feat(cryptostorm): add WireGuard and OpenVPN provider support#3164
mpatton125 wants to merge 24 commits into
passteque:masterfrom
mpatton125:claude/crazy-brahmagupta

Conversation

@mpatton125

@mpatton125 mpatton125 commented Feb 26, 2026

Copy link
Copy Markdown

Description

  • Add cryptostorm.is as a new VPN provider with both WireGuard and OpenVPN support
  • Both protocols use port 443 (TCP/UDP), server data fetched from cryptostorm JSON API
  • Includes port forwarding support via cryptostorm's internal forwarding server

Issue

Fix #3112


🤖 Generated with Claude Code <--- Full disclosure

@qdm12 qdm12 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Big hallucination on the updating servers code. this needs to be fixed somehow, or this PR won't be accepted
  • Port forwarding: I need a human to check the information returned from the port forwarding endpoint when connected
  • When above is done, I need a human to verify openvpn tcp, udp, wireguard and port forwarding all work fine.

// their publicly available node list. If the upstream format changes,
// update the nodeData struct and parsing logic accordingly.
func fetchAPI(ctx context.Context, client *http.Client) (data []nodeData, err error) {
const url = "https://cryptostorm.is/wireguard/nodes.json"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no json API, this url is the same as https://cryptostorm.is/wireguard. This returns html, not json and this will fail

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now scraping endpoint data from https://cryptostorm.is/wireguard.

func (p *Provider) GetConnection(selection settings.ServerSelection, ipv6Supported bool) (
connection models.Connection, err error,
) {
defaults := utils.NewConnectionDefaults(443, 443, 443) //nolint:mnd

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please double check openvpn udp tcp and wireguard all work with those 443 ports.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely both work with 443 - provider says they accept OpenVPN and Wireguard connections on any port (1-65535).

Comment thread internal/provider/cryptostorm/openvpnconf.go Outdated
}
]
},
"cryptostorm": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that was not obtained from the code in the updater, so it most likely pure hallucination; fix the updater and revert these changes.

Comment thread Dockerfile Outdated
// If the port is already forwarded (e.g. from a previous session) it will appear in
// the list regardless of whether the POST succeeded, so we treat that as success.
// Valid port range is 30000–65535.
// See: https://cryptostorm.is/port_forwarding

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

404 not found, possible hallucination? please fix

it's https://cryptostorm.is/portfwd

postBody = "port=" + strconv.FormatUint(uint64(objects.ListeningPort), 10)
}

pfURL := "http://" + cryptostormPFServer + "/fwd"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a comment for IPv6 it's http://[2001:db8::7]/fwd - that will be used later.

postBody = "port=" + strconv.FormatUint(uint64(objects.ListeningPort), 10)
}

pfURL := "http://" + cryptostormPFServer + "/fwd"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mpatton125 can you check what http://10.31.33.7/fwd returns when you are connected to the VPN? To confirm what the AI wrote is correct


const base, bitSize = 10, 16
for _, match := range matches {
portUint64, err := strconv.ParseUint(match[1], base, bitSize)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can there be multiple ports? 🤔

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this provider allows multiple port forwards, per endpoint.

@qdm12 qdm12 Mar 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a maximum limit of ports forwarded??

Let's wait for #3208 to be merged, then this can use the new setting field PortsCount

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rebase on upstream master, which supports multiple ports. What's the number of ports limit?

Comment thread internal/provider/utils/portforward.go Outdated
Comment on lines +28 to +30
// ListeningPort is the port to request from the provider, used by Cryptostorm.
// A value of 0 means let the provider assign a port.
ListeningPort uint16

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mpatton125 please confirm if http://10.31.33.7/fwd tells you you can specify a port field, and if that port field can be set to 0 to let cryptostorm assign you a random port.

If this is the case:

  1. use the VPN_PORT_FORWARDING_LISTENING_PORT value to set this. If this one is not set, try to read it from /gluetun/portforward/cryptostorm.json. If this file does not exist, leave the ListeningPort field to 0 to let cryptostorm decide.
  2. in all cases, persist the resulting port(s) to /gluetun/portforward/cryptostorm.json

And also:

  • add a comment in internal/provider/privateinternetaccess/provider.go next to const jsonPortForwardPath = "/gluetun/piaportforward.json" saying TODO v4: move to /gluetun/portforward/privateinternetaccess.json
  • update the comment for the settings field VPN_PORT_FORWARDING_LISTENING_PORT and make sure it runs a no-op in case the forwarded port is already the value specified in VPN_PORT_FORWARDING_LISTENING_PORT

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes you can specify a port at that URL, from 30000-65535. No, it can't be 0 for a random port - user must specify.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, let's wait for #3208

note for future:

we'll then use VPN_PORT_FORWARDING_LISTENING_PORTS=8000,8001 to specify ports you want. We should then change func (p *PortForwarding) setDefaults() to func (p *PortForwarding) setDefaults(vpnProvider string) and set, if the vpnProvider is cryptostorm only, p.PortsCount = gosettings.DefaultComparable(p.PortsCount, len(p.ListeningPorts))

@qdm12 qdm12 marked this pull request as draft March 7, 2026 05:21
@qdm12 qdm12 force-pushed the master branch 2 times, most recently from 2c06921 to 9a5995f Compare March 16, 2026 13:48
@mpatton125 mpatton125 marked this pull request as ready for review April 15, 2026 00:49
@mpatton125 mpatton125 requested a review from qdm12 April 15, 2026 22:23
Michael Patton and others added 16 commits April 23, 2026 09:57
Add cryptostorm.is as a new VPN provider supporting both WireGuard and
OpenVPN protocols with port forwarding.

OpenVPN uses the ECC cipher suite (AES-256-GCM, tls-crypt, ECDSA CA)
with per-server x509 name verification. Both protocols use port 443
for TCP/UDP. Server data is fetched from the cryptostorm JSON API,
producing dual WireGuard + OpenVPN entries per node.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the hardcoded linux/amd64 default from the BUILDPLATFORM ARG
so Docker BuildKit can set it automatically during multi-platform builds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add the ListeningPort field that Cryptostorm's port forwarding
implementation requires to request a specific port from the provider.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Populate cryptostorm entry with all 39 servers (78 entries total:
WireGuard + OpenVPN per server) resolved from the cryptostorm.is
WireGuard page. Covers 22 countries including Australia, US, UK,
Germany, Canada, Japan, Singapore, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Revert Dockerfile BUILDPLATFORM change (needed for older Docker)
- Revert hallucinated servers.json data
- Remove OpenVPN 2.5 tls-version-min line (2.5 being dropped)
- Rewrite updater to parse HTML from cryptostorm.is/wireguard
  instead of non-existent JSON API, with DNS resolution for IPs
- Fix portforward.go: use local const, fix 404 URL to /portfwd,
  add IPv6 comment, tighten port regex to 30000-65535 range
- Add TODO v4 comment in PIA provider for JSON path migration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… wiring

- Response from http://10.31.33.7/fwd is plain text, not HTML.
  Format: "37.120.234.253:55555 -> 10.10.123.139:55555"
  Update regex to match actual response format.
- Pass ListeningPort into PortForwardObjects so VPN_PORT_FORWARDING_LISTENING_PORT
  actually reaches the provider's PortForward method.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The storage layer requires every registered provider to have an entry
in the hardcoded servers.json, even if empty. The updater will populate
server data at runtime via HTML parsing and DNS resolution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of pre-resolving server IPs, populate servers.json with
metadata only (country, city, hostname, wgpubkey) from
cryptostorm.is/wireguard and resolve hostnames via DNS at connection
time in a custom GetConnection. This adds all 40 servers as both
WireGuard and OpenVPN entries (80 total).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The port forward endpoint returns HTML to Go's HTTP client but plain
text to curl. Parse the forwarded ports from the HTML hidden input
fields (name="delfwd" value="PORT") as a fallback when the plain text
regex doesn't match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cryptostorm requires a port in the 30000-65535 range to be specified.
When VPN_PORT_FORWARDING_LISTENING_PORT is not set (0), generate a
random port instead of sending an empty POST body.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cryptostorm requires the user to specify a port (30000-65535) unlike
other providers where the port is server-assigned. Return a clear error
message instead of silently picking a random port.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Persist forwarded ports to /gluetun/portforward/cryptostorm.json so
they survive container restarts. Read persisted port when
VPN_PORT_FORWARDING_LISTENING_PORT is not set. Skip firewall redirect
when the forwarded port already matches the listening port.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…se ListeningPorts

Update PortForward to satisfy the updated PortForwarder interface (map[uint16]uint16)
and read the requested port from objects.ListeningPorts[0] instead of the removed
objects.ListeningPort field, aligning with master's multi-port port-forwarding refactor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mpatton125 mpatton125 force-pushed the claude/crazy-brahmagupta branch from cd21991 to e79b2df Compare April 23, 2026 00:02
Michael Patton and others added 2 commits April 23, 2026 10:15
…er forwards

The Cryptostorm forwarding server lists all ports currently active for the
session (including stale ones from prior runs). Returning all of them causes
an index-out-of-range panic in onNewPorts when ListeningPorts has fewer
entries than the number of returned ports. Filter to only the requested port.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…er on teardown

- On PortForward: delete any server-side ports that are not the one we requested,
  clearing stale entries from prior sessions before they accumulate
- On KeepPortForward: after context cancellation, send a best-effort delfwd
  request so the port is freed when the container stops cleanly
- Both paths are best-effort (warn but don't fail) since the VPN may be
  partially torn down at teardown time

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@qdm12 qdm12 force-pushed the master branch 3 times, most recently from 40f126b to 44d5104 Compare May 3, 2026 04:29
Comment on lines +42 to +43
// is its default as well. For Cryptostorm, this also specifies the
// port to request for forwarding (must be between 30000 and 65535).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(no action for now) to be updated after #3208 is merged and this branch gets updated with master.

Comment thread internal/portforward/service/start.go Outdated
CanPortForward: s.settings.CanPortForward,
Username: s.settings.Username,
Password: s.settings.Password,
ListeningPort: s.settings.ListeningPort,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(no action for now) to be updated after #3208 is merged - add comment stating this is only used for cryptostorm to request specific ports, for other providers, it's only used to redirect ports

Comment thread internal/portforward/service/start.go Outdated
}

err = s.onNewPorts(ctx, ports)
if s.settings.ListeningPort != 0 && port != s.settings.ListeningPort {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(no action for now) to be updated after #3208 is merged - update to check by index ports[i] != s.settings.ListeningPorts[i] and also we should ensure both slices are sorted

type nodeData struct {
Location string // e.g. "Canada - Montreal", "Austria", "US - Texas - Dallas"
Hostname string // e.g. "austria.cstorm.is"
WgPubKey string // WireGuard public key

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit unneeded

Suggested change
WgPubKey string // WireGuard public key
WgPubKey string

return nil, nil, fmt.Errorf("fetching HTML: %w", err)
}

return parseHTML(rootNode)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return parseHTML(rootNode)
nodes, warnings, err = parseHTML(rootNode)
if err != nil {
return nil, nil, fmt.Errorf("parsing HTML: %w", err)
}
return nodes, warnings, nil

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why t??? change to ip. same below


const base, bitSize = 10, 16
for _, match := range matches {
portUint64, err := strconv.ParseUint(match[1], base, bitSize)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rebase on upstream master, which supports multiple ports. What's the number of ports limit?

common.Fetcher
}

func New(storage common.Storage, randSource rand.Source,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rebase on upstream master, randSource is no longer needed.

client *http.Client, updaterWarner common.Warner,
parallelResolver common.ParallelResolver,
) *Provider {
const jsonPortForwardPath = "/gluetun/portforward/cryptostorm.json"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit rename to portForwardPath

PortsCount uint16
// ListeningPorts are the user-specified ports. For Cryptostorm, ListeningPorts[0]
// is the port to request from the forwarding server (must be 30000–65535).
// A nil or [0] value means fall back to the persisted port.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can there be multiple ports requested?

Michael Patton and others added 6 commits May 6, 2026 13:30
- Remove hardcoded servers.json entries (hallucinated data; updater populates these)
- Remove ErrHTMLTableNotFound sentinel error; inline the message
- Wrap fetchNodes parseHTML call to return a formatted error
- Skip header row with rows[1:] slice instead of index guard
- Pre-allocate seen map and hosts slice with len(hts) capacity
- Warn on empty location in addition to empty hostname
- Use switch/case for the empty-field warning block
- Remove redundant WgPubKey inline comment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The storage layer panics on startup if a registered provider has no entry
in the hardcoded servers.json. Add an empty skeleton so the container boots;
actual servers are populated by running the updater.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fetch and embed the 37 Cryptostorm nodes (WireGuard + OpenVPN entries each)
scraped from https://cryptostorm.is/wireguard. Replaces the empty skeleton
that caused a startup panic when a country/city filter was configured.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Provider)

- Remove TLSCipher from openvpnconf.go (unneeded for OpenVPN 2.6)
- Skip RedirectPort in onNewPorts when sourcePort == destinationPort to
  avoid no-op self-redirects when Cryptostorm returns the requested port
  as both the internal and external port; add comment explaining the
  dual-use of ListeningPorts
- Change setDefaults() to setDefaults(vpnProvider string) and default
  PortsCount to len(ListeningPorts) for Cryptostorm when listening ports
  are explicitly set, so the ports count tracks the user's request
- Pass p.Name to p.PortForwarding.setDefaults(p.Name) in provider.go

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Regenerated by running the Cryptostorm updater against the live
https://cryptostorm.is/wireguard endpoint. Replaces the previously
manually-fetched data with output produced by FetchServers() itself,
confirming the updater code works end-to-end.

74 servers (37 nodes x WireGuard + OpenVPN), timestamp 1778040556.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ipPattern regex to match both IPv4 and bracketed IPv6 addresses
  in plain-text server responses
- Replace single portForwardURL constant with portForwardURLs slice
  covering both http://10.31.33.7/fwd (IPv4) and
  http://[2001:db8::7]/fwd (IPv6)
- Extract registerPort helper that POSTs to a single endpoint and
  handles stale-port cleanup for that endpoint
- Register with both endpoints on PortForward; IPv6 failure is
  non-fatal (logged as warning) so IPv4-only tunnels still work
- Deregister from both endpoints on teardown in KeepPortForward
- Add deletePortFromURL as the URL-parameterised delete primitive;
  keep deletePort as a shim for IPv4-only callers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: cryptostorm.is VPN

2 participants