Emit pinSHA256 (hex) in Hysteria2 subscription links

Environment

  • Panel / backend: 2.8.0
  • Node: 2.8.0, core: rw-core / Xray-core 26.6.27
  • Client tested: Happ (bundled core)

What we’d like

When a Hysteria2 inbound uses an inline self-signed certificate and the host has pinnedPeerCertSha256 set, please add the certificate pin to the generated subscription so that strict clients can validate the connection without enabling insecure.

Per the Hysteria2 URI scheme, the server certificate pin is passed as pinSHA256=<hex-sha256>. Today the base64 subscription (XRAY_BASE64) omits this parameter, so the pin configured on the host never reaches the client.

Current output (base64 subscription)

hysteria2://<uuid>@<ip>:443/?sni=www.twinmotion.com&fm=%7B%22quicParams%22%3A%7B%22debug%22%3Afalse%2C%22congestion%22%3A%22bbr%22%7D%7D#<name>

→ no pinSHA256, no insecure → strict clients can’t validate the self-signed cert.

Desired output (verified to work in Happ — cert accepted, no insecure)

hysteria2://<uuid>@<ip>:443/?security=tls&sni=www.twinmotion.com&fm=...&insecure=0&pinSHA256=<HEX_SHA256_OF_LEAF_CERT>#<name>

i.e. hex-encode the host’s pinnedPeerCertSha256 and add it as pinSHA256 (hex, per the Hysteria2 URI spec). This lets self-signed Hysteria2 work out of the box, no manual insecure.

Note on XRAY_JSON (encoding mismatch to consider)

With serveJsonAtBaseSubscription=true, XRAY_JSON emits tlsSettings.pinnedPeerCertSha256 in base64 (e.g. hLzIuDGgIzwCNP1YbnFz14PXIYwR966VK6PEkZCD/lE=). Happ’s bundled Xray core hex-decodes this field and fails to build the TLS config:

infra/conf: Failed to build TLS config. > encoding/hex: invalid byte: U+0068 'h'

Encoding differs by context: Xray-core v26.1.23 uses base64 for pinnedPeerCertSha256 in xray-json tlsSettings, while the Hysteria2 URI spec uses hex for pinSHA256. It would help to emit the correct encoding per output format (hex for the URI).

Steps to reproduce the current behavior

  1. Hysteria2 inbound with inline self-signed cert (e.g. CN=www.twinmotion.com), alpn: ["h3"], congestion bbr.
  2. Host with pinnedPeerCertSha256 (base64) set, securityLayer=TLS, sni, alpn h3.
  3. Add the inbound to a squad; assign to a user.
  4. Fetch the base64 subscription (client UA + x-hwid), base64-decode → the hysteria2:// line has no pinSHA256.

Why it matters

Self-signed Hysteria2 is a stateless, domain-free setup (no ACME needed). Emitting the pin the client already trusts would make it usable in strict clients (e.g. Happ) without insecure — the pin is already configured on the host and only needs to be serialized into the link.

The panel does not generate this field. You set it yourself in the “Advanced” host settings.

So the only possible reason why you have base64 in this field is because you yourself wrote such a value there.

pinSHA256 was added. Please note, it just use the value from “Advanced” host section. it does not generate or modify it. The same applies to Xray-Json.

Thank you for the quick fix and the clarification — that makes sense.

Could you tell me which release will include commit 4856dab (“add pinnedPeerCertSha256 support in hysteria2 base64 share-link”)? Is it planned for the next stable release (e.g. 2.8.1 / 2.9.0), and is there a rough ETA? I’d prefer to wait for a tagged release rather than run the dev image on a production panel.

One detail I’d like to confirm, since the value is passed through verbatim: the Hysteria2 URI scheme expects pinSHA256 in hex, while the “Advanced” host field currently holds the base64 SHA-256. Should I store the pin as hex in the Advanced field so it ends up as pinSHA256=<hex> in the link, or will the panel take care of the encoding?

Thanks again for the great work!

Next release, probably 2.8.1.

As I mentioned above, the panel doesn’t modify or encode the values you specify. If you need hex, you must specify the hex value in the “Advanced” tab.