# SMProxy — full documentation
## Главная
Source: https://docs.smproxy.io/en/index/
title: Home
---
# SMProxy documentation
**SMProxy** is a client for VLESS, Reality, VMess, Trojan, Shadowsocks and
WireGuard/AmneziaWG. iOS, iPadOS, macOS, Apple TV, Android, Android TV, Windows and Linux.
The app ships no servers and provides no VPN service: the user imports a subscription from
a provider and connects. **Everything is free** — there are no paid tiers and no gated
features.
- :material-code-braces: **[Developer documentation](dev-docs/index.md)**
Integration for subscription providers and panels: subscription formats, HTTP headers,
deep links, routing profiles, WireGuard/AmneziaWG parameters, encrypted values.
- :material-cog: **[App settings](settings/index.md)**
Every setting in the app: what it does, its default, and which platforms have it —
plus operating system requirements.
## Documentation for AI assistants
The documentation has **its own MCP server**: an assistant connects in one command and then
finds the right page by itself, instead of inventing header names.
```
claude mcp add --transport http smproxy-docs https://provider.smproxy.io/mcp
```
Setup for Cursor, VS Code, Codex and others is on the
[Documentation for AI](dev-docs/ai-assistants.md) page.
If your assistant has no MCP support, point it at the whole documentation in one file:
[llms-full.txt](https://docs.smproxy.io/llms-full.txt); the page index is at
[llms.txt](https://docs.smproxy.io/llms.txt).
## The app
Downloads and release notes: [GitLab](https://gitlab.com/smproxy/smproxy-binary/-/packages) ·
Website: [smproxy.io](https://smproxy.io)
---
## Обзор
Source: https://docs.smproxy.io/en/dev-docs/index/
SMProxy App is a **free cross-platform client** for Vless/Reality, VMess, Trojan, Shadowsocks, Socks, WireGuard and AmneziaWG, built on the Xray core. The app **does not provide servers** and **is not a VPN service** — the user imports a subscription (config) from a provider and connects. Available on iOS, iPadOS, macOS, Apple TV, Android, Android TV, Windows and Linux.
It's a universal client: it imports a config and connects. Everything else — branding, routing, expiry, statistics — is determined by what the **provider** returns in the subscription response and by the provider account settings.
📘
**Reading these docs with an AI assistant**
This documentation is published as a machine-readable source, so Claude Code, Cursor, Codex and other MCP clients can read the real header formats and deep links instead of guessing them.
```
claude mcp add --transport http smproxy-docs https://gitmcp.io/SMProxy/docs
```
[Setup for other clients →](ai-assistants.md)
---
## Стоимость — сейчас всё бесплатно
Source: https://docs.smproxy.io/en/dev-docs/pricing-everything-is-free-right-now/
SMProxy App is in active development and open testing, so **all features are free right now** and will stay that way for a long time: subscription import, all section-04 headers, device-bound (`addhw`) and encrypted (`crypt1`) links, auto-update, kill-switch, split-tunnel, themes, statistics and messages/push. There are no paid plans and no locked features.
✓Subscription import
✓All response headers
✓addhw & crypt1
✓Auto-update
✓Kill-switch
✓Split-tunnel
✓Themes
✓Statistics
✓Messages / push
🌱
In the future, once the project leaves the testing stage, some features may change. We'll announce any changes openly and in advance — what's available now will stay free for the whole beta period.
## 🔒Fields gated by `provider_active`
Locked keys are provider-controlled branding/behaviour (site/Telegram buttons, renewal UI, `fallback-url`, migration and `hide-settings`). They apply only when the response contains `providerid` and it has `provider_active == true`.
If the provider is not active (no `providerid`, unknown, or `provider_active == false`), these fields are force-reset on every update (so a foreign/stale provider can't swap buttons or fallback). They're restored as soon as the provider is active again.
**Not gated** (always work): `profile-title`, `s-title`, `profile-update-interval`, `subscription-userinfo`, `announce`, `support-url`, `profile-web-page-url`, `providerid`, `geoipurl`/`geositeurl`.
---
## Что требует активного Provider ID
Source: https://docs.smproxy.io/en/dev-docs/what-needs-an-active-provider-id/
One place to check instead of hunting for the 🔒 marks. **Active** means the Provider ID sent with the subscription belongs to a provider account that is currently active. Without it the keys in the first table are simply ignored — the subscription itself keeps working.
## 🔒Requires an active Provider ID
| Area | Keys |
|---|---|
| Bypass | `s-fragment` `s-noise` `s-noises` `s-resolve` `s-dns` — and their INCY/Happ equivalents `fragmentation-*` `noises-*` `server-address-resolve-*` |
| Migration & fallbacks | `new-url` `new-domain` `fallback-url` `fallback-domains` |
| Provider branding | `s-siteurl` `s-sitename` `s-tgbot` `x-tgbot` `sort-order` `serverDescription` |
| Banner & contacts | `banner-text` `banner-button-text` `banner-button-url` `banner-bg-color` `banner-button-color` `announce-url` `support-email` |
| Expiry UI | `sub-expire` `s-showexpire` `sub-expire-button-text` `sub-expire-button-link-site` `sub-expire-button-link-tg` `notification-subs-expire` |
| Access | `hide-settings` |
## ✓Works with or without a Provider ID
| Key | Note |
|---|---|
| `profile-title / s-title` | Subscription name |
| `profile-update-interval` | Auto-refresh interval |
| `profile-web-page-url` | Provider web page |
| `announce` | The announcement text itself — but `announce-url` (making it clickable) needs an active provider |
| `support-url` | Support link |
| `subscription-userinfo` | Quota and expiry date |
| `subid` | Stable subscription id |
| `routing` `autorouting` `routing-update-url` `routing-update-interval` `routing-profile` | Routing profiles, including the deep-link command forms — a profile carries rules only, so it needs no active provider |
| `geoipurl / geositeurl` | Geo database addresses — subscription response only |
The same gating applies however the value arrives — an HTTP header, a `#key:` line in the body, or the provider account.
This reference reflects current app behaviour. Verify exact format/base64 rules for individual headers against the server before publishing.
---
## Добавление подписки
Source: https://docs.smproxy.io/en/dev-docs/adding-a-subscription/
Three import methods:
🔗
**Subscription link**
Paste an https://… link — the server returns the config body and headers.
📷
**QR code**
Scan a QR encoding a URL or an smproxy:// deep-link.
⚡
**Deep-link**
An smproxy://… link opened from a browser/message or pasted into the add form — resolved on all platforms.
The subscription body can be a list of `vless://…`, base64links, a base64 blob or JSON configs. All routing, branding and expiry arrive in the **response headers** (section 04).
---
## Метаданные подписки
Source: https://docs.smproxy.io/en/dev-docs/subscription-metadata/
The subscription endpoint response is the **body** (server list) plus **metadata** (branding, expiry, updates). Metadata can be delivered **two ways**, and the client reads both.
AHTTP response header
`Profile-Title: My VPN`
BComment line in the body
`#profile-title: My VPN`
★
**The HTTP header wins.** A `#key: value` line in the body is used only if there's no header of the same name. This lets you deliver metadata when you can't set headers (static hosting, Telegram files, etc.).
⇄
**On/off values.** Any header that switches a feature on or off accepts a family of equivalents, case-insensitive: `1` / `on` / `true` / `yes` all mean *enabled*, and `0` / `off` / `false` / `no` all mean *disabled*.
⚠
**Bypass features: your setting wins over the user's.** For all four bypass headers — `s-fragment`, `s-dns`, `s-noise`, `s-resolve` — the value you send takes precedence over the user's own toggle, `off` included. The user's toggle only applies when you send nothing at all. This is deliberate: these parameters have to match what your server expects, and a user switching them on their own would simply break the connection.
## Live subscription updates
A provider can change a subscription's bypass settings and routing **without waiting for the next subscription re-fetch**: changes made in the provider account reach online devices within minutes and are applied without reconnecting — new values take effect on the next connect. The same rules apply as for the HTTP headers above — in particular, they are honored only while the provider is **active**. The delivery mechanism is internal to the app: nothing needs to be implemented on the subscription endpoint.
## Body line syntax
- Starts with # , then a key [A-Za-z0-9-]+ , then the value.
- The colon is optional — #profile-title My VPN also works.
- The key is case-insensitive.
- The first occurrence of a key wins; duplicates are ignored.
- The body is scanned both as-is and after full base64 decoding — it works for plain text and for a single base64 blob (the usual vless-link format).
- Only the keys in the table below are read from the body; lines with other keys are ignored.
**base64 for non-ASCII (e.g. Cyrillic)**
Text fields can be encoded by prefixing `base64:` — works in both header and body. Applies to `profile-title`, `s-title`, `announce`, `sub-expire-button-text`.
`#profile-title: base64:TXkgVlBO → “My VPN”`
## Keys
Each key works both as a header `Key: value` and as a body line `#key: value`. **All free.**
## Branding & links
| Key (header / #body) | Purpose |
|---|---|
| `profile-title / s-title`🔒 | Subscription title (text or base64:) |
| `s-sitename`🔒 | Provider/site name |
| `s-siteurl`🔒 | Provider website URL |
| `s-tgbot / x-tgbot`🔒 | Telegram bot/channel link |
| `support-url`🔒 | Support link |
| `announce`🔒 | Announcement banner text (text or base64:), up to 5 lines — longer text is ellipsized |
| `announce-url`🔒 | Makes the announcement clickable — opens this URL |
| `support-email`🔒 | Support email — adds an “Email support” button to the subscription card |
| `profile-web-page-url`🔒 | Provider web page |
## Subscription status & renewal
| Key (header / #body) | Purpose |
|---|---|
| `subscription-userinfo`🔒 | upload=…; download=…; total=…; expire=… (quota + expiry epoch) |
| `sub-expire / s-showexpire`🔒 | Show expiry status |
| `sub-expire-button-text`🔒 | Renewal button label (text or base64:) |
| `sub-expire-button-link-site`🔒 | Renewal button → website |
| `sub-expire-button-link-tg`🔒 | Renewal button → Telegram |
| `notification-subs-expire`🔒 | 1 — send “subscription expires in N days” notifications, starting 3 days before expiry. Independent of sub-expire / s-showexpire (does not control the renewal buttons and does not depend on them). Absent = no notifications. Also accepted as #notification-subs-expire: 1 in the body. |
## Updates & data
| Key (header / #body) | Purpose |
|---|---|
| `profile-update-interval`🔒 | Subscription auto-update interval (hours) |
| `fallback-url`🔒 | Fallback subscription URL |
| `geoipurl / geositeurl`🔒 | GeoIP / GeoSite database sources (section 07) |
| `sort-order`🔒 | Order of servers in the list: ping (fastest first, by measured latency — unmeasured go last), name (alphabetical) or none (as sent, the default). Unknown values are treated as none. |
## Migration & access
| Key (header / #body) | Purpose |
|---|---|
| `new-url`🔒 | Fully replace the subscription URL (migration) — see below |
| `new-domain`🔒 | Replace only the URL host: host or host:port (safe for personal links). Can also be driven from the provider portal without touching your panel — see section 05b |
| `fallback-domains`🔒 | Backup hosts, comma-separated. If the subscription host stops answering, the app tries these in order, keeping each user's own path and token. Unlike new-domain this changes nothing permanently — a way out when your main address is blocked, not a migration. Set it from the provider portal or send it yourself (section 05b) |
| `hide-settings`🔒 | Hides the subscription URL and its config from the user — full details in section 04b. 1 hides, 0 un-hides; an absent header changes nothing. |
## Identification
| Key (header / #body) | Purpose |
|---|---|
| `providerid / s-providerid`🔒 | Provider ID (section 05). The s- variant is preferred; also read from the URL ?providerid= |
| `subid`🔒 | Stable subscription ID — lets you change the subscription URL without your users ending up with duplicates (section 05a) |
## Bypass
| Key (header / #body) | Purpose |
|---|---|
| `s-noise`🔒 | Noise packets before the handshake — junk traffic sent ahead of the connection so DPI can't recognise its start. Works alongside s-fragment (they share the same egress). on / 1 uses the defaults (rand, packet 50-100, delay 10-20); off / 0 disables. Full form: type=rand;packet=50-100;delay=10-20, or positional rand,50-100,10-20; type is rand, str or hex. → full details in section 08 |
| `s-resolve`🔒 | Pre-resolve the server address over DoH before the tunnel comes up — for networks where the local DNS forges the answer for your node’s domain. on / 1, off / 0, or a resolver URL with an optional bootstrap IP. → full details in section 08 |
| `s-fragment`🔒 | TLS ClientHello fragmentation against SNI-based DPI (full reference — section 08). |
| `s-dns`🔒 | DNS-over-HTTPS inside the tunnel: on / 1 — the built-in default resolver (dnsforge.de); a URL — that resolver; off / 0 — disable. → full details in section 08 |
🔒
Locked keys work **only with an active provider** — [see the full list](#gating).
## Provider banner — `banner-*`
A banner on the subscription card, shown **only while the provider id is active** (INCY-compatible). Colors must be `#RRGGBB` — anything else is ignored and the app's own theme color is used. The button caption color is picked automatically from the button's brightness, so it stays readable on light and dark backgrounds alike.
| Header | Value & purpose |
|---|---|
| `banner-text`🔒 | text or `base64:` — banner body, up to 5 lines |
| `banner-button-text`🔒 | text or `base64:`, ≤ 25 characters — button caption |
| `banner-button-url`🔒 | a URL or a deep link — where the button leads |
| `banner-bg-color`🔒 | `#RRGGBB` — banner background |
| `banner-button-color`🔒 | `#RRGGBB` — button background |
## Bypass parameters at a glance
A summary of the four bypass headers — the detailed reference for each is in section 08.
| Header | Values |
|---|---|
| `s-fragment` | `on` \| `off` \| `packets=;length=;interval=[;maxsplit=]` · default when only `on` is sent: `packets=tlshello;length=50-100;interval=10-20` |
| `s-noise (`s-noises`)` | `on` \| `off` \| `type=;packet=;delay=` · default when only `on` is sent: `type=rand;packet=50-100;delay=10-20` |
| `s-resolve` | `on` \| `off` \| `[;ip=][, …]` · default when only `on` is sent: the built-in resolver |
| `s-dns` | `on` \| `off` \| `` · default when only `on` is sent: the built-in resolver (dnsforge.de) |
## Compatibility with INCY / Happ header names
If you already configure another client, the app also understands their atomic keys and folds them into the equivalent setting. **Our `s-*` header always wins** — the keys below are read only when the matching `s-*` header is absent. `*-enable: 0` means explicitly off.
| Their keys | Mapped to |
|---|---|
| `fragmentation-enable · fragmentation-packets · fragmentation-length · fragmentation-interval · fragmentation-maxsplit` | `s-fragment` |
| `noises-enable · noises-type / noises-packet-type · noises-packet · noises-delay · noises-rand` | `s-noise` |
| `server-address-resolve-enable · server-address-resolve-dns-domain · server-address-resolve-dns-ip` | `s-resolve` |
`noises-rand: ` becomes `type=rand;packet=`. `noises-rand-range` is not supported.
**Where a value can come from, and what wins:** 1) the HTTP response header — highest priority; 2) a `#key: value` line in the body (and, for routing commands, a bare `*://routing/…` line) — used when the header is absent; 3) the app's own setting — when the provider sends nothing. For the four bypass headers the provider's value wins over the user's toggle, `off` included.
Text values accept plain UTF-8 or a `base64:` prefix; any header value may also arrive `crypt1/`-encrypted.
## URL migration: `new-url` / `new-domain`
Re-point the saved subscription URL — the provider can move without the user having to re-add the subscription.
- `new-url` — replaces the whole URL (any format), the same for all subscriptions.
- `new-domain` — changes only the host (scheme, port, path, query are kept); each subscription keeps its own path/token — safe for personal URLs. If both are sent, `new-url` wins.
Applies only if the same response contains `providerid` AND that provider has `provider_active == true`. All local subscriptions with that providerId are migrated. The `locked` flag is preserved: a subscription from `crypt1`/`addhw` stays locked on the new URL.
## Compatibility aliases (Happ / INCY)
Headers other clients use are read as their SMProxy counterparts when the SMProxy header is absent — a panel written for Happ or INCY works without changes. Aliases are accepted both in HTTP headers and in the `#key: value` body form.
| Other client sends | Read as |
|---|---|
| `subscription-name` | `profile-title` — INCY fallback name |
| `content-disposition: attachment; filename="x.txt"` | `profile-title` — filename without `.txt` / `.yaml` / `.yml`; lowest priority |
| `homepage` | `profile-web-page-url` — INCY |
| `hide-url` | `hide-settings` — `1`/`true` hides, `0`/`false` un-hides |
| `sub-info-text` | `banner-text` — Happ “advanced announcements”; `0` = no banner |
| `sub-info-button-text / sub-info-button-link` | `banner-button-text` / `banner-button-url` |
| `sub-info-color (red · blue · green)` | `banner-bg-color` — mapped to a hex colour |
| `sub-expire-button-link` | `sub-expire-button-link-tg` for `t.me` / `tg://`, otherwise `sub-expire-button-link-site` — Happ single-link form |
| `routing-enable: 0` | `routing: routing/off` — Happ “disable routing” |
| `subscription-userinfo: 0` | header absent — INCY “hide the traffic block” |
| `expire= in subscription-userinfo` | seconds — values above 32 000 000 000 are treated as ms |
The SMProxy header always wins when both are present.
---
## Заголовки запроса
Source: https://docs.smproxy.io/en/dev-docs/request-headers-client-server/
Every subscription fetch carries these headers, so a panel can identify the device and bind a subscription to it (a routing-profile download carries only the `User-Agent`; geo-database downloads carry none):
| Header | Value |
|---|---|
| `X-Hwid` | **Stable device id**. Computed once on first launch and never changes afterwards — not on app updates and not on reinstall. Shown in the app’s About screen; it is what `addhw` links compare against. Each client app has its own HWID scheme, so a device bound in another client is a different HWID for the panel. |
| `X-Device-Os` | `Android` · `iOS` · `macOS` · `tvOS` · `Windows` · `Linux` |
| `X-Ver-Os` | OS version string — e.g. `16`, `18.6`, `10.0.26200` |
| `X-Device-Model` | Device model — e.g. `SM-S942B`, `iPhone16,2`, `PC` |
| `X-Device-Locale` | `ru_RU` etc. — device language and region |
| `X-App-Version` | `2.0.5 (422)` — marketing version + build in one string |
| `X-App-Build` | `422` — build number alone — the value to gate on (Android 423+, Apple 313+, desktop 199+; older builds send only `X-App-Version` and the `User-Agent`) |
| `User-Agent` | `SMProxy//-` — e.g. `SMProxy/2.0.5/422-Android`. Use it when you want to serve different content to different builds |
All header names are case-insensitive.
## `User-Agent` — one format on every platform
```
SMProxy//-
```
| Platform | Example |
|---|---|
| Android | `SMProxy/2.0.5/422-Android` |
| iOS / iPadOS | `SMProxy/2.0.9/312-iOS` |
| macOS | `SMProxy/2.0.9/312-macOS` |
| tvOS | `SMProxy/2.0.9/312-tvOS` |
| Windows | `SMProxy/2.0.2/198-Windows` |
| Linux | `SMProxy/2.0.2/198-Linux` |
It is sent on every subscription fetch and whenever a routing profile is downloaded by URL. **The build number is what you gate on** — the third segment grows on every release and never repeats, while the marketing version can stay the same across several builds. A panel that serves different content to different builds should parse it like this:
```
^SMProxy/[^/]+/(\d+)-(Android|iOS|macOS|tvOS|Windows|Linux)$
```
---
## Описание сервера
Source: https://docs.smproxy.io/en/dev-docs/server-description/
A per-server caption shown under the server name **instead of** the default protocol label (`VLESS | TCP | Reality`). Use it to say something the label can't — *Netflix / streaming*, *for gaming*, *10 Gbps*. Unlike the keys above it is not sent per subscription: it rides with each individual server.
🔒
**Honored only while the provider id is active** — the same gating as `s-fragment` and `s-dns`. Without an active provider id the app falls back to the protocol label.
| Rule | Value |
|---|---|
| `serverDescription` | Field name |
| **base64** | Encoding in links (plain text in JSON configs) |
## In share links
Inside the `#fragment`, after the title, separated by `?`:
```
vless://uuid@host:443?security=reality&type=tcp#🇳🇱 Netherlands?serverDescription=TmV0ZmxpeCAvIHN0cmVhbWluZw==
```
The same syntax works for `vless://`, `trojan://`, `ss://` and `socks://`.
## In vmess links
A field inside the base64-encoded JSON body:
```
{ "ps": "🇳🇱 Netherlands", "add": "host", "port": "443", "serverDescription": "TmV0ZmxpeCAvIHN0cmVhbWluZw==" }
```
## In JSON configs
Inside a `meta` object next to `remarks` — here the value is **plain text**, not base64:
```
{
"remarks": "🇳🇱 Netherlands",
"meta": { "serverDescription": "Netflix / streaming" },
"outbounds": [ … ]
}
```
In links, a value that isn't valid base64, or decodes to nothing, is ignored — the server keeps its protocol label rather than showing broken text.
---
## Идентификатор подписки
Source: https://docs.smproxy.io/en/dev-docs/subscription-id-subid/
**What it solves.** When you hand a user a *new* subscription URL — a domain migration, a re-issued token, a move to another panel — the app has no way to tell it is the same subscription. It adds a second entry and the user ends up with two copies of the same thing.
Send a `subid` and that stops happening: the app treats a matching `subid` as **the same subscription**, replaces its URL with the new one and refreshes its servers in place.
**Where it can come from (any one — same rules as the other metadata)**
1. HTTP header — `subid: my-sub-42`
2. Body comment — a line `#subid: my-sub-42` in the subscription body
3. URL query — `…?subid=my-sub-42`
An HTTP header wins over a body comment, which wins over the URL. **Format:** letters, digits, `-`, `_` and spaces (`[A-Za-z0-9 _-]+`). Anything else is ignored — the value is simply not stored.
| Situation | What the app does |
|---|---|
| Same URL already added | Updates that subscription (unchanged behaviour) |
| New URL, subid matches an existing subscription | Updates that one: new URL + fresh servers, keeps the user's custom name |
| New URL, subid is new or absent | Adds a new subscription (unchanged behaviour) |
**`subid` is optional.** Without it everything works exactly as before — duplicates are detected by URL only. Sending it is simply a way to keep control of your users' entries when your addresses change.
Pick a value that **never changes** for a given user's subscription. It doesn't have to be secret — it is only compared with what the app already has — but it must be stable and unique per subscription. A UUID or your internal subscription id are both fine.
**Example**
`subid: 8f3a91c2-4b77-4c1e-9a02-5e6d1f0b7c33`
Available on all platforms: iOS, Android, macOS, Windows, Linux and Apple TV.
---
## Скрытие URL подписки
Source: https://docs.smproxy.io/en/dev-docs/hiding-the-subscription-url-hide-settings/
`hide-settings: 1` stops the subscription URL and its raw content from leaving the device. It puts the subscription into exactly the same state as an encrypted (`crypt1`) or hardware-bound (`addhw`) one — internally the app treats “locked” and “hidden by the provider” as one condition.
## What it blocks
| Blocked | Detail |
|---|---|
| The URL field in the subscription editor | The field is not rendered at all — the user cannot read, copy or change the address |
| “Copy link” in the subscription menu | Removed |
| “View config” | Removed, and the config-export routine itself refuses to return anything even if called from elsewhere in the app |
| Editing the URL through an update | An edit that would change the address is ignored; the name and the routing profile stay editable |
## What keeps working
Connecting, ping, traffic statistics, subscription refresh, the server list, choosing servers, routing profiles, the expiry UI — the user simply cannot see or export the address.
## Values and behaviour
| Value | Effect |
|---|---|
| `1` | Hide. Also clears any pending “unlock” request from the user. |
| `0` | Un-hide — **and also lifts a `crypt1` lock** on that subscription, if the provider set one. This is deliberate: the provider owns the link, so an explicit `0` from an active provider opens the subscription completely. |
| *absent* | Changes nothing — the current state is kept. The flag is **sticky**: set it once and it holds across refreshes until you send an explicit `0`. |
Anything other than `1` or `0` is ignored and logged as “value not recognised” — `on` / `off` / `true` / `false` are **not** accepted here.
Accepted as an HTTP response header, as `#hide-settings: 1` in the body, and from the provider account. **Honored only while the provider id is active** — an inactive or unknown provider cannot hide (or reveal) anything.
---
## Два канала, одно правило
Source: https://docs.smproxy.io/en/dev-docs/two-channels-one-rule-who-set-a-value-can-clear-it/
The same keys can arrive two ways — in the **subscription response** (an HTTP header or a `#key:` line) and from the **provider account** (applied to online devices within minutes). They follow one rule:
| Situation | Result |
|---|---|
| The key is in the subscription response | Its value wins and the field is marked “set by the subscription” |
| The key is in the provider account | Its value wins over the subscription’s and the field is marked “set by the account” |
| The key is **missing** from the account update, and the field was set **by the account** | The field is **cleared** — you removed it in the panel, so it disappears in the app |
| The key is **missing** from the account update, and the field was set **by the subscription** | The field is **kept** — the account never clears what the subscription set |
In short: **whoever wrote last, wins**, and a value can only be removed by the side that put it there. That is how you take an announcement or a banner down — clear it in the panel and it disappears on the next update; the subscription response keeps whatever it sends.
Fields with no “empty” state — the subscription name, brand links, `hide-settings` — are never auto-cleared; send a new value to change them.
⚠️ This applies to **all** keys, including bypass (`s-fragment`, `s-noise`, `s-resolve`, `s-dns`) and routing: if you set `s-fragment` from the panel and later remove it there, fragmentation switches off on the next update.
---
## Форматы ссылок
Source: https://docs.smproxy.io/en/dev-docs/link-formats/
Everything the client accepts, in one place. All schemes are case-insensitive.
## Server links
| Scheme | Payload |
|---|---|
| `vless:// vmess:// trojan:// ss:// socks://` | Standard share links, as issued by any panel |
| `wireguard:// · amneziawg:// · awg:// · wg://` | base64 of a plain WireGuard .conf — the same text the official client takes — or base64 of xray-style JSON settings |
| `hysteria2:// · hy2://` | Accepted and stored, but not yet supported by xray-core — such a server will not connect |
**Base64 is read leniently** — standard and URL-safe alphabets, with or without padding.
A `#fragment` sets the display name (percent-encoded). It may also carry a server description — see section 04a.
## What “View config” shows
The client applies one simple rule: **if the link carries base64, it shows the decoded content; otherwise it shows the link as it is**. So `wireguard://` displays the readable `.conf`, while `vless://uuid@host:443?…` stays a link — there is nothing to decode. JSON payloads are pretty-printed.
## Subscriptions
A subscription URL may return:
- **plain text** — one link per line;
- **base64** of that same list;
- **JSON** — a full xray config, or a container with several WireGuard/AmneziaWG servers:
```
{"type":"amneziawg","version":1,"servers":[
{"name":"Germany","config":""}]}
```
`type` is accepted as `amneziawg`, `awg`, `wireguard` or `wg`. A broken entry is skipped rather than failing the whole subscription.
## Encrypted header values
Header values can be sent in a closed form, so links and settings don't travel around in the open. **Any** subscription header may carry its value encrypted, using the same `crypt1/` prefix as the deep links:
```
routing-update-url: crypt1/
routing: crypt1/
autorouting: crypt1/
s-dns: crypt1/
s-fragment: crypt1/
s-resolve: crypt1/
s-noise: crypt1/
new-url: crypt1/
s-siteurl: crypt1/
```
The client strips the prefix, decrypts the payload and then treats the result exactly as if it had arrived in the clear. **Without the prefix nothing changes** — existing subscriptions keep working untouched. Encryption is **optional and per-header**: a single response can freely mix encrypted and plain values.
**The encryption is exactly the one used by `smproxy://crypt1/…` links** — AES-256-GCM, `nonce || ciphertext || tag`, base64-encoded, under the same shared key. A panel that already issues those links needs no new code: encrypt the value, prefix it with `crypt1/`, and put it in the header.
To get an encrypted value without writing any code, use the public generator — no authentication needed. The response has two fields: `link` is the full ready-made link, and `cryptedPart` is just the encrypted payload, which is what goes into a header after the `crypt1/` prefix.
```
POST https://provider.smproxy.io/public/crypto-link
{ "url": "https://example.com/routing.json" }
→ {
"link": "smproxy://crypt1/AbCd…",
"cryptedPart": "AbCd…"
}
routing-update-url: crypt1/AbCd…
s-dns: crypt1/AbCd…
```
[Encrypted link generator →](https://smproxy.io/link-crypto.html) — public, no sign-in needed. All tools: [Link generators](tools.md).
Both separators are accepted — `crypt1/` and `crypt1:` — but **use the slash**: it matches the deep links, so there is one format to remember.
⚠
**This is obfuscation, not secrecy.** The key ships inside every client, so anyone who takes the app apart can read these values. It keeps the profile URL out of plain sight — away from a casual glance at traffic, or from being pasted into a chat — and nothing more. Do not put anything genuinely sensitive behind it.
## Deep links
Two schemes are accepted: **`smproxy://`** (current) and `smartvpn://` (pre-rename, kept working for links already handed out). That applies to **clickable links only** — the OS hands a link to the app registered for its scheme. Inside the `routing` / `autorouting` headers any scheme word is accepted (section 07a).
| Link | Effect |
|---|---|
| `smproxy://add?url=` | Add a subscription |
| `smproxy://add/ · add/` | Same, path form |
| `smproxy://crypt1/` | Add from an encrypted payload |
| `smproxy://routing/add/` | Import a routing profile, don't switch to it |
| `smproxy://routing/onadd/` | Import and activate |
| `smproxy://autorouting/add\|onadd/` | Same, plus keep it updated from that URL |
| `smproxy://routing/off` | Turn routing off |
The payload may also be passed as `?data=`. For routing links the target may hold the profile JSON, its base64, or another `smproxy://` link — all three resolve.
---
## Профили маршрутизации
Source: https://docs.smproxy.io/en/dev-docs/routing-profiles/
A **routing profile** is a full rule set: what goes straight out, what goes through the tunnel and what is blocked, plus optional split-DNS. It answers *“my subscription is a single `vless://` link, so there is no routing inside it”* — the profile carries routing separately from the servers.
★
**Format compatibility.** The profile format is the same one Happ uses, field names included. That's deliberate: profiles travel between apps as links and files, and interoperability beats our own naming.
## The profile
```
{
"Name": "Bypass RU",
"GlobalProxy": true,
"DirectSites": ["geosite:category-ru", "domain:gosuslugi.ru"],
"DirectIp": ["geoip:ru", "192.168.0.0/16"],
"ProxySites": ["geosite:youtube"],
"BlockSites": ["geosite:category-ads-all"],
"RemoteDNSType": "DoH", "RemoteDNSDomain": "https://dnsforge.de/dns-query",
"DomesticDNSType": "DoU", "DomesticDNSIP": "77.88.8.8",
"DnsHosts": { "example.com": "1.2.3.4" },
"DomainStrategy": "IPIfNonMatch",
"UpdateUrl": "https://example.com/routing.json",
"UpdateInterval": 24,
"LastUpdated": 1756600000
}
```
| Field | Meaning |
|---|---|
| `Name` | Profile name. It is also the update key: importing a profile with the same name replaces the old one instead of creating a duplicate. |
| `GlobalProxy` | Where unmatched traffic goes: true — through the tunnel, false — direct. |
| `DirectSites / ProxySites / BlockSites` | Domains and categories: geosite:ru, domain:example.com, regexp:… |
| `DirectIp / ProxyIp / BlockIp` | IPs, subnets and geoip:ru. |
| `RemoteDNS* / DomesticDNS*` | Split-DNS: the remote resolver answers for proxied names, the domestic one for direct names. Six fields — see “Split-DNS fields” below. |
| `DnsHosts` | Static domain → IP mappings. |
| `DomainStrategy` | Rule matching order: AsIs (default), IPIfNonMatch, IPOnDemand. |
| `FakeDNS` | Accepted for compatibility (true/false, boolean or string as in Happ); the SMProxy core does not use FakeDNS, the value is ignored. |
| `UpdateUrl` | Where to fetch a fresh copy of this profile. With it the profile is self-contained: hand it out as base64 or a file and it still keeps itself up to date. |
| `UpdateInterval` | How often to refresh, in hours. 12…168 (a week); 24 by default. |
| `LastUpdated` | Optional. Unix timestamp of the profile’s last change — number or numeric string, as in Happ. Shown in the profile card and used as a version guard — see below. |
Rules are applied in the order **block → direct → proxy**, and they sit **ahead of** any rules that came with the server config.
`GlobalProxy` and `FakeDNS` are accepted both as JSON booleans and as the strings `"true"` / `"false"` that Happ and INCY emit. Fields other clients add for their own UI — `RouteOrder`, `UseChunkFiles` — are ignored: SMProxy always applies rules in the order **block → direct → proxy**.
## Split-DNS fields
| Field | Meaning |
|---|---|
| `RemoteDNSType` | Protocol of the remote resolver: `DoH`, `DoT`, `DoQ` or `DoU` (plain UDP). |
| `RemoteDNSDomain` | Resolver address — a DoH/DoT URL such as `https://dnsforge.de/dns-query`. Empty for plain UDP. |
| `RemoteDNSIP` | Resolver IP. With a `RemoteDNSDomain` it is the **bootstrap address** of that domain — the same role `;ip=…` plays in the `s-resolve` header; without a domain it is a plain UDP resolver. |
| `DomesticDNSType / DomesticDNSDomain / DomesticDNSIP` | The same three fields for the domestic resolver (direct names). |
| `RemoteDns / DomesticDns` | **Legacy** spellings from older Happ/INCY profiles — an alternative to `RemoteDNSIP` / `DomesticDNSIP`. Read only when the new field is absent or empty. Do not use them in new profiles. |
How the resolvers are used:
- **Which resolver answers what.** Remote — names that go through the tunnel, domestic — names routed direct (`DirectSites`). The remote resolver is queried through the tunnel, the domestic one over the physical network.
- **`RemoteDNSDomain` is also the client's own resolver** for its service requests (subscription refresh, profile download) when the subscription sends no `s-resolve`. A routing profile therefore sets DNS for everything, not just for traffic inside the tunnel.
- **Bootstrap.** The `;ip=…` suffix of `s-resolve` does **not** work inside `RemoteDNSDomain` — there is no parsing there, the address would go out with junk at the end. Put the bootstrap address into `RemoteDNSIP`.
- **Fallback chain.** The profile's resolvers go first, then the DNS servers from the connection config itself (their own `domains` / `skipFallback` splits are kept). If every profile resolver is unreachable, resolution falls through to the config's servers.
- **Client support for arrays:** SMProxy from Android 411 / iOS, macOS, tvOS 305 / Windows, Linux 193. Happ, INCY and older SMProxy builds read only the string form — send a string unless you know your users are updated.
**String or array — an SMProxy extension.** `RemoteDNSDomain`, `RemoteDNSIP`, `DomesticDNSDomain` and `DomesticDNSIP` each accept a single string (the classic form, compatible with Happ/INCY) or an array of strings:
```
"RemoteDNSDomain": ["https://dns1.example/dns-query", "https://dns2.example/dns-query"],
"RemoteDNSIP": ["1.2.3.4", "5.6.7.8"]
```
Order = priority: the core queries resolvers top-down, so the second entry is the fallback for the first. `*DNSIP[i]` is the bootstrap address for `*DNSDomain[i]`; an IP without a paired domain acts as a plain resolver.
## Version guard — `LastUpdated`
When a profile with the same `Name` arrives **automatically** (subscription headers, an `UpdateUrl` refresh, a link), it replaces the stored one **only if its `LastUpdated` is greater**. If either side has no timestamp, the profile is replaced unconditionally. Bump the value whenever you change the profile. Manual imports from the app UI always apply.
[🧩 — **Don't write the JSON by hand** — The builder assembles a profile from plain fields and gives you the JSON, an smproxy://routing/onadd/… link and its QR code. Everything runs in your browser. — Routing builder →](https://smproxy.io/routing-link.html)
## Handing a profile to the user
**By link** — the natural way: from a browser, a message or a QR code.
| Link | Effect |
|---|---|
| `smproxy://routing/add/` | Import the profile, don't switch to it |
| `smproxy://routing/onadd/` | Import and activate |
| `smproxy://autorouting/add/ · …/onadd/` | Same, plus keep it updated from that URL |
| `smproxy://routing/off` | Turn routing off |
The payload may also be passed as `?data=`. A URL may return the profile JSON, its base64, or another `smproxy://` link — all three are accepted.
**By subscription header** — the profile arrives with the subscription itself.
| Header | Purpose |
|---|---|
| `routing` | The profile: base64, raw JSON, or a URL to fetch it from. Refreshed together with the subscription. |
| `autorouting` | A URL to fetch the profile from and keep updating on its own schedule. |
| `routing-update-url` | Where to update the profile from — when you don't want UpdateUrl inside the profile itself. |
| `routing-update-interval` | Update interval in hours (12…168). Clamped to that range. |
| `routing-profile` | Which profile this subscription should use, by name. |
A profile arriving from a subscription is **added to the list but not switched on** — the choice stays with the user. These keys work with or without a Provider ID.
**Headers take deep-link commands too.** The value of `routing` / `autorouting` may be a deep-link command — the same forms as the clickable links, with **any scheme word** (`smproxy://`, `happ://`, `yourapp://`): inside a header the word before `://` does not matter.
| Header value | Effect |
|---|---|
| `*://routing/add/` | Import the profile into the list, **don’t** assign it |
| `*://routing/onadd/` | Import **and assign** to this subscription |
| `*://autorouting/add/ · …/onadd/` | Same, plus keep the profile updated from that URL |
| `*://routing/off` | **Detach the profile from this subscription** — the provider switches its routing off |
The payload may be `crypt1/`-encrypted, like any other header value.
The same commands are also accepted as a **bare line in the subscription body**, right among the server links (INCY-compatible):
```
vless://uuid@server1:443?security=tls#Server1
vmess://eyJhZGQiOiAic2VydmVyMi...
incy://routing/onadd/ewogICJOYW1lIjogIl...
```
A real HTTP header, or an explicit `#routing:` body header, takes precedence over a bare line.
## Global vs per-subscription
The user picks one active profile in Settings — the **global** choice. A subscription can override it: if it names a profile (in its own settings, or via the `routing-profile` header), connecting to that subscription's server uses **that** profile instead. Both are switchable off: globally in Settings (or with `routing/off`), and per subscription with *“Do not apply”*.
## Which profile wins
When a subscription refreshes, several of the headers above can arrive at once. They are resolved in this order:
1
**`routing` / `autorouting`** — a profile sent inline wins over one fetched from `routing-update-url`. Send the profile itself when you want to be sure exactly which version the client gets.
2
**`routing-update-url` alone** — if no profile came inline, the client fetches one from this URL and imports it. The URL works on its own; it does not need `routing` alongside it.
3
**`routing-profile` (a name)** — decides which profile the subscription *uses*, and it outranks whatever arrived in this refresh. Naming `Corporate` while also sending a profile called `Default` means the subscription uses `Corporate`; `Default` is still imported into the list.
**A name that matches nothing changes nothing.** If `routing-profile: Corporate` arrives and no profile by that name exists on the device, the subscription keeps the routing settings it already had. The client will not fall back to some other profile, and it will not clear the setting — a typo in the header can't silently reroute your users' traffic.
**The global choice is never touched.** Everything above sets the profile *for that subscription*. The profile the user picked in Settings stays as it is, and keeps applying to every other subscription. To change what a subscription uses, use `routing-profile`; there is no header that repoints the user's global selection.
**Changes apply on the next connect.** Profiles are read when the tunnel config is built, so a profile that arrives while connected takes effect after a reconnect — refreshing the subscription mid-session does not reroute live traffic.
**Which resolver is used, in order**
the `s-resolve` header → the `RemoteDNSDomain` field of the active routing profile → the user's own setting → the built-in list (if the user enabled it) → the system resolver.
`s-resolve: off` disables pre-resolving altogether — the profile is not picked up in that case.
---
## Обход блокировок
Source: https://docs.smproxy.io/en/dev-docs/censorship-bypass/
**WHY IT MATTERS**
DPI systems often block by **SNI** (the destination domain) read from the **TLS ClientHello** — the first packet of every TLS/Reality handshake. **Fragmentation** splits that ClientHello into many tiny TCP segments with micro-delays, so DPI can't reassemble the SNI → can't see the domain → can't cut by domain. It runs entirely on the stock Xray core (a `freedom` outbound with `fragment` settings) — with no extra server or infrastructure.
The four parameters below solve different problems and are often used together.
✓
**The app configures everything for you.** You do NOT need to add a `fragment` outbound manually: when enabled, the app inserts it and routes all direct TLS/Reality traffic through it on ANY config, leaving multi-hop chains untouched — only the hop the DPI sees gets fragmented.
## 08.1 · `s-fragment` — TLS fragmentation
## Enabling — the `s-fragment`
| Value | Effect |
|---|---|
| `absent / off` | Off (an absent header leaves the previous value unchanged) |
| `on` | On, with default parameters |
| `packets=tlshello;length=50-100;interval=10-20;maxsplit=100-200` | On, with these parameters |
Sent as the HTTP response header `s-fragment:` or as a body comment line `#s-fragment: packets=tlshello;length=50-100;interval=10-20`.
🔒
**Requires an active Provider ID.** Like any provider-controlled field, `s-fragment` is honoured only while your Provider ID is active. An inactive or unknown provider can't enable fragmentation on users' devices.
## Parameters
| Parameter | Format · default · description |
|---|---|
| `packets` | tlshello or N-M · default tlshelloWhich packets to fragment. tlshello — only the TLS ClientHello (carries the SNI); recommended, minimal overhead. 1-3 — the first 1–3 outbound packets. |
| `length` | min-max (bytes) · default 50-100Fragment size, random within the range. Smaller = harder to reassemble but more overhead. |
| `interval` | min-max (ms) · default 10-20Delay between fragments, random within the range. Jitter hinders buffering and reassembly. |
| `maxsplit` | min-max · default unsetOptional (newer cores only). Caps the number of fragments; without it, unlimited. Older cores ignore it. |
Defaults suit most networks. Tune for a specific DPI: harder blocking → smaller `length` and/or larger `interval`; slower speed → larger `length` and smaller `interval`.
Parameter names are **case-insensitive** (`maxsplit` and `maxSplit` are the same key), and the packets key is **plural** — `packets=`, not `packet=`. `packet=` belongs to `s-noise`, where it means the payload.
## ⚙️Users can enable it themselves
A user can also enable fragmentation and edit the same parameters manually — in the app: **Settings → Bypass → TLS fragmentation**, independently of the provider. When enabled manually, their parameters take priority over the header.
## 08.2 · `s-noise` — noise packets
Junk traffic sent ahead of the connection so DPI can't recognise its start. It works **alongside fragmentation** — they share the same egress, and together they cover both the start of the connection and what's inside it.
Values: `on` / `1` uses the defaults (`rand`, packet 50-100, delay 10-20); `off` / `0` disables it. Full form is `type=rand;packet=50-100;delay=10-20`, or positional `rand,50-100,10-20`; `type` accepts `rand`, `str` or `hex`.
Both spellings of the header are accepted — `s-noise` and `s-noises`.
## 08.3 · `s-resolve` — pre-resolving the server address
Resolves the server address over DoH **before the tunnel comes up**. Use it where the local DNS returns a forged answer for your node's domain: without it the client gets a wrong IP and simply never connects.
Values: `on` / `1` uses the default resolver (dnsforge.de); `off` / `0` disables it; a URL sets your own.
Where even the resolver's own domain is poisoned, add a bootstrap IP with `;ip=…` — for example `https://dnsforge.de/dns-query;ip=49.12.67.122`. The original domain stays as SNI, so TLS/Reality still validate. If the resolver doesn't answer within a second and a half the normal lookup is used — the parameter can only help, never block the connection.
**Several resolvers** can be listed, separated by commas, each with its own bootstrap IP — they are tried in order until one answers. A single address behaves exactly as before: it is the same format without commas.
```
s-resolve: https://a.example/dns-query;ip=1.2.3.4, https://b.example/dns-query;ip=5.6.7.8
```
A list beats the app's built-in set: you name the resolvers you *know* work on your users' networks, which the built-in list cannot know. It also can't be switched on by a header at all — only by the user's own toggle.
## 08.4 · `s-dns` — DNS-over-HTTPS in the tunnel
Encrypts ordinary DNS queries inside the tunnel. Values: `on` / `1` enables the built-in default resolver (dnsforge.de); a URL such as `https://dnsforge.de/dns-query` sets your own; `off` / `0` disables it. Resolved DNS travels through the tunnel, not the physical network.
**Order of application**
Provider parameters **always outrank the user's own settings**, `off` included. When the user configures it themselves, the order is: fragmentation → noise → DoH.
---
## GeoIP / GeoSite
Source: https://docs.smproxy.io/en/dev-docs/geoip-geosite/
The Geo rules the tunnel uses live **inside the imported Xray config** and are resolved against the `geoip.dat` / `geosite.dat` databases. The app ships with built-in databases and can download **updated** ones from the `geoipurl` / `geositeurl` headers (supported on iOS, Android and desktop OSes); a freshly downloaded database replaces the built-in one for the engine.
---
## WireGuard и AmneziaWG
Source: https://docs.smproxy.io/en/dev-docs/wireguard-amneziawg/
WireGuard and AmneziaWG servers behave like any others: they sit in the same list next to VLESS/Trojan/Shadowsocks and the user picks them the same way. There is no separate mode and no extra setup. It runs on the stock Xray core (a `wireguard` outbound) — nothing new is required on the provider side beyond the config itself.
## Option A — in a JSON subscription
A normal Xray config whose outbound has `"protocol": "wireguard"`:
```
{
"remarks": "🇳🇱 Netherlands — WG",
"outbounds": [{
"tag": "proxy",
"protocol": "wireguard",
"settings": {
"secretKey": "",
"address": ["10.0.0.2/32"],
"mtu": 1420,
"peers": [{
"publicKey": "",
"preSharedKey": "",
"endpoint": "example.com:51820",
"allowedIPs": ["0.0.0.0/0", "::/0"],
"keepAlive": 25
}]
}
}]
}
```
The server address shown in the app comes from `peers[0].endpoint`. WireGuard has no `vnext` or `servers` block — the endpoint string is the only place the host lives.
**AmneziaWG fields.** The same outbound accepts obfuscation parameters — add them next to the usual ones. They are **only for AmneziaWG**; for plain WireGuard leave them out entirely (any zero value is ignored and the connection behaves exactly as before).
```
{
"protocol": "wireguard",
"settings": {
"secretKey": "…",
"address": ["10.66.66.17/32"],
"mtu": 1420,
"jc": 4, "jmin": 40, "jmax": 70,
"s1": 15, "s2": 25, "s3": 0, "s4": 0,
"h1": 1111111, "h2": 2222222, "h3": 3333333, "h4": 4444444,
"peers": [
{ "publicKey": "…", "endpoint": "1.2.3.4:51820", "allowedIPs": ["0.0.0.0/0"] }
]
}
}
```
| Field | Meaning |
|---|---|
| `jc` | How many junk packets to send before the handshake (4–12) |
| `jmin / jmax` | Junk packet size bounds, bytes (about 8–80) |
| `s1 / s2` | Prefix size for the init and response packets (15–150) |
| `s3 / s4` | Prefix size for the cookie and transport packets; optional — omit when the server does not set them |
| `h1–h4` | Rewritten header types — without them the packets are recognised by signature. A single number or, for AmneziaWG 3.1 servers, a range "a-b" as a string ("h1": "1000-2000"); ranges are applied from Android 428, iOS/macOS/tvOS 2.0.10 (315) and desktop 203 — older builds accept numbers only |
⚠
`s1`–`s4` and `h1`–`h4` **must be identical on the server and in the client config**, otherwise the connection silently fails: the tunnel comes up but no traffic passes. `jc`, `jmin` and `jmax` do not have to match — each side sends its own junk.
**Server version — 3.1 and older.** The app is built on the AmneziaWG 3.1 branch and stays compatible with every earlier one, so there is nothing to upgrade on your side. Send only the parameters your server actually uses; anything you leave out keeps its default and is ignored.
| Added in | Parameters |
|---|---|
| 0.2.11 and earlier | `jc, jmin, jmax, s1, s2, h1–h4` |
| 0.2.13 | `s3, s4` |
| 0.2.16 | `i1–i5` |
| 3.0 | `header_protection_key, content_padding_addition, rekey_after_time, rekey_timeout, reject_after_time, keepalive_timeout, max_handshake_attempts` |
| 3.1 | `random_trailers, disable_cookies, ranges a-b for h1–h4 and PersistentKeepalive` |
A 3.1-only parameter sent to an older server is simply not understood by it, so keep the two sides in step: configure the client with exactly what the server runs.
## Option B — a wireguard:// link
`wireguard://` — the base64 holds an **ordinary WireGuard configuration file**, exactly the text you hand to the official client. An optional `#Name` anchor sets the displayed title:
```
wireguard://W0ludGVyZmFjZV0K…#🇳🇱%20Netherlands
```
The decoded payload is simply:
```
[Interface]
PrivateKey =
Address = 10.0.0.2/32
DNS = 1.1.1.1
[Peer]
PublicKey =
PresharedKey =
Endpoint = example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
```
This form works everywhere the other share links do: pasting into the add form, inside an encrypted `crypt1` link, or as a line in the subscription body.
**Build a link without writing code**
Paste a `.conf` into the helper page — it assembles the `wireguard://` link and a QR code right in your browser, with no outbound requests.
[Open the link builder →](https://smproxy.io/wireguard-link.html)
Keys are passed to the core as-is — the app does not re-encode them.
## Client-side import
From Android 379, iOS/macOS 2.0.3 (277+) and desktop 169 the add screen takes a WireGuard/AmneziaWG config in four ways — you can hand configs to users in any of them:
1. **A `.conf` file** — the “Choose config file” button (Android, iOS, macOS, Windows, Linux; Apple TV has no file picker — it adds configs from the phone).
2. **Raw config text** — the `[Interface]…[Peer]` body pasted straight into the add field, on every platform.
3. **A link that serves the config** — an https address returning the `.conf` text itself. Many panels hand out configs exactly this way.
4. **A scheme link or QR** — `wireguard://` / `amneziawg://` / `awg://` / `wg://` carrying base64 of the config, as described above.
AmneziaWG obfuscation parameters (`Jc`, `Jmin`, `Jmax`, `S1`, `S2`, `H1`–`H4`) are picked up from `[Interface]` in all four. Older builds accept the scheme link and QR only.
## C. AmneziaWG — WireGuard with obfuscation
**AmneziaWG** is the same WireGuard plus obfuscation: junk packets and rewritten header types that keep DPI from recognising a WireGuard handshake.
**Link schemes.** `wireguard://`, `amneziawg://`, `awg://` and `wg://` are all accepted and parsed the same way — the payload is base64 of a standard `.conf`, with an optional `#Name` fragment.
```
amneziawg://#Germany
```
**Obfuscation parameters** live in `[Interface]` of that `.conf`, next to the usual keys:
```
[Interface]
PrivateKey = …
Address = 10.66.66.17/32
MTU = 1420
Jc = 4 ; how many junk packets to send before the handshake
Jmin = 40 ; junk packet size, lower bound
Jmax = 70 ; junk packet size, upper bound
S1 = 15 ; init packet prefix size
S2 = 25 ; response packet prefix size
S3 = 0 ; cookie packet prefix size — optional, omit unless the server sets it
S4 = 0 ; transport packet prefix size — same
H1 = 1111111 ; rewritten header types — otherwise recognised by signature (3.1 servers may use a range: H1 = 1000-2000)
H2 = 2222222
H3 = 3333333
H4 = 4444444
[Peer]
PublicKey = …
Endpoint = 194.61.120.25:57932
AllowedIPs = 0.0.0.0/0,::/0
```
Leave them out and you get plain WireGuard — the app and the core behave exactly as before. The same applies field by field: send only what your server runs. Any AmneziaWG version works, 3.1 and older — see the version table in section A.
[🛠️ — **Build the link from a .conf** — The generator takes AmneziaWG configs too — obfuscation parameters carry over as-is. Everything runs in your browser. — Link generator →](https://smproxy.io/wireguard-link.html)
**Several locations in one file.** A subscription URL may return a JSON container instead of links:
```
{
"type": "amneziawg",
"version": 1,
"servers": [
{ "name": "Germany", "config": "" },
{ "name": "Netherlands", "config": "" }
]
}
```
`type` is accepted as `amneziawg`, `awg`, `wireguard` or `wg`. A malformed entry is skipped without taking the rest of the list down with it.
## Notes for operators
- **IPv6 endpoints** are supported, written the standard way: `[2606:4700:d0::a29f:c001]:2408`.
- **`keepAlive`** (seconds) — persistent keepalive. Set it if your clients are behind NAT: without it the translation expires and the tunnel goes quiet in one direction.
- **MTU in the config controls what the client sends.** What the server sends is set by the MTU of the `wg0` interface on the node — configure both, otherwise a path that can't carry full-size packets will pass the handshake and then stall.
- **Routing on the node matters:** `ip route get ` must resolve via `wg0`. If it points at the default gateway the handshake still succeeds (the daemon answers it directly) but data never reaches the client. Looks like blocking — it isn't.
---
## Deep-links
Source: https://docs.smproxy.io/en/dev-docs/deep-links-smproxy/
| Form | Meaning |
|---|---|
| `smproxy://add?url=` | Add a subscription by URL |
| `smproxy://add/ · add/` | Same, as a path |
| `smproxy://crypt1/` | Add an encrypted subscription link (URL hidden) |
| `smproxy://addhw/` | Device-bound subscription — works only on the device with a matching HWID; on others: “link belongs to another device”. |
| `smproxy://import/` | Auto-detect and import: a subscription URL, a server link (vless://, wireguard://, …), base64 of either, or a raw WireGuard/AmneziaWG .conf (INCY form) |
| `smproxy://onadd/` | Short form of routing/onadd/ — download a routing profile from the URL and activate it (INCY form) |
| `smproxy://connect · open` | Connect to the selected server (INCY form) |
| `smproxy://disconnect · close` | Disconnect |
| `smproxy://toggle` | Toggle the tunnel |
| `smproxy://status` | Just open the app |
Encryption uses a shared AES-256-GCM key; `addhw` binds a link to a single device (anti-sharing). Tapping a link goes through the deep-link handler; pasting into the add form accepts the same forms.
**Other clients' links, pasted or scanned.** The forms above are accepted with **any scheme** when the link is pasted into the add form or scanned with the in-app camera: `happ://add/`, `incy://import/`, `v2raytun://import/`, `streisand://import/`, `://install-sub?url=…`, `sub://` (Shadowrocket). Encrypted links of other clients (`happ://crypt4/`, `incy://crypt1/`) cannot be decoded — the app says so. The app does **not** claim `happ://` / `incy://` system-wide on Android, iOS or macOS; on Windows and Linux it registers them only when no other handler is installed, so an installed Happ or INCY keeps its links.
---
## Сообщения и уведомления
Source: https://docs.smproxy.io/en/dev-docs/messages-notifications/
SMProxy exclusive
A provider can send users **in-app messages**. They arrive inside the Provider ID check response and land in the app inbox (bell icon + unread counter), optionally triggering a system notification.
Each message has: `id`, `title`, `body`, optionally `date`, `type` (info/update/promo/warning), `url`, `notify` and `targeting`. Read status is stored locally per device.
## Targeting
Compose messages in the provider portal. By default a message goes to everyone; each filter below narrows that down, and they combine.
### Platform and app build
Tick the platforms that should receive the message. Leave the build field empty to reach every version of that platform, or set a condition (`<`, `≤`, `=`, `≥`, `>`) to reach only some of them.
Build numbers are **per-platform** — iOS 200 and Android 200 are unrelated releases — so a condition is always tied to a platform. Ticking nothing means “all platforms”; as soon as you tick one, the others receive nothing. That is how you reach users still on an old iOS build without touching Android:
| Ticked | Build condition · Who gets it |
|---|---|
| nothing | — · everyone |
| iOS | — · every iOS user |
| iOS | `< 200` · iOS users below build 200 |
| iOS, Android | iOS `< 200`, Android `< 300` · old builds on both, nothing elsewhere |
### Subscription domain
Pick a verified domain to reach only the users whose subscription is served from it — `*.example.com` for the domain and all its subdomains, or a single host such as `sub.example.com` for that host alone. The portal shows how many devices are currently on each host.
Requires a verified domain — see [Domain verification](#domains) (section 05a).
### A single device
Paste a HWID to send the message to exactly one device — useful when you are walking one user through a problem.
---
## Генераторы ссылок
Source: https://docs.smproxy.io/en/dev-docs/tools/
Ready-made tools on the website: no registration, everything is computed in the browser.
| Tool | What it does |
|---|---|
| [WireGuard / AmneziaWG link](https://smproxy.io/wireguard-link.html) | A `.conf` file → a `wireguard://` link ready to serve in a subscription |
| [Routing builder](https://smproxy.io/routing-link.html) | Builds a routing profile from plain fields: JSON, an `smproxy://routing/onadd/…` link and a QR code — no hand-written JSON |
| [Encrypted link](https://smproxy.io/link-crypto.html) | Hides the real subscription address inside an `smproxy://crypt1/…` link |
| [Device-bound link](https://smproxy.io/link-hwid.html) | Binds a subscription to one device — it opens only there |
The same operations are available as an API, see [Provider portal](provider-portal.md).
More about profiles in [Routing profiles](routing-profiles.md).
---
## Документация для ИИ
Source: https://docs.smproxy.io/en/dev-docs/ai-assistants/
You can connect the SMProxy documentation to an AI assistant, and it will answer questions
about headers, deep links and parameters from the real pages instead of inventing them. No
registration, the server is public.
## Connect in one command
The server address is `https://provider.smproxy.io/mcp`.
=== "Claude Code"
```bash
claude mcp add --transport http smproxy-docs https://provider.smproxy.io/mcp
```
=== "Claude Desktop"
In **Settings → Developer → Edit config**, file `claude_desktop_config.json`:
```json
{
"mcpServers": {
"smproxy-docs": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://provider.smproxy.io/mcp"]
}
}
}
```
=== "Cursor"
In `~/.cursor/mcp.json`, or `.cursor/mcp.json` inside the project:
```json
{
"mcpServers": {
"smproxy-docs": { "url": "https://provider.smproxy.io/mcp" }
}
}
```
=== "VS Code"
In `.vscode/mcp.json` inside the project:
```json
{
"servers": {
"smproxy-docs": { "type": "http", "url": "https://provider.smproxy.io/mcp" }
}
}
```
=== "Codex CLI"
In `~/.codex/config.toml`:
```toml
[mcp_servers.smproxy-docs]
command = "npx"
args = ["-y", "mcp-remote", "https://provider.smproxy.io/mcp"]
```
=== "Windsurf"
In `~/.codeium/windsurf/mcp_config.json`:
```json
{
"mcpServers": {
"smproxy-docs": { "serverUrl": "https://provider.smproxy.io/mcp" }
}
}
```
Restart your editor after connecting.
## What the server can do
| Tool | What it does |
|---|---|
| `search_docs` | Search the documentation, with snippets around each match |
| `get_page` | One page in full, as Markdown |
| `list_pages` | Table of contents: every page with its section and description |
The default language is Russian; pass `lang=en` for English.
## What to ask
- "which subscription response headers does SMProxy support?"
- "what stops working without an active Provider ID?"
- "show me the deep-link format for importing a subscription"
- "which AmneziaWG version introduced the i1–i5 parameters?"
The last one doubles as a check: the correct answer is **0.2.16**. That fact is not in a
model's general knowledge, so a correct answer means the assistant is really reading the docs.
## Without MCP
If your assistant has no MCP support, point it at the whole documentation in one file:
```
https://docs.smproxy.io/llms-full.txt
```
A page index lives at [llms.txt](https://docs.smproxy.io/llms.txt). Any page is available as
Markdown by adding `.md` to its address.
---
## Кабинет провайдера
Source: https://docs.smproxy.io/en/dev-docs/provider-portal/
The portal at **[provider.smproxy.io](https://provider.smproxy.io/provider/register)** is
self-service: registration, your Provider ID, usage statistics, in-app messages and link
generators. It is free and takes a minute to set up.
[Create an account](https://provider.smproxy.io/provider/register) ·
[Sign in](https://provider.smproxy.io/provider/login)
## What you get
- **Provider ID** — your identifier; you can set a custom one instead of the generated value.
- **Statistics** — how many devices use your subscriptions.
- **Messages** — in-app broadcasts, see [Messages & notifications](messages-notifications.md).
- **Link generators** — encrypted and device-bound links, see [Link generators](tools.md).
- **Domain verification** — statistics without a Provider ID, see [Domain verification](domain-verification-statistics-without-a-provider-id.md).
Portal sections change along with the app — the current list is always in the portal itself.
## Statistics in three steps
1. Register.
2. Copy your Provider ID, or set your own.
3. Send it with the subscription as the `providerid` or `s-providerid` response header.
Your users' apps then start filling in the statistics you see in the portal. More about the
identifier in [Provider ID](provider-id.md), and about what it unlocks in
[What needs an active Provider ID](what-needs-an-active-provider-id.md).
---
## Provider ID
Source: https://docs.smproxy.io/en/dev-docs/provider-id/
Provider ID links a subscription to a **provider account** so the provider can see **usage statistics** for their subscription. It **restricts nothing** — the app works fully without it. Free.
**How to send it (any method)**
1. URL query — `…?providerid=`
2. Comment in the subscription body — line `#providerid `
3. HTTP response header — `providerid:` or `s-providerid:`
If several are provided, the **HTTP header** takes priority. You get your Provider ID (and the option to set a custom one) in the provider portal — see section 08.
✓
Without a Provider ID the subscription still works fully; the provider just gets no statistics. What **does** depend on an active Provider ID is listed below — the same tables are repeated at the [end of this page](#gating) for quick reference.
## 🔒Requires an active Provider ID
| Area | Keys |
|---|---|
| Bypass | `s-fragment` `s-noise` `s-noises` `s-resolve` `s-dns` — and their INCY/Happ equivalents `fragmentation-*` `noises-*` `server-address-resolve-*` |
| Migration & fallbacks | `new-url` `new-domain` `fallback-url` `fallback-domains` |
| Provider branding | `s-siteurl` `s-sitename` `s-tgbot` `x-tgbot` `sort-order` `serverDescription` |
| Banner & contacts | `banner-text` `banner-button-text` `banner-button-url` `banner-bg-color` `banner-button-color` `announce-url` `support-email` |
| Expiry UI | `sub-expire` `s-showexpire` `sub-expire-button-text` `sub-expire-button-link-site` `sub-expire-button-link-tg` `notification-subs-expire` |
| Access | `hide-settings` |
## ✓Works with or without a Provider ID
| Key | Note |
|---|---|
| `profile-title / s-title` | Subscription name |
| `profile-update-interval` | Auto-refresh interval |
| `profile-web-page-url` | Provider web page |
| `announce` | The announcement text itself — but `announce-url` (making it clickable) needs an active provider |
| `support-url` | Support link |
| `subscription-userinfo` | Quota and expiry date |
| `subid` | Stable subscription id |
| `routing` `autorouting` `routing-update-url` `routing-update-interval` `routing-profile` | Routing profiles, including the deep-link command forms — a profile carries rules only, so it needs no active provider |
| `geoipurl / geositeurl` | Geo database addresses — subscription response only |
The same gating applies however the value arrives — an HTTP header, a `#key:` line in the body, or the provider account.
---
## Подтверждение домена
Source: https://docs.smproxy.io/en/dev-docs/domain-verification-statistics-without-a-provider-id/
Statistics only work when the app knows your Provider ID. If a build does not send it — an older release, a client you do not control, a config imported by hand — those check-ins land nowhere and you lose sight of your own users.
Verify the domain your subscriptions are served from, and check-ins **without** a Provider ID are attributed to you automatically, based on the subscription URL the app reports.
**How to verify**
1. Open [provider.smproxy.io/provider/domains](https://provider.smproxy.io/provider/domains?lang=en) and add your domain. You can paste a full subscription URL — the domain is extracted for you.
2. Add the TXT record shown on the page.
3. Press **Check**. DNS does not propagate instantly — if the first attempt fails, wait a couple of minutes and try again.
```
Name: smproxy-verification.your-domain.com
Value: 8f3a91c24b774c1e9a025e6d1f0b7c33
```
☁️
**Cloudflare shortcut.** If your DNS is on Cloudflare, press **Add via Cloudflare** instead. You are taken to Cloudflare's consent screen, we create the TXT record for you, and the domain is verified straight away — no copying required. We ask for DNS access only, and the record stays in place afterwards.
## What it covers
Subdomains are included. Verifying `example.com` covers `sub.example.com`, `eu.sub.example.com` and any other depth — you do not need to add them separately.
Matching is done on the registrable domain, so multi-level suffixes behave correctly: `example.co.uk` is a domain you can own, `co.uk` is not, and `evil-example.com` never matches `example.com`.
A domain belongs to one provider. If someone else has already verified it, adding it again is refused.
## Priority — Provider ID wins
| Check-in contains | Attributed to |
|---|---|
| A Provider ID that exists | That provider — the domain is not consulted |
| No Provider ID, subscription URL on a verified domain | The domain's owner |
| A Provider ID we do not know, URL on a verified domain | The domain's owner |
| Neither | Nobody; the check-in is marked unattributed |
Domain verification never overrides an explicit Provider ID — it only fills the gap when one is missing or unusable.
## Where the domain shows up
- **Statistics** — a *Subscription domains* breakdown showing how many devices use each host, and the host under each device's `subid`. Full hosts are shown, so you can tell `sub.example.com` from `sub3.example.com`.
- **Messages** — you can target a message at a domain: `*.example.com` for the domain and all its subdomains, or a single host for just its users.
Only the host is ever displayed — subscription tokens in the URL are never shown.
---
## Перевод на другой домен
Source: https://docs.smproxy.io/en/dev-docs/moving-users-to-another-domain/
Your subscription domain gets blocked, or you simply move to a new one. Both cases are handled from the portal — you do not have to change anything in your panel, and users do not have to re-add their subscription.
Both rules are keyed on the **domain the request arrived at**, so you can run several at once — one per subdomain — and remove them individually.
## Migration — `new-domain`
Open [provider.smproxy.io/provider/domains](https://provider.smproxy.io/provider/domains?lang=en), press **Move** next to a verified domain and pick:
- **From** — `*.example.com` to move subscriptions from the whole domain including its subdomains, or a single host such as `sub.example.com` to move only that one.
- **To** — the new host. It has to be verified in the same account, so nobody can point your users at a domain they do not own.
From then on, every subscription request that arrives at the old host gets a `new-domain` header, and the app rewrites the address itself. Each user keeps their own path and token:
```
old.example.com/sub/abc123 → new.example.com/sub/abc123
```
⛔
**Chains are refused.** If `b.example.com` already has a migration of its own, you cannot make it the destination of another one — users would be handed an address that immediately moves again.
## Backup hosts — `fallback-domains`
Press **Backups** next to a domain and list the hosts to fall back to, one per line. Every entry must be a verified domain of yours. The list is sent as `fallback-domains` for requests to that domain, and the app walks it in order if the main host stops answering.
Use a migration when you have actually moved; use backups when the main address may become unreachable and you want the app to find its own way through.
## Two delivery channels
Both rules reach the app in two independent ways:
- **Subscription response headers** — `new-domain` / `fallback-domains`, as described above. Requires the request to still reach one of your domains.
- **The app's own service channel** — the app also picks up the `new-domain` / `fallback-domains` you configured for your domain over its own service connection, without fetching the subscription. They are applied on the next subscription refresh: `new-domain` re-points the stored URL permanently, backup hosts are saved and tried whenever the main host stops answering.
The second channel matters when your subscription domain is **already unreachable**: it runs over the app's own service endpoints, so existing users still receive the migration or the backup list even though they cannot fetch the subscription itself.
Requires an app build from late August 2026 or newer; older builds keep using headers only.
---
## Что где настраивается
Source: https://docs.smproxy.io/en/settings/index/
SMProxy has two independent layers of configuration — don't confuse them. The **provider's** configuration always owns routing, chains, balancers and DNS rules. **App settings** only adjust how the client behaves around that configuration.
[](index.md)
| Layer | Controlled by · documented in |
|---|---|
| **Subscription / routing / branding** | The **provider**, via subscription response headers and the config body — see the developer docs. |
| **App settings** | The **user**, in the app's Settings screen — this page. |
---
## Подключение
Source: https://docs.smproxy.io/en/settings/connection/
## Run mode — VPN or proxy only
**Android, Windows, Linux**
| Mode | Behaviour |
|---|---|
| **VPN** (default) | A system tunnel captures **all** device traffic. |
| **Proxy only** | **No tunnel is created.** The app only runs local SOCKS/HTTP proxies; only apps explicitly pointed at those ports go through the connection. |
Proxy-only mode is useful when another VPN or an MDM profile already owns the system VPN slot, or when only one application should be routed. It requires SOCKS or HTTP (below) to be enabled — otherwise the app refuses to start with an explanatory message rather than showing a misleading “connected” state.
**Not available on iOS, tvOS and macOS.** On Apple platforms the Xray core runs inside a Network Extension, which the system keeps alive only together with a VPN tunnel — remove the tunnel and no process is left to hold a local port. iOS also gives apps no way to set a system proxy. Local SOCKS/HTTP proxies **do** work on Apple platforms, but only while the tunnel is up (section 03).
## Other connection settings
### MTU
1280 · 1280–1500
**All platforms**
Leave at the default unless you have a reason to change it. Higher isn't automatically better: at 1500, large downloads can stall completely on some networks, because oversized packets are silently dropped rather than fragmented.
### Preferred IP type
auto
**All platforms**
auto / ipv4 / ipv6. On auto the app routes IPv6 into the tunnel only when the device genuinely has IPv6 upstream — advertising IPv6 with no real upstream makes apps prefer AAAA records and then fail on anything routed directly.
### Kill switch
off
**All platforms**
When enabled, disconnecting is blocked while the tunnel is active, so traffic can't silently fall back to the open network.
### Silent reconnect
on
**Android, iOS, tvOS, macOS**
Restores the connection after an unexpected drop, without notifications (still written to the connection log). An explicit Connect always takes over the VPN slot even from another VPN app; a silent reconnect deliberately does not — if another VPN took the slot, SMProxy stands down.
### Auto-connect
off
**All platforms**
Connects to the last used server when the app starts. Separate switches connect automatically on Wi-Fi or mobile data — see section 03d.
### Launch at boot / login
off
**Android · macOS · Windows · Linux**
On Windows this requires the signed build (section 06). Not available on iOS and tvOS — the system doesn't permit apps to launch themselves.
## “The server is not answering the handshake”
**WireGuard / AmneziaWG · Android 400, iOS / macOS / tvOS 295, desktop 182 and later**
When a WireGuard or AmneziaWG tunnel is up but the server stays silent, the main screen shows a banner: *“The server is not answering the handshake — still retrying. If it never connects, contact your provider: keys, Endpoint and AmneziaWG parameters must match the server.”*
- Appears about 20–30 seconds after connecting; the app keeps retrying in the background.
- Disappears on its own as soon as real traffic starts flowing, on disconnect, or when you switch servers.
- The check is passive: the app reads its own traffic counters and never sends probe requests.
- If the banner never goes away, the config does not match the server — wrong keys, Endpoint, or AmneziaWG obfuscation parameters (`S1`/`S2`, `H1`–`H4`, `Jc`…). Ask your provider for a corrected config.
---
## Быстрое переключение
Source: https://docs.smproxy.io/en/settings/quick-toggle/
Turn the tunnel on and off without opening the app. On both platforms you add the control yourself, once.
🔲
## Quick Settings tile
**Android 7 and later**
A tile in the notification shade: swipe down and tap to connect or disconnect. It shows the current state, and you never leave the app you're in. The tile doesn't appear on its own — add it once by editing your shade.
**Where to add it**
Pixel / stock Android — swipe down fully, then the ✏️ pencil
Samsung — swipe down twice, then ⋮ → Edit buttons
Xiaomi — swipe down, then ⚙️ or Sort
OnePlus / Oppo — swipe down, then pencil or ⋮ → Edit tiles
🔘
## Widget
**iOS 17 and later**
A widget with a single button, on the home screen or in Control Centre. Tap to connect or disconnect without opening the app. Connect from the app at least once first — that's when iOS asks permission for the VPN profile. The widget's state can lag a second or two behind.
**Where to add it**
Home screen — touch and hold an empty area, tap +, find SMProxy
Control Centre — swipe down from the top-right corner, tap +, then Add a control
## Connect automatically on Wi-Fi or mobile
**Android, iOS · both switches off by default**
Two switches in settings — “on Wi-Fi” and “on mobile data”. Turn on the ones you want, and the VPN comes up by itself in those networks and goes down in the rest. Disconnecting by hand is always respected: the tunnel stays down until the network changes or you connect again yourself.
| Platform | What to expect |
|---|---|
| **iOS** | The system applies the rules, so it works even when the app isn't running. Switching networks is handled for you: leave home and the tunnel comes up on mobile data, come back to Wi-Fi and it goes down again. A manual disconnect lasts until the network type changes — reconnecting to the same Wi-Fi doesn't count as a change. You can also mark trusted Wi-Fi networks by name and stay off the tunnel there. |
| **Android** | The app watches the network, so it works while the app is running. A manual disconnect lasts until the connection changes — switching Wi-Fi off and on again is enough. If the system unloads the app, auto-connect resumes on the next launch; for a guaranteed always-on tunnel use Always-on VPN in Android settings. |
Using another VPN alongside SMProxy? On iOS the rules belong to the system, so if you connect a different VPN while SMProxy isn't running, a network change can hand the slot back to SMProxy. Turn these switches off if you rely on another VPN.
---
## Локальный прокси
Source: https://docs.smproxy.io/en/settings/local-proxy/
**All platforms.** Both proxies listen on `127.0.0.1` only — never on the local network.
| Setting | Default · notes |
|---|---|
| **SOCKS5 proxy** | off — port 1080 |
| **HTTP proxy** | off — port 8080 |
| **Username / password** | empty — one credential pair shared by both proxies. Both fields must be set for authentication to apply; leave empty for no authentication. |
Ports are configurable and persist when the proxy is toggled off. Defaults sit outside the `10808–10810` range on purpose, because subscription configs commonly use those ports for internal chain hops — a collision would silently drop the inbound. On Apple platforms these proxies exist only while the tunnel is connected.
---
## DNS
Source: https://docs.smproxy.io/en/settings/dns/
**All platforms.** These live in their own **DNS** section in Settings — they apply in ordinary VPN mode too, not only when the local proxy is in use.
### Local DNS
off · port 10853
**All platforms**
Resolves DNS inside the tunnel instead of through the system resolver. A subscription's own DNS section always takes precedence.
### DNS leak protection
on
**All platforms**
Captures every DNS query the OS or an app makes (ports 53 and 853) into the tunnel, so lookups can't slip out to your ISP's resolver over the physical connection. Leave it on unless a specific setup needs the system resolver reachable directly.
### DNS-over-HTTPS (DoH)
off
**All platforms**
Resolves DNS over an encrypted HTTPS connection (dnsforge.de by default) instead of plaintext UDP, so the network can't read or tamper with your lookups. You can point it at any DoH endpoint; the query goes through the tunnel, not the physical network. A subscription's own DNS section always takes precedence; a provider can also enable DoH via a subscription header, and your toggle overrides it locally.
### Fallback DoH resolvers
off
**All platforms**
If the resolver your provider specified doesn't answer, the address is looked up through a small built-in list of resolvers instead (dnsforge.de, 42l.fr, ffmuc.net and others — all known to work with the app). Off by default: with it off, a resolver that doesn't answer simply falls back to your system DNS.
Turn it on if your network blocks the resolver your provider uses.
**Which resolver is used, in order:**
the provider's `s-resolve` header → the `RemoteDNSDomain` field of the active routing profile → your own setting → the built-in fallback list (if enabled) → your system DNS.
---
## Мультиплексирование
Source: https://docs.smproxy.io/en/settings/multiplexing/
Multiplexing carries several logical connections over one physical connection to the server. It can reduce handshake overhead, but it is not automatically better — leave both toggles off unless you have a reason.
★
**Mux and XMUX are independent mechanisms**, which is why there are two switches. Turning Mux off does not disable XMUX, and vice versa.
## Mux
**All platforms · default off · works with any transport**
| Parameter | Default · meaning |
|---|---|
| **TCP connections** | 8 — concurrent streams per connection (−1 to 1024) |
| **XUDP connections** | 8 — concurrent UDP streams (−1 to 1024) |
| **QUIC handling** | reject — what happens to QUIC inside the Mux tunnel: reject / allow / skip |
## XMUX
**All platforms · default off · applies only to the XHTTP transport**
On any transport other than XHTTP it does nothing.
| Parameter | Default · meaning |
|---|---|
| **Streams per connection** | 16-32 — how many streams share one HTTP connection (a range is valid) |
| **Max connections** | 0 — upper bound on connections; 0 means unlimited |
If the subscription specifies its own multiplexing settings, those always take precedence over these toggles.
---
## Раздельная маршрутизация
Source: https://docs.smproxy.io/en/settings/split-routing/
Decide what actually goes through the tunnel and what goes straight out — by address, and on Android by app. Both lists work the same way: the list itself is just a list, and a **mode** decides what it means.
## By address
**All platforms · default “bypass list”**
Add domains, IP addresses and subnets (one field takes both a single address like `1.2.3.4` and a subnet like `10.0.0.0/8`), then pick a mode:
| Mode | What it does |
|---|---|
| **Off** | The list is not applied — everything goes through the tunnel. The provider's own routing rules still apply. |
| **Bypass list** | Everything goes through the tunnel except the entries you list — those connect directly. |
| **Only list** | Only the entries you list go through the tunnel. Everything else connects directly. |
The default matches how the old exclusion list behaved, so nothing changes for existing users after an update.
⚠
**Asterisks don't work.** `*.example.com` is treated as literal text and simply never matches. To cover a domain together with its subdomains, write `domain:example.com`.
### How domain entries match
| You write | What it matches |
|---|---|
| `example.com` | A substring match — covers example.com and its subdomains, but can also catch unrelated hosts that contain the same text. |
| `domain:example.com` | The domain and all its subdomains — this is what people mean by an asterisk. |
| `full:example.com` | That exact name only, no subdomains. |
| `regexp:…` | A regular expression, for cases the forms above don't cover. |
**“Bank and government sites direct, everything else over VPN”**
Mode: bypass list — add domain:yourbank.com to the list.
**“Only one service over VPN, the rest as usual”**
Mode: only list — add just that service's domains, as domain:service.com.
## By app
**Android · default “off”**
Choose which apps use the tunnel and which go around it, with the same three modes:
| Mode | What it does |
|---|---|
| **Off** | Per-app routing is not applied; the tunnel covers every app. |
| **Bypass list** | Every app goes through the tunnel except the ones you pick — those connect directly. |
| **Only list** | Only the apps you pick go through the tunnel. Everything else connects directly. |
The picker shows each app's icon, name and package name, with search and a “show system apps” toggle — user apps only by default, system apps marked with an asterisk. On iOS, Mac and Windows, routing by address covers the same need using addresses instead of apps.
**Worth knowing**
- Changing the mode or the list takes effect after reconnecting — the app tells you instead of restarting the tunnel behind your back.
- In “only list” mode with an empty list, nothing goes through the tunnel at all and the VPN is effectively off. The app warns about this in the interface.
- Whatever the mode, the app's own service addresses and the hosts of your subscriptions always stay in the tunnel — otherwise a subscription refresh would leave over the direct connection. You don't need to add them by hand.
---
## Профили маршрутизации
Source: https://docs.smproxy.io/en/settings/routing-profiles/
**All platforms · default off (none selected)**
A **routing profile** is a complete rule set — what goes straight out, what goes through the tunnel and what is blocked, plus optional DNS. Where the routing rules above are a short list of addresses you keep by hand, a profile is a ready-made set, usually supplied by your provider.
**Where profiles come from.** You import one by a link (from a message or a QR code), or it arrives with your subscription. A profile is only *added* to the list — switching it on stays your decision.
**Choosing one.** Settings → Routing → *Routing profiles*: pick a profile, or **Off** to use none. A profile marked ↻ updates itself from its source on its own schedule.
**Per subscription.** A subscription can use its own profile regardless of the global choice — set it while editing that subscription:
| Choice | Effect |
|---|---|
| **Use the global one** | The profile selected in Settings applies (default) |
| **Do not apply** | No routing at all for this subscription, even if a global profile is on |
| **A profile name** | That profile applies whenever you connect to this subscription's servers |
The routing rules above keep working independently of profiles. The profile format and how providers hand them out are in the [developer docs → routing profiles](index.md).
---
## Обход блокировок
Source: https://docs.smproxy.io/en/settings/censorship-bypass/
## TLS fragmentation
**All platforms · default off**
Splits the TLS handshake so DPI systems can't match the SNI in a single packet. A provider can also drive it for its own users via a subscription header, and that value wins over your toggle — including when the provider turns it off. Your toggle applies only when the provider sends nothing. Defaults suit most blocking setups — change them only if a specific network needs it.
| Parameter | Default · meaning |
|---|---|
| `packets` | tlshello — which packets to fragment |
| `length` | 50-100 — fragment size range, bytes |
| `interval` | 10-20 — delay between fragments, ms |
| `maxSplit` | empty — optional cap on the number of fragments |
Full technical reference and the provider-side header are in the [developer docs → TLS fragmentation](index.md).
## Noise packets
**All platforms · default off**
Sends junk data before the handshake so DPI can't recognise the start of the connection. Works alongside fragmentation — the two share the same egress and don't conflict.
| Parameter | Default · meaning |
|---|---|
| `Type` | rand — Junk content: rand, str or hex |
| `Packet` | 50-100 — Junk packet size range, bytes |
| `Delay` | 10-20 — Delay before the handshake, ms |
⚠
As with fragmentation, **the provider's setting takes precedence over yours** — including when the provider turns it off. That's deliberate: noise has to match what the server expects.
## Server address over DoH
**All platforms · default off**
Looks up the server's address over HTTPS **before** connecting. Use it where the local DNS returns a forged answer for the server's domain — without it the app would get a wrong address and never connect.
| Parameter | Default · meaning |
|---|---|
| `DoH resolver` | dnsforge.de — Which resolver to ask |
| `Resolver IP` | empty — Address of the resolver itself, for networks where even its domain is poisoned |
If the resolver doesn't answer within a second and a half the normal lookup is used — the setting can only help, never block a connection. The provider's setting takes precedence over yours.
---
## Подписки
Source: https://docs.smproxy.io/en/settings/subscriptions/
**All platforms.**
| Setting | Default · what it does |
|---|---|
| **Automatic refresh** | on — Reloads your subscriptions in the background so new servers and expiry dates stay current |
| **Refresh interval** | 1 hour — How often that happens. Your value overrides whatever interval the provider suggested |
| **Refresh on open** | off — Also reloads every subscription each time you open the app |
| **Collapsible subscriptions** | on — Lets you fold a subscription's server list in the main screen. Groups start expanded; which ones you collapsed is remembered |
| **Refresh all subscriptions** | — — Reloads everything right now, ignoring the interval |
A subscription is also refreshed regardless of these settings when it looks expired, so a renewal on the provider's side is picked up rather than reported as “expired”.
🏷
Each server can carry a short caption from the provider, shown under its name in place of the protocol label (for example `Netflix / streaming` instead of `VLESS | TCP | Reality`). This only appears for subscriptions whose provider is active.
---
## Приватность, данные и внешний вид
Source: https://docs.smproxy.io/en/settings/privacy-data-appearance/
### Diagnostics
off · opt-in
**iOS, tvOS, macOS only**
Apple platforms ask for consent before sending any diagnostic check-in. This gate is intentionally Apple-only, to satisfy App Store requirements.
### Traffic statistics
off
**All platforms**
Counts uploaded/downloaded bytes locally. Nothing is transmitted.
### Signature check
off
**All platforms**
Requires subscriptions to carry a valid provider signature; unsigned subscriptions are rejected.
### Logs
**All platforms**
Grouped into tabs — connections, subscriptions and statistics (plus per-connection core logs) — each viewable, copyable and clearable in the app. Trimmed automatically (oldest first) and never grow without bound. Nothing is uploaded.
### Theme
System
**All platforms**
System (default), light, or dark.
### Language
**All platforms**
Russian and English. Follows the system language and can be changed in-app; the interface updates immediately.
### Reset
**Android, iOS, tvOS, macOS**
Removes every subscription, setting and log, returning the app to a freshly installed state.
## Log actions
Every individual log has its own actions, so you don't have to wipe everything to get rid of one entry.
| Action | What it does |
|---|---|
| **Copy** | Puts the log text on the clipboard |
| **Share** | Hands the log to the system share sheet as a .log file, so you can send it via any app — messenger, mail, cloud storage. On Windows and Linux there is no share sheet, so it saves the file wherever you choose instead |
| **Clear this log** | Deletes just this one log, leaving the rest untouched |
Sharing sends a file rather than pasted text, so long logs arrive as an attachment instead of an unreadable wall of text. Logs are shared only when you choose to — the app never sends them anywhere on its own.
---
## Системные требования
Source: https://docs.smproxy.io/en/settings/system-requirements/
| Platform | Minimum | Architectures | Distribution |
|---|---|---|---|
| **iOS** | iOS 16 | arm64 | App Store |
| **iPadOS** | iPadOS 16 | arm64 | App Store |
| **macOS** | macOS 13 Ventura | Apple Silicon + Intel | App Store |
| **tvOS** | tvOS 17 | arm64 | App Store |
| **Android** | Android 8.0 (API 26) | arm64-v8a, armeabi-v7a | Google Play + direct APK |
| **Android TV** | Android 8.0 (API 26) | arm64-v8a, armeabi-v7a | Google Play + direct APK |
| **Windows** | Windows 10 or 11 (64-bit) | x64 | Signed installer (.exe) |
| **Linux** | glibc-based distros | x86_64, aarch64 | .deb, .rpm |
- Windows installs a TUN driver and therefore requires administrator rights during installation.
- Windows 7, 8 and 8.1 are not supported, nor is 32-bit Windows. The app is built on Electron 33, which runs only on Windows 10 and newer, 64-bit; on an older or 32-bit system the executable is not even recognised as a Windows application. From desktop build 202 the installer checks this first and stops with a clear message instead of installing a copy that cannot start.
- Linux packages install a small root helper service, so connecting doesn't prompt for a password every time. Proxy-only mode needs no root at all.
- Android requires the system VPN permission, granted once on first connect. Devices with aggressive battery management may need SMProxy excluded from optimisation for the tunnel to survive long background periods; the always-on option makes this more robust.
## Code signing
| Platform | Signature |
|---|---|
| **Windows** | Signed with an OV code-signing certificate issued to RB Media FZC LLC by Certum. The installer and every bundled executable are signed and timestamped, so Windows shows a named publisher instead of “Unknown publisher”, and SmartScreen/Defender warnings are correspondingly reduced. |
| **macOS, iOS, tvOS** | Signed and notarised by Apple; distributed through the App Store. |
| **Android** | Signed with the developer key; Play builds additionally re-signed by Google Play. |
| **Linux** | Packages are not signed; verify downloads against the checksums published with each release. |
---
## Чего SMProxy не делает
Source: https://docs.smproxy.io/en/settings/what-smproxy-does-not-do/
- No servers, no VPN service — SMProxy is a client. Connectivity comes entirely from the subscription the user imports.
- No traffic logging — Statistics are byte counters kept on the device.
- No account — Nothing to register; the subscription is the only credential.
- Everything is free — No paid tier, no gated features.
Everything above reflects what the apps do today. Defaults and platform availability may change as the apps evolve.
---