Skip to content

Creating config file for Amnezia WG app from Amenzia VPN 'vpn://...' string #1407

Description

@astafan8

Since Amnezia VPN iOS app is blocked on Russian App Store, while Amnezia WG somehow isn't (or I just managed to have it installed on my phone before it got blocked) I decided to try and make Amnezia WG app work. Unfortunately, Amnezia WG app does not take Amnezia VPN configuration, neither QR code, nor a .vpn file with vpn://... string. So I decided to figure out how to turn those into a file that Amenzia WG would take. Thanks to the open source code of Amenzia, I was able to find my way, and below I offer you all a python script that does the conversion. With this script, given a .vpn file from someone from runs an Amnezia server, one can turn it into a .conf file that Amnezia WG app can load, making the configuration and setup very easy.

If I am doing something useful here (and not stupid), I would love to contribute this script somewhere where others can make use of this - so let me know what's the best way to do this.

"""
Converts Amnezia VPN 'vpn://...' configuration to WireGuard
configuration file that Amnezia WG app can load.

Execute like this in a terminal:

>>> python -m amnezia_vpn_to_wg_config.py amnezia_config.vpn
"""

import argparse
import base64
import configparser
import io
import json
from pathlib import Path
import zlib


def from_base64_urlsafe(s: str) -> bytes:
    # Link to StackOverflow answer on how to decode Base64 URL-Safe
    # https://stackoverflow.com/questions/3302946/how-to-decode-base64-url-in-python/9956217#9956217
    return base64.urlsafe_b64decode(s + "=" * (4 - len(s) % 4))


def zlib_decompress_from_qcompress(s: bytes) -> bytes:
    # Link to documentation of qUncompress Qt function
    # https://doc.qt.io/qt-6/qbytearray.html#qUncompress
    # https://doc.qt.io/qtforpython-6/PySide6/QtCore/QtCore_globals.html#PySide6.QtCore.qUncompress
    # Link to an answer that refers to qUncompress function docs and explains
    # that the first 4 bytes need to be skipped if one is uncompressing the data
    # outside of Qt functions
    # https://forum.qt.io/topic/123304/uncompressing-data-python-which-is-compressed-using-qt-quncompress-function/2
    # https://forum.qt.io/post/641554
    s_without_first_four_bytes = s[4:]
    return zlib.decompress(s_without_first_four_bytes)


def extract_data_from_vpn_string(vpn_string: str) -> str:
    # Link to “vpn://...” string decompression code in Amnesia GitHub repository
    # https://github.com/amnezia-vpn/amnezia-client/blob/703b9137e0e903b5b9e8c2de2c123ba98195a859/client/ui/controllers/importController.cpp#L153-L154
    encoded_data = vpn_string.strip("vpn://")
    compressed_data = from_base64_urlsafe(encoded_data)
    decompressed_bytes = zlib_decompress_from_qcompress(compressed_data)
    decompressed_data = decompressed_bytes.decode()
    return decompressed_data


def make_wireguard_config_from_amnezia_config(amnezia_config_json: str) -> str:
    amnezia_config = json.loads(amnezia_config_json)

    last_config_json = amnezia_config["containers"][-1]["awg"]["last_config"]
    last_config = json.loads(last_config_json)
    wireguard_config = last_config["config"]

    # WireGuard config is INI formatted

    # Below we are going to add a few modifications to make it
    # look exactly as Amnezia WG app creates it upon export

    wireguard_config = wireguard_config.replace("$PRIMARY_DNS", amnezia_config["dns1"])
    wireguard_config = wireguard_config.replace(
        "$SECONDARY_DNS", amnezia_config["dns2"]
    )

    config = configparser.ConfigParser()
    # Avoid normalizing section names to lowercase by changing
    # ``optionxform`` as documented here
    # https://docs.python.org/3/library/configparser.html#configparser.ConfigParser.optionxform
    config.optionxform = lambda optionstr: optionstr
    config.read_string(wireguard_config)

    config["Interface"]["MTU"] = last_config["mtu"]
    config["Interface"]["ListenPort"] = f"{last_config['port']}"

    with io.StringIO() as final_wireguard_config_string:
        config.write(final_wireguard_config_string)
        final_wireguard_config = final_wireguard_config_string.getvalue()

    return final_wireguard_config


def run(vpn_file: Path) -> Path:
    vpn_file_content = vpn_file.read_text()

    amnezia_config_json = extract_data_from_vpn_string(vpn_file_content)
    wireguard_config = make_wireguard_config_from_amnezia_config(amnezia_config_json)

    wireguard_config_file = vpn_file.with_suffix(".conf")
    wireguard_config_file.write_text(wireguard_config)
    return wireguard_config_file


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description=(
            "Converter from Amnezia VPN 'vpn://...' file to "
            "Amnezia WG (WireGuard) configuration '.conf' file"
        ),
        add_help=True,
    )
    parser.add_argument(
        "vpn_file",
        type=Path,
        help="Amnezia VPN file (usually '.vpn') with 'vpn://...' string",
    )

    args = parser.parse_args()

    resulting_file = run(
        vpn_file=args.vpn_file,
    )
    print(f"Wrote Amnezia WG config to: {resulting_file}")

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions