Skip to content

Instantly share code, notes, and snippets.

@fusetim
Last active October 11, 2024 13:24
Show Gist options
  • Save fusetim/1a1ee1bdf821a45361f346e9c7f41e5a to your computer and use it in GitHub Desktop.
Save fusetim/1a1ee1bdf821a45361f346e9c7f41e5a to your computer and use it in GitHub Desktop.
Generate lots of Wireguard configuration for your ProtonVPN Account.
import http.client
import http.cookies
import json
import base64
import hashlib
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import x25519
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
"""
Copyright - FuseTim 2024
This code is dual-licensed under both the MIT License and the Apache License 2.0.
You may choose either license to govern your use of this code.
MIT License:
https://opensource.org/licenses/MIT
Apache License 2.0:
https://www.apache.org/licenses/LICENSE-2.0
By contributing to this project, you agree that your contributions will be licensed under
both the MIT License and the Apache License 2.0.
"""
######################################################################################
# Credentials (found in Headers and Cookies)
auth_server = "------" # See `x-pm-uid` header
auth_token = "------" # See `AUTH-<x-pm-uid>` cookie
session_id = "------" # See `Session-Id` cookie
web_app_version = "[email protected]" # See `x-pm-appversion` header
# Settings
prefix = "PREFIX" # Prefix is used for config file and name in ProtonVPN Dashboard
output_dir = "./"
selected_countries = ["CH"]
selected_tier = 2 # 0 = Free, 2 = Plus
selected_features = [ ] # Features that a server should have ("P2P", "TOR", "SecureCore", "XOR", etc) or not ("-P2P", etc)
max_servers = 2 # Maximum of generated config
listing_only = False # Do not generate config, just list available servers with previous selectors
config_features = {
"SafeMode": False,
"SplitTCP": True,
"PortForwarding": True,
"RandomNAT": False,
"NetShieldLevel": 0, # 0, 1 or 2
};
######################################################################################
# Contants
connection = http.client.HTTPSConnection("account.protonvpn.com")
C = http.cookies.SimpleCookie()
C["AUTH-"+auth_server] = auth_token
C["Session-Id"] = session_id
headers = {
"x-pm-appversion": web_app_version,
"x-pm-uid": auth_server,
"Accept": "application/vnd.protonmail.v1+json",
"Cookie": C.output(attrs=[],header="", sep="; ")
}
def generateKeys():
"""Generate a client key-pair using the API. Could be generated offline but need more work..."""
print("Generating key-pair...")
connection.request("GET", "/api/vpn/v1/certificate/key/EC", headers=headers)
response = connection.getresponse()
print("Status: {} and reason: {}".format(response.status, response.reason))
resp = json.loads(response.read().decode())
priv = resp["PrivateKey"].split("\n")[1]
pub = resp["PublicKey"].split("\n")[1]
print("Key generated:")
print("priv:", priv)
print("pub:", pub)
return [resp["PrivateKey"], pub, priv]
def getPubPEM(priv):
"""Return the Public key as string without headers"""
return priv[1]
def getPrivPEM(priv):
"""Return the Private key as PKCS#8 without headers"""
return priv[2]
def getPrivx25519(priv):
"""Return the x25519 base64-encoded private key, to be used in Wireguard config."""
hash__ = hashlib.sha512(base64.b64decode(priv[2])[-32:]).digest()
hash_ = list(hash__)[:32]
hash_[0] &= 0xf8
hash_[31] &= 0x7f
hash_[31] |= 0x40
new_priv = base64.b64encode(bytes(hash_)).decode()
return new_priv
def registerConfig(server, priv):
"""Register a Wireguard configuration and return its raw response."""
h = headers.copy()
h["Content-Type"]= "application/json"
print("Registering Config for server", server["Name"],"...")
body = {
"ClientPublicKey": getPubPEM(priv),
"Mode": "persistent",
"DeviceName": prefix+"-"+server["Name"],
"Features": {
"peerName": server["Name"],
"peerIp": server["Servers"][0]["EntryIP"],
"peerPublicKey": server["Servers"][0]["X25519PublicKey"],
"platform": "Windows",
# You can add features there (PortForwarding, SplitTCP, ModerateNAT
# See https://github.com/ProtonMail/WebClients/blob/8b5035d6f848b76d005814fca260bb616e83a4b2/packages/components/containers/vpn/WireGuardConfigurationSection/feature.ts#L53
"SafeMode": config_features["SafeMode"],
"SplitTCP": config_features["SplitTCP"],
"PortForwarding": config_features["PortForwarding"] if server["Features"] & 4 == 4 else False,
"RandomNAT": config_features["RandomNAT"],
"NetShieldLevel": config_features["NetShieldLevel"], # 0, 1 or 2
}
}
connection.request("POST", "/api/vpn/v1/certificate", body=json.dumps(body), headers=h)
response = connection.getresponse()
print("Status: {} and reason: {}".format(response.status, response.reason))
resp = json.loads(response.read().decode())
print(resp)
return resp
def generateConfig(priv, register):
"""Generate a Wireguard config using the ProtonVPN API answer."""
conf = """[Interface]
# Key for {prefix}
PrivateKey = {priv}
Address = 10.2.0.2/32
DNS = 10.2.0.1
[Peer]
# {server_name}
PublicKey = {server_pub}
AllowedIPs = 0.0.0.0/0
Endpoint = {server_endpoint}:51820
""".format(prefix=prefix, priv=getPrivx25519(priv), server_name=register["Features"]["peerName"], server_pub=register["Features"]["peerPublicKey"], server_endpoint=register["Features"]["peerIp"])
return conf
def write_config_to_disk(name, conf):
f = open(output_dir+"/"+name+".conf", "w")
f.write(conf)
f.close()
# VPN Listings
connection.request("GET", "/api/vpn/logicals", headers=headers)
response = connection.getresponse()
print("Status: {} and reason: {}".format(response.status, response.reason))
servers = json.loads(response.read().decode())["LogicalServers"]
for s in servers:
feat = [
"SecureCore" if s["Features"] & 1 == 1 else "-SecureCore",
"TOR" if s["Features"] & 2 == 2 else "-TOR",
"P2P" if s["Features"] & 4 == 4 else "-P2P",
"XOR" if s["Features"] & 8 == 8 else "-XOR",
"IPv6" if s["Features"] & 16 == 16 else "-IPv6"
]
if (not s["EntryCountry"] in selected_countries and not s["ExitCountry"] in selected_countries) or s["Tier"] != selected_tier:
continue
if len(list(filter(lambda sf: not (sf in feat), selected_features))) > 0:
continue
print("- Server", s["Name"])
print(" > ID:", s["ID"])
print(" > EntryCountry:", s["EntryCountry"])
print(" > ExitCountry:", s["ExitCountry"])
print(" > Tier:", s["Tier"])
print(" > Features:")
print(" - SecureCore:", "Y" if s["Features"] & 1 == 1 else "N")
print(" - Tor:", "Y" if s["Features"] & 2 == 2 else "N")
print(" - P2P:", "Y" if s["Features"] & 4 == 4 else "N")
print(" - XOR:", "Y" if s["Features"] & 8 == 8 else "N")
print(" - IPv6:", "Y" if s["Features"] & 16 == 16 else "N")
print(" > Score:", s["Score"])
print(" > Load:", s["Load"])
print(" > Status:", s["Status"])
print(" > Instance:")
for i in s["Servers"]:
print(" - Instance n°",i["Label"],":", i["ID"])
print(" > EntryIP:", i["EntryIP"])
print(" > ExitIP:", i["ExitIP"])
print(" > Domain:", i["Domain"])
print(" > X25519PublicKey:", i["X25519PublicKey"])
if not listing_only:
keys = generateKeys()
reg = registerConfig(s, keys)
config = generateConfig(keys, reg)
write_config_to_disk(reg["DeviceName"], config)
max_servers-=1
if (max_servers <= 0):
break
connection.close()
@fusetim
Copy link
Author

fusetim commented Nov 11, 2022

Revision 2, config generator has been successfully working up to this day.
These configs, used with a working NAT-PMP client can allow Port Forwarding! Up to this day, there is no issue to use port-forwarding with ProtonVPN.

Note:

  • natpmpc is not working due to some incompatibilities with ProtonVPN NAT-PMP implementation.
  • py-natpmp (cf Github) is a working client (but not maintained).
  • Deluge and QBittorrent internal NAT-PMP clients can work sometimes : opening port in both UDP and TCP is not supported by ProtonVPN NAT-PMP implementation, therefore sometimes it is just closing one and the other repeatedly to accept the other protocol.
  • Qbittorrent & NATPMP: Docker Mod, docker compose script
  • ProtonVPN headers (Auth) can be found on the page: https://account.proton.me/api/core/v4/events/latest

@dlecan
Copy link

dlecan commented Dec 15, 2022

Do you know a working bittorrent client with this configuration?

Can you explain how to use this script ?
Before each connection? Once an for all?

Do you know if such an algorithm could work for OpenVPN as well?

@fusetim
Copy link
Author

fusetim commented Dec 16, 2022

Do you know a working bittorrent client with this configuration?

Can you explain how to use this script ? Before each connection? Once an for all?

Do you know if such an algorithm could work for OpenVPN as well?

  1. I am not aware of bittorrent client that works entirely from scratch with this.
  2. The script should be run one time and then you just have to use the generated config. These configs expire after one year, so you might need to run this script again or renew the config using the official ProtonVPN dashboard.
  3. I'm not aware of that. But clearly Wireguard is more easy to make this work. Someone interested by a similar OpenVPN config generator might found the useful information from official client source code.

