Skip to content

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 →

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/<base64\|url> Import the profile, don't switch to it
smproxy://routing/onadd/<base64\|url> Import and activate
smproxy://autorouting/add/<url> · …/onadd/<url> Same, plus keep it updated from that URL
smproxy://routing/off Turn routing off

The payload may also be passed as ?data=<base64>. 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/<base64\|url> Import the profile into the list, don’t assign it
*://routing/onadd/<base64\|url> Import and assign to this subscription
*://autorouting/add/<url> · …/onadd/<url> 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/<base64>-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.