Command-line version of Happwner. A Happ and V2RayTun subscription-link decryptor/encryptor that can fetch, decrypt, encrypt, and convert proxy profiles. It is available as both a CLI tool and a Rust library, built on a from-scratch reimplementation of the Happ and V2RayTun crypto.
Developed by slavrom21 & Omegaplex
- Decrypt
happ://crypt*/links. Supported formats:crypt5,crypt4,crypt3,crypt2,crypt - Decrypt
v2raytun://crypt/links. Supported keys:crypt3,crypt4,key3 - Encrypt plaintext back into any of those formats
- Resolve wrapper/redirect links:
happ://add/,v2raytun://import/,incy://add/,incy://import/, andhapp://orv2raytun://links embedded inhttp(s)URLs - Fetch subscription profiles over HTTP (custom User-Agent / X-HWID) and automatically decrypt the AES-128-GCM body layer (full build only, see Variants)
- Convert Xray configs into
vless/vmess/trojan/hy2/ss/tuiclinks, or into a merged sing-box config
hpwnr builds in two flavors, selected with a Cargo feature:
- full (default) — everything above, including HTTP fetch and AES-128-GCM response decryption. Pulls in a TLS stack (
ureq+ring). - lite (
--no-default-features) — crypt/decrypt and convert only, with the whole network stack removed: no fetch, no AES response layer, no User-Agent/HWID handling. It has no C dependencies, which makes it roughly a third of the size (~0.8 MB vs ~2.1 MB) and portable to far more targets.
cargo build --release # full
cargo build --release --no-default-features # liteThe fetch feature is what carries the network stack; default = ["fetch"], so dropping default features gives the lite build.
hpwnr <link> <arguments>
Use h or help to show the help page. The argument order and leading hyphens do not matter, so h, -h, --help all work.
https://…- a regular HTTPS URLhapp://crypt…- an encrypted Happ linkv2raytun://crypt/…- an encrypted V2RayTun linkhapp://add…,v2raytun://import…,incy://add…,incy://import…- the Happ/V2RayTun/INCY "open-in" deeplinkshttps://somesite.com/happ%3A%2F%2Fcrypt…- an encrypted Happ link embedded in an HTTPS URLhttps://somesite.com/mysub?key=key[01-10]- a URL with akeyparameter. In fetch mode, if the server returns anEncrypt-Tagheader, the response body is decrypted automatically (AES-128-GCM) with that key
No arguments needed. Use hpwnr <crypt link> to decrypt.
crypt/crypt[2-5]- encrypt the given text into the matchinghapp://crypt*format.crypt5is the default. Works only with decrypted textcrypt5old/crypt5legacy- encrypt with the legacycrypt5layout; plaincrypt5uses the new salted+XOR layout. Both decrypt automaticallyv2/v2r/v2ray/v2raytun- encrypt intov2raytun://cryptformat. The default key iscrypt3. Works only with decrypted textcrypt3/crypt4/key3- pick the V2RayTun key (use after thev2argument)
Full build only (omitted from the lite variant).
A link is fetched when you pass fetch, hwid, or ua, or when a convert mode (sb/uri/b64/raw) follows an http(s) URL. It is resolved and decrypted first if needed, then requested.
fetch- fetch the link with no extra headers (use when you do not needhwid/ua)hwid <value>- fetch and send theX-HWIDheader. Usually paired withuaua <happ / incy / v2 / "MyCustomUA">- fetch and send theUser-Agentheader. Aliases:happ→Happ/3.26.1,incy→INCY/3.3.1,v2/v2r/v2ray/v2raytun→v2raytun/android
Conversion runs on a fetched profile, on a local file, or on stdin. A convert mode after an http(s) URL fetches that URL first; after a local path it converts the file; with no positional input it reads stdin.
raw- return the response as is, no conversionb64- same, but decode Base64 if neededuri- convert Xray configs tovless/vmess/trojan/hy2/ss/tuiclinks (default)sb- convert Xray configs to a merged sing-box config (unsupported outbounds are skipped)
Combine sb and uri to get only sing-box-compatible URIs: unsupported outbounds are dropped and vless flow is normalized to a sing-box-valid form.
- A fetched response prints when it is small; over 15000 characters it is written to
hpwnresp_<domain>.txtinstead, overwritten on each run. - Converting a local file writes a sibling copy
<name>_<mode>.<ext>, where<mode>issb,uri,b64, orsb+uri, replacing any existing mode suffix. - Everything else (decryption, encryption, stdin conversion, small responses) prints to stdout.
# Happ
hpwnr happ://crypt5/…
hpwnr happ://crypt4/…
# V2RayTun
hpwnr v2raytun://crypt/…
# Happ (crypt5 by default, or pick a format)
hpwnr https://example.com/sub
hpwnr https://example.com/sub crypt4
hpwnr https://example.com/sub crypt5old # legacy crypt5 layout
# V2RayTun (crypt3 by default, or pick a key)
hpwnr https://example.com/sub v2
hpwnr https://example.com/sub v2r crypt4
hpwnr https://example.com/sub fetch
hpwnr https://example.com/sub hwid abc123
hpwnr https://example.com/sub hwid abc123 ua happ
hpwnr https://example.com/sub hwid f8xdh2e6d5140zv7 ua "My/Custom/UA"
# fetch + convert
hpwnr https://example.com/sub sb
hpwnr https://example.com/sub hwid 1234 ua v2 sb
hpwnr https://example.com/sub hwid 1234 ua happ sb uri
# local file / stdin (no fetch)
hpwnr subscription.txt sb
cat subscription.txt | hpwnr uri
cargo build --release # full, binary at target/release/hpwnr
cargo build --release --no-default-features # lite (see Variants)Cross-compile with cross, which supplies the per-target toolchains (including the Android NDK):
cross build --release --target aarch64-linux-android
cross build --release --target x86_64-unknown-linux-muslThe release profile is size-optimized and stripped; musl targets are statically linked. Add --no-default-features to any of these commands to build the lite variant instead.
Both variants build for every mainstream target: musl Linux (x86_64/i686/aarch64/armv7/riscv64), Windows (gnu/MinGW, x86_64 and i686), every Android ABI (aarch64/armv7/x86_64/i686), and macOS and iOS (linked on a Mac). Because lite has no C dependencies it also reaches targets the full build cannot, such as ARMv6 (arm-unknown-linux-musleabihf) and WebAssembly (wasm32-wasip1).
Two manual GitHub Actions workflows build both variants for all of these targets. .github/workflows/release.yml publishes them to a GitHub Release, with assets named hpwnr-<platform> (full) and hpwnr-lite-<platform> (lite). .github/workflows/build.yml builds without releasing: by default it uploads each binary as its own artifact, or, with its individual_binaries option turned off, collects them into two zip archives (hpwnr_v<version> and hpwnr-lite_v<version>).
[dependencies]
hpwnr = "1.0.0"use hpwnr::{decrypt, encrypt_happ, encrypt_crypt5_legacy, encrypt_v2, fetch, unwrap_link, inspect, convert, ConvertOps, HappMode, V2Key};
let link = encrypt_happ(HappMode::Crypt5, "https://example.com/sub")?; // crypt5 = new salted layout
let old = encrypt_crypt5_legacy("https://example.com/sub")?; // legacy crypt5 layout
let plain = decrypt(&link)?; // happ and v2raytun
let real = unwrap_link("happ://add/…"); // resolve wrappers/redirects
let body = fetch(&plain, "happ", "HWID123")?; // download + strip AES-GCM
let info = inspect(&link); // classify without decrypting
let norm = convert(subscription, ConvertOps::ALL); // JSON outbounds -> proxy URIs
let sb = convert(subscription, ConvertOps::SINGBOX | ConvertOps::BASE64); // Xray -> sing-box
# let subscription = "";
# Ok::<(), hpwnr::Error>(())For pasted input, resolve_and_decrypt unwraps any wrapper/embedded link then decrypts, returning Ok(None) if the result is not an encrypted link:
use hpwnr::resolve_and_decrypt;
if let Some(plain) = resolve_and_decrypt("happ://add/…")? {
println!("{plain}");
}
# Ok::<(), hpwnr::Error>(())All fallible calls return Result<T, hpwnr::Error>; Error is a plain enum you can match on. ConvertOps values (LINKS, BASE64, ALL, SINGBOX) combine with |.
The fetch and decrypt_response_body functions, along with the AesGcm / AesTagLen / Http error variants, require the default fetch feature. For a library without the bundled HTTP/TLS stack (matching the lite CLI variant), disable it:
[dependencies]
hpwnr = { version = "1.0.0", default-features = false }Every public item is documented; run cargo doc --open for the full API reference.
Note: the crate exposes a Rust API. To call it from another language, run the CLI binary as a subprocess (see Calling from other languages).
hpwnr ships as a Rust library, but the CLI binary doubles as a language-agnostic backend: spawn it as a subprocess, pass each link or value as its own argument (or pipe a subscription blob to stdin), and read the result from stdout.
Contract
- The result (decrypted URL, encrypted link, or converted config) is written to stdout.
- Errors are written to stderr, prefixed with
error:, with exit code 1; success is exit code 0. - Pass each link/value as one argument, and prefer the argv/exec form (arrays,
ProcessBuilder,ArgumentList) over a shell command string so that?,&, and%inside URLs need no escaping. - Convert a blob you already have by piping it to stdin:
… | hpwnr sb. - One exception to stdout: a fetched response over 15000 characters is written to
hpwnresp_<domain>.txtin the working directory (the path is echoed to stderr assaved to: …) instead of stdout. Decrypt, encrypt, and local/stdin conversion always print to stdout.
Python
import subprocess
def hpwnr(*args, stdin=None):
r = subprocess.run(["hpwnr", *args], input=stdin,
capture_output=True, text=True)
if r.returncode != 0:
raise RuntimeError(r.stderr.strip())
return r.stdout.strip()
url = hpwnr("happ://crypt5/…") # decrypt
link = hpwnr("https://example.com/sub", "crypt4") # encrypt
sb = hpwnr("sb", stdin=open("sub.txt").read()) # convert a blob to sing-boxNode.js
const { execFileSync } = require("node:child_process");
const hpwnr = (args, stdin) =>
execFileSync("hpwnr", args, { input: stdin, encoding: "utf8" }).trim();
const url = hpwnr(["happ://crypt5/…"]);
const link = hpwnr(["https://example.com/sub", "crypt4"]);
const sb = hpwnr(["sb"], subscriptionText);Java
static String hpwnr(String... args) throws IOException, InterruptedException {
var cmd = new ArrayList<String>(List.of("hpwnr"));
cmd.addAll(List.of(args));
Process p = new ProcessBuilder(cmd).start();
byte[] out = p.getInputStream().readAllBytes(); // stdout
byte[] err = p.getErrorStream().readAllBytes(); // stderr (always small)
if (p.waitFor() != 0) throw new IOException(new String(err).strip());
return new String(out).strip();
}
String url = hpwnr("happ://crypt5/…");
String link = hpwnr("https://example.com/sub", "crypt4");C#
using System.Diagnostics;
static string Hpwnr(string[] args, string? stdin = null) {
var psi = new ProcessStartInfo("hpwnr") {
RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true,
};
foreach (var a in args) psi.ArgumentList.Add(a);
using var p = Process.Start(psi)!;
if (stdin is not null) { p.StandardInput.Write(stdin); p.StandardInput.Close(); }
string outp = p.StandardOutput.ReadToEnd();
string errp = p.StandardError.ReadToEnd();
p.WaitForExit();
if (p.ExitCode != 0) throw new Exception(errp.Trim());
return outp.Trim();
}
string url = Hpwnr(new[] { "happ://crypt5/…" });
string link = Hpwnr(new[] { "https://example.com/sub", "crypt4" });C / C++
// popen captures stdout; stderr stays on the parent. For stdin input or robust
// error capture, use pipe() + fork() + execvp() instead of a shell string.
FILE *p = popen("hpwnr 'happ://crypt5/…'", "r"); // quote args in shell form
char out[65536];
size_t n = fread(out, 1, sizeof out - 1, p);
out[n] = '\0';
int rc = pclose(p); // rc != 0 => errorAny language that can spawn a process and read stdout works the same way (Go os/exec, Ruby Open3, PHP proc_open, and so on).
Provided as is, without warranty. Use it only for legal analysis, interoperability, research, and education, and only with data you are entitled to access. Happ, V2RayTun, and related names may be protected by third-party rights; this project is not affiliated with their owners.