@fusetim
Copy link
Author

fusetim commented Dec 18, 2022

Note: NAT-PMP (Port Forwarding) configs are now availables in the official ProtonVPN dashboard!
Weirdly the option is available for free tier accounts too.

@pvanryn
Copy link

pvanryn commented Jan 2, 2023

I've been reading your posts on reddit. So if I create a wireguard config with port forwarding enabled, how do I know which port is open (Linux) ? If I do sudo wg-quick up wg0, I get:

interface: wg0
public key: redacted
private key: (hidden)
listening port: 44781
fwmark: 0xca6c

Listening port does not seem to correspond to port forward.

@fusetim
Copy link
Author

fusetim commented Jan 3, 2023 via email

@JackRoublard
Copy link

Note: NAT-PMP (Port Forwarding) configs are now availables in the official ProtonVPN dashboard! Weirdly the option is available for free tier accounts too.

Hi, Thanks for your work ! I've stumbled here thanks to reddit while also trying to setup port forwarding with proton wireguard/openvpn. Do you reckon that the feature has (mistakenly?) been made available to free-tier users ? Given that the official website states it's a windows-only/paid-only feature (which is at least half false/outdated), I have a faint hope that it is the case.

However, I've been trying both [wg-quick + official proton config files] and [networkmanager openvpn "+pmp" in the username field] techniques but my natpmp clients keep saying the gateway is not compatible. So far I have tried :

