Skip to content

feat: Tailscale override (proxy node + MagicDNS routing + UI) - #811

Open
yourtion wants to merge 3 commits into
MetaCubeX:mainfrom
yourtion:feat/tailscale-override-pr
Open

feat: Tailscale override (proxy node + MagicDNS routing + UI)#811
yourtion wants to merge 3 commits into
MetaCubeX:mainfrom
yourtion:feat/tailscale-override-pr

Conversation

@yourtion

@yourtion yourtion commented Aug 3, 2026

Copy link
Copy Markdown

Summary

Adds a first-class Tailscale override to the override settings screen. Flipping the switch creates a Tailscale proxy node + a select group + routing rules + DNS wiring, so tailnet FQDNs (*.ts.net) and CGNAT IPs (100.64.0.0/10) reach the user's tailnet through MagicDNS without manual subscription editing.

Why

CMFA already bundles the tailscale outbound type (mihomo's tsnet integration), but wiring it up today means hand-editing the subscription YAML — proxy node, proxy-group, rules, and critically the nameserver-policy that points *.ts.net at the tailscale:// DNS transport. The latter is easy to miss: without it, MagicDNS FQDNs silently fall through to the system DNS and fail to resolve. This PR automates all of it from the UI.

What it does

Layer Change
Kotlin model New flat ConfigurationOverride.Tailscale — a single enabled toggle + optional fields (authKey, hostname, controlUrl, stateDir, exitNode, ipCidrs)
Go (kernel) patchTailscale expands the override into a fixed-name proxy Tailscale + group Tailscale-Group + prepended rules; patchDns injects the nameserver-policy and fake-ip-filter entries
UI / i18n New Tailscale category in override settings, with all fields gated on the enabled switch. 8 locales

Design choices (and the bugs they avoid)

  • Fixed node/group names (Tailscale / Tailscale-Group), not user-configurable. A user-supplied name could collide with mihomo built-ins (DIRECT, REJECT, … → "duplicate name" load error) or embed / (which url.Parse splits into Host/Path, desyncing the DNS lookup key from the transport registration). Constants make both classes of bug structurally impossible.
  • enabled is a single boolean, not "any field non-empty". Tailscale supports a fully-default node (interactive login, persistent state dir, auth-key cleared after first use). Inferring "enabled" from non-empty fields would drop the node exactly when the user wants to keep it running with defaults.
  • Both fixed names are cleared from both cfg.Proxy and cfg.ProxyGroup before append. mihomo registers proxies and groups in one shared map, so a subscription-defined group named Tailscale would clash with our proxy even though we only append a group named Tailscale-Group. Clearing both names from both slices covers every cross-category collision.
  • nameserver-policy for +.ts.net is always overwritten. If the subscription already has a +.ts.net policy pointing elsewhere, keeping it would silently leave MagicDNS broken — the opposite of what enabling the feature promises.
  • fake-ip-filter entry is shaped per FakeIPFilterMode: +.ts.net for blacklist (default), nothing for whitelist (the policy short-circuits before fake-ip), and DOMAIN-SUFFIX,ts.net,real-ip for rule mode (a bare suffix fails parseFakeIPRules).
  • Short names (nas) are intentionally not supported. tsnet's QueryDNS does not append the MagicDNS search suffix, so only full *.ts.net FQDNs (or IPs) resolve. This is documented in the code comments.

Commits

  1. feat(core): add Tailscale override model — Kotlin data class
  2. feat(core): inject Tailscale proxy, routing and DNS via override — Go expansion + DNS wiring
  3. feat(design): add Tailscale override UI — settings screen + strings

Verification

  • gofmt clean, go build -tags "android cmfa" + go vet pass
  • Full app:assembleAlphaRelease succeeds (all 5 ABIs)
  • Manually tested on device: enabling the switch with an auth-key creates the node, resolves *.ts.net via MagicDNS, and routes traffic to the Tailscale-Group
Screenshot_2026-08-03-22-29-45-302_com github metacubex clash alpha

Notes for reviewers

  • The override is single-instance (one Tailscale node per profile). The model is a flat object, not a list.
  • routingEnabled was intentionally folded into enabled after iteration — the two-stage toggle added complexity without enabling any real use case that a single switch doesn't cover.

Add a flat ConfigurationOverride.Tailscale data class that the Kotlin
app serializes into the override JSON. A single 'enabled' toggle
controls creation (allowing a fully-default node, e.g. after clearing a
one-time auth-key or relying on tsnet interactive login). The node and
group names are fixed constants on the kernel side, so there is no
user-configurable name.

Fields: enabled, hostname, authKey, controlUrl, stateDir, exitNode,
ipCidrs. Kept as a plain @serializable so it rides through the Android
Parcel (IPC) path correctly.
Expand the override 'tailscale' block into a proxy node, a select group,
prepend rules and DNS wiring, all on the kernel side so the Kotlin model
stays a plain flat object.

Node and group names are fixed constants ('Tailscale' / 'Tailscale-Group')
to keep them URL-safe in the 'tailscale://<name>' nameserver-policy and
free of collisions with mihomo built-in names. Before appending, any
same-named proxy/group already present in the subscription is removed
from BOTH cfg.Proxy and cfg.ProxyGroup, since mihomo registers proxies
and groups in one shared map.

DNS wiring:
- nameserver-policy '+.ts.net' -> 'tailscale://Tailscale' (always
  overwritten, so enabling the feature owns MagicDNS resolution)
- fake-ip-filter entry shaped per FakeIPFilterMode: '+.ts.net' for
  blacklist, nothing for whitelist (policy short-circuits), and
  'DOMAIN-SUFFIX,ts.net,real-ip' for rule mode (a bare suffix would
  fail parseFakeIPRules).

Routing rules prepend DOMAIN-SUFFIX,ts.net and the CGNAT range
100.64.0.0/10 (overridable via ipCidrs) to the Tailscale-Group.
Add a Tailscale category to the override settings screen with an
'enabled' switch and editable fields for auth-key, hostname, control
URL, state directory, exit node and route IP CIDRs. All fields below
the switch are disabled until it is turned on.

String resources added for all 8 locales (en, zh, zh-rTW, zh-rHK, ja,
ko, ru, vi).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant