tunelly Download

For operators

For a panel that hands a subscriber a link.

This is the contract the iOS app keeps with a panel. If you run the server side and give people an address or a share link, this is what the app will request, what it will accept, and what it will refuse. It is not a list of providers. How a configuration typically reaches someone is on where to get servers.

How a configuration gets in

iOS registers one URL type. A tap on a registered scheme opens the app, and the whole URL is imported. There is no path or host router. The schemes, exact strings:

tunelly, vless, vmess, trojan, ss, hysteria2, hy2, tuic, wireguard.

vless, hysteria2, hy2, wireguard and tunelly are how a working configuration arrives. vmess, trojan, ss and tuic open the app only so it can refuse them cleanly — they are not supported.

socks, socks5, http and https are understood by the parser but are not registered, so a tap on such a link does not open the app.

The same text can also arrive from the welcome screen's one-tap clipboard paste, from Add a server, or by typing into the field. A subscriber-facing walkthrough is on how to add a configuration.

Classification, in order: a sealed envelope is checked first. Then a whole wg-quick file. Then the rest of the text. A bare panel.example.com/sub/abc is rewritten to https:// and fetched. https://host:port#name and http://user:pass@host are treated as share proxies, not fetched — fetching those would lose the server.

The subscription fetch

This is the part a panel has to get right. The app issues a GET over http or https only. The session is ephemeral. The request times out at twenty seconds, the resource at forty. A cached copy is ignored.

Request headers

User-Agent
One of the identities below, in that order.
Accept
*/*
x-hwid
The iOS vendor identifier when one is present. It is not a hardware identifier. It is stable for this app on this device and resets when the app is removed.
x-device-os
ios, sent with x-hwid.

Identity walk

Panels already branch on a handful of well-known names and hand back different documents for each. We ask in those dialects so the panel returns a list we can read. The names are the contract. The version numbers move with releases; the strings below belong to the current build.

HTTP 401, 403 and 404 stop the walk: those answers concern the address, not the identity. Another HTTP status, or a body this identity cannot read, continues to the next name.

The body

The body is sniffed. Content-Type is not used — panels serve base64 as text/html and JSON as text/plain often enough that the header is not evidence.

Accepted:

Refused by name:

Those documents are parsed here. The traffic is carried by our own engine, not passed through to another core.

Response metadata

Read from headers or from #name: value lines in the body. When both speak, the body wins. The filename in Content-Disposition is not read.

profile-title
Display name. A base64: prefix is decoded. There is no character cap.
subscription-userinfo
upload, download, total, expire. Every field is optional.
profile-update-interval
Hours. A panel that means daily writes 24.
profile-web-page-url
A page the operator wants shown.
support-url
Where the subscriber can ask you, not us.
announce
A free-text notice. A base64: prefix is decoded.
announce-url
A link that belongs with the notice.
new-url
A whole new fetch address. Shown to the person. Never applied silently.
new-domain
A new host; the path — the credential — stays. Same confirmation as new-url.
fallback-url
Where to go if the stored address stops answering.
update-always
true or 1 treats the subscription as stale whenever the app opens. The person's own refresh switch still wins.

Protocols the engine runs

VLESS, Hysteria2, WireGuard (including AmneziaWG), and nothing else. A configuration for any other protocol is refused at import, so a row that carries no traffic never exists. The protocol pages hold the parameter tables.

Two corrections operators get wrong. The Hysteria2 password is the URI userinfo; an auth query parameter is not read. spx / SpiderX is parsed but not honoured by the engine — do not present it as working.

Hiding the address

A sealed carrier hides the panel address from a subscriber using the app. The URI form is tunelly://e/<ver>/<kid>/<payload>. The file form is tunelly-seal/1 <kid> <payload>. Three kinds: a subscription address, a single share link, or a body snapshot. A sealed envelope is checked before every other import branch.

This hides the address from a subscriber using the app. It is not a secret from someone who inspects the binary, and it is not a secret from someone who watches the device's traffic — the device has to send packets to the server. Treat it as a deterrent against casual copying, not as a substitute for access control on your panel. The keys are not a secret embedded in the binary in any stronger sense than that.

The browser tool is on seal a subscription. We do not open other clients' encrypted link formats.

What will not work

A configuration the engine cannot honour is refused at import, with the reason:

An operator arriving from another client will also look for things that are not here. Stated so nobody builds against imagined behaviour:

Where a server comes from

We do not operate servers and we do not sell access. Where to get servers is how a configuration typically reaches an application of this kind, without naming anyone to buy from.