(The third one actually doesn't say the gateway is incompatible but I'm not sure I'm using it correctly, hence my question.)

Is there a surefire way to find out which port has supposedly been made available by proton upon connection ? So far every natpmp client i've tried just asks for an input port, which I don't have (although, like @pvanryn, I initially thought that it was the listening port given by wg-quick upon connection, since it changes with every connection and seems randomized as it should. However, external port-checkers keep saying it's closed so I'm guessing this is probably wrong.)

@fusetim
Copy link
Author

fusetim commented Jan 17, 2023

Note: NAT-PMP (Port Forwarding) configs are now availables in the official ProtonVPN dashboard! Weirdly the option is available for free tier accounts too.

Hi, Thanks for your work ! I've stumbled here thanks to reddit while also trying to setup port forwarding with proton wireguard/openvpn. Do you reckon that the feature has (mistakenly?) been made available to free-tier users ? Given that the official website states it's a windows-only/paid-only feature (which is at least half false/outdated), I have a faint hope that it is the case.

However, I've been trying both [wg-quick + official proton config files] and [networkmanager openvpn "+pmp" in the username field] techniques but my natpmp clients keep saying the gateway is not compatible. So far I have tried :

* natpmpc

* py-natpmp

* nmap --script=nat-pmp-info (https://nmap.org/nsedoc/scripts/nat-pmp-info.html)

(The third one actually doesn't say the gateway is incompatible but I'm not sure I'm using it correctly, hence my question.)

Is there a surefire way to find out which port has supposedly been made available by proton upon connection ? So far every natpmp client i've tried just asks for an input port, which I don't have (although, like @pvanryn, I initially thought that it was the listening port given by wg-quick upon connection, since it changes with every connection and seems randomized as it should. However, external port-checkers keep saying it's closed so I'm guessing this is probably wrong.)

The feature is available when generating a wireguard config, nonetheless, in theory to really use NAT-PMP you need to be connected to a P2P servers thus the free servers cannot be used for Port forwarding.
I don't think this will change in the future.

The Nmap script only provide part of the NATPMP protocol which features a way to discover the external IP of the server, but this script can not reserve a port for you. But if it displayed the correct address (a ProtonVPN one) then you should be able to request a port mapping.

Note that you might need to specify the gateway address (most likely 10.2.0.1 -- see the wireguard config, and replace the last digit of the address field by 1) as it might try to get a port from your own router (which might provide this feature).

The input port that needed is in reality not important because ProtonVPN use a special NATPMP gateway. Nonetheless, using 0 as input port will ask for a random port to be forwarded.

Finally yes, the mapped port is completely different from your wireguard listening port.

@JackRoublard
Copy link

JackRoublard commented Jan 20, 2023

Thank you for the detailed reply. You're probably right about them having distinct features implemented on different types of server.

The Nmap script only provide part of the NATPMP protocol which features a way to discover the external IP of the server, but this script can not reserve a port for you. But if it displayed the correct address (a ProtonVPN one) then you should be able to request a port mapping.

I am aware that the nmap script does not do the actual port mapping, as there is another one for that purpose. However, interestingly enough after fiddling with the nmap command parameters I managed to get the following listing :

PORT     STATE  SERVICE
53/tcp       open     domain
4443/tcp   open     pharos
4446/tcp   open     n1-fwp 

Guessing "fwp" could stand for "forwardport, I tried to actually map the port in question :
sudo nmap -p <listening_port> --script=nat-pmp-mapport 10.2.0.1 --script-args='op=map,pubport=4446,privport=25565,protocol=tcp'
But to no avail.

@quantum77
Copy link

quantum77 commented Feb 23, 2023

# python /home/bill/bin/natpmp_client.py -g 10.2.0.1 -u -l 60 34741 34741
PortMapResponse: version 0, opcode 129 (129), result 0, ssec 488668, private_port 34741, public port 46912, lifetime 60
# python /home/bill/bin/natpmp_client.py -g 10.2.0.1 -u -l 60 57342 57342
PortMapResponse: version 0, opcode 129 (129), result 0, ssec 488136, private_port 57342, public port 38508, lifetime 60
# python /home/bill/bin/natpmp_client.py -g 10.2.0.1 -u -l 60 34741 34741
PortMapResponse: version 0, opcode 129 (129), result 0, ssec 488668, private_port 34741, public port 46912, lifetime 60

I can't get a fixed outside port. How can anyone contact me if the outside port is always changing? Same problem with natpmpc.

Is there a NATPMP client which actually woks?

@fusetim
Copy link
Author

fusetim commented Feb 23, 2023

It seems it worked as expected, the port you got were 46912 (for 1/3) and 38508. But please note that you cannot choose the port you get and that you should renew the port mapping at least every 60s (using the port you got previously).
Actually the first time you want a port, you should try to use port 0 (as internal and external) and then renew the port with the given one.

Also checkout this new documentation : https://protonvpn.com/support/port-forwarding-manual-setup/

@pvanryn
Copy link

pvanryn commented Feb 23, 2023

I can't get a fixed outside port. How can anyone contact me if the outside port is always changing? Same problem with natpmpc.

See script here

[OP Edit]: This script suffers from the same limitations.

@thibaultmol
Copy link

I suggest putting in the readme that you can find those headers in the request with the url 'https://account.proton.me/api/core/v4/events/latest'

@Lenni-builder
Copy link

Please add a license so people can legally fork it. I think adding it as a comment to the code is the best way to do it for a script like this.
(Without a license it's under copyright, so it's essentially like proprietary software but with the source code you aren't allowed to do much with being public)

@fusetim
Copy link
Author

fusetim commented Jun 15, 2024

Please add a license so people can legally fork it. I think adding it as a comment to the code is the best way to do it for a script like this. (Without a license it's under copyright, so it's essentially like proprietary software but with the source code you aren't allowed to do much with being public)

Your wishes have been granted, this code is officially licensed under MIT & Apache 2.0, even though I never intended to particularly protect this piece of code. I hope this can help you ;)

@tiimk
Copy link

tiimk commented Jun 30, 2024

I have made some small modifications to this and figure I would share back here. I added in the ability to choose local cities as well. So instead of generating a batch of US servers I can generate US-NY and US-VA for example. I also added in a sleep timer since I was running in to issues of generating too many configs.

As well I added in a simple extend argument to renew any configs to help futureproof. python script.py -extend will do as such. Same 1 minute sleep on that.

As well there is an additional script for anyone who would like to work as a possible "auto" connect for best server available from proton using wireguard. I would recommend importing all the configs to wireguard just to be able to control it from the app if needed. but not required.

https://gist.github.com/tiimk/56e88a6e5d47157dedf40e2761683cf1

If using the connect script you can run it and it will choose the best, run it again and it will choose the second best if already connected to the best. or run python connectscript.py -location=US-NY to only connect to the best server in US-NY. This will only connect to servers you have configs made for so may need some time to generate enough configs to get a wide variety.

@fusetim
Copy link
Author

fusetim commented Jun 30, 2024

👍 Cool additions, thank you for sharing this with everyone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment