Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Releases from v2.0.0 on are signed with a new key, so Android refuses to install
- Backups in the desktop's `.thrbackup` format, restorable on either side: save, share or sync over WebDAV
- Route profiles shared with desktop, preloaded from [routeprofiles](https://github.com/throneproj/routeprofiles), plus Android-only rule conditions: apps, Wi-Fi network (SSID / BSSID), network type and metered networks
- Auto Selector profiles that keep the best server of one or more groups ranked and switch automatically
- Cloudflare WARP: registration (WireGuard or MASQUE identity) and a built-in WARP mode for routing
- Cloudflare WARP: WireGuard, AmneziaWG or MASQUE at entry, exit, or both ends of the selected connection
- URL, IP / country and speed tests, with a bulk-test panel (live progress, latency histogram, fastest servers, sort / remove unavailable / connect to fastest)
- The sing-box dashboard built in (live traffic, connections, logs)
- JSON editor with sing-box schema checking, completion and formatting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@ object DataStore : OnPreferenceDataStoreChangeListener {
// warp
var enableWarp by SettingsRegistry.ENABLE_WARP
var warpMode by SettingsRegistry.WARP_MODE
var warpChainOrder by SettingsRegistry.WARP_CHAIN_ORDER
var warpEntryConfig by SettingsRegistry.WARP_ENTRY_CONFIG
var warpEp by SettingsRegistry.WARP_EP
var warpPrivateKey by SettingsRegistry.WARP_PRIVATE_KEY
var warpPublicKey by SettingsRegistry.WARP_PUBLIC_KEY
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ object SettingsMapper {
directTestUrl = DataStore.directTestUrl,
enableWarp = DataStore.enableWarp,
warpMode = DataStore.warpMode,
warpChainOrder = DataStore.warpChainOrder,
warpEntryConfig = DataStore.warpEntryConfig,
warpEp = DataStore.warpEp,
warpPrivateKey = DataStore.warpPrivateKey,
warpPublicKey = DataStore.warpPublicKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ object SettingsRegistry {
val NTP_OUTBOUNDS = listOf("direct", "proxy")

@JvmField
val WARP_MODES = listOf("wireguard", "masque")
val WARP_MODES = listOf("wireguard", "amnezia", "masque")

/** defaultTunPrivateRanges (SettingsRepo.h:18-21). */
@JvmField
Expand Down Expand Up @@ -303,6 +303,8 @@ object SettingsRegistry {
// ------------------------------------------------------------------------------------------------ warp

@JvmField val ENABLE_WARP = bool("enable_warp", false)
@JvmField val WARP_CHAIN_ORDER = oneOf("warp_chain_order", "proxy-first", listOf("proxy-first", "warp-first", "warp-both"))
@JvmField val WARP_ENTRY_CONFIG = string("warp_entry_config", "")
@JvmField val WARP_MODE = oneOf("warp_mode", "wireguard", WARP_MODES)
@JvmField val WARP_EP = string("warp_ep", "")
@JvmField val WARP_PRIVATE_KEY = string("warp_private_key", "")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ internal class ChainBuilder(
private val ctx: BuildContext,
private val state: BuildState,
private val warp: Outbound? = null,
private val entryWarp: Outbound? = null,
) {
/** usesXrayCore (generate.cpp:428-431). */
fun usesXrayCore(outbound: Outbound): Boolean = outbound.isXray() || outbound.isXrayFullConfig()
Expand Down Expand Up @@ -128,7 +129,11 @@ internal class ChainBuilder(
var xrayFullConfig = false
var inXray = false
for (id in hopIds) {
val hop = profiles.get(id) ?: continue
val hop = when (id) {
WarpHop.PROFILE_ID -> warp
WarpHop.ENTRY_PROFILE_ID -> entryWarp
else -> profiles.get(id)
} ?: continue
if (hop.isXrayFullConfig()) xrayFullConfig = true
val xray = hop.isXray()
if (xray && !inXray) singToXray = true
Expand Down Expand Up @@ -189,8 +194,9 @@ internal class ChainBuilder(
val scan = ChainScan()
var inXray = false
for (id in hopIds) {
if (id == WarpHop.PROFILE_ID) {
val warpHop = warp ?: return missingHopError(id, origin)
if (id == WarpHop.PROFILE_ID || id == WarpHop.ENTRY_PROFILE_ID) {
val warpHop = (if (id == WarpHop.ENTRY_PROFILE_ID) entryWarp else warp)
?: return missingHopError(id, origin)
if (inXray) {
state.xrayToSingTransitioned = true
scan.coreTransitions++
Expand Down Expand Up @@ -395,6 +401,15 @@ internal class ChainBuilder(
}
val obj = result.json
obj["tag"] = tag
if (hop.id == WarpHop.ENTRY_PROFILE_ID && nextTag.isEmpty()) {
// A separate WireGuard device per selector member would reuse one identity and race handshakes.
if (state.endpoints.none { (it as? JsonObject)?.string("tag") == "warp-entry" }) {
obj["tag"] = "warp-entry"
state.endpoints.add(obj)
}
state.outbounds.add(jsonObjectOf("type" to "selector", "tag" to tag, "outbounds" to JsonArray.of("warp-entry")))
continue
}
state.collectEchQueryName(hop.outbound)
// Realm reads its STUN resolver off this key only; without it the hosts go through DNS rules (:1356-1358).
if (TypeAccess.realmActive(hop.outbound)) obj["domain_resolver"] = jsonObjectOf("server" to Tags.DNS_DIRECT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,11 @@ class ConfigGenerator @JvmOverloads constructor(
val front = if (plan != null) -1L else frontProxyId

val state = BuildState(forTest = false, ports)
val chains = ChainBuilder(reads, buildContext, state, if (settings.enableWarp) WarpHop.outbound(settings) else null)
val chains = ChainBuilder(
reads, buildContext, state,
if (settings.enableWarp) WarpHop.outbound(settings) else null,
if (WarpHop.before(settings)) WarpHop.outbound(settings, entry = true) else null,
)
calculatePrerequisites(state, chains, reads, profile, route, landing, front, plan)
if (state.failed) return GeneratedConfig.failure(state.error)

Expand Down Expand Up @@ -612,12 +616,17 @@ class ConfigGenerator @JvmOverloads constructor(
state: BuildState, chains: ChainBuilder, reads: ProfileProvider, profile: Outbound, profileId: Long,
landingProxyId: Long, frontProxyId: Long, plan: AutoSelectorPlan?, origin: HopOrigin,
) {
val warpWrap = settings.enableWarp
val warpBefore = WarpHop.before(settings)
val warpAfter = WarpHop.after(settings)
val warpWrap = warpAfter && !warpBefore
if (profile is AutoSelector && plan != null) {
buildAutoSelectorGroup(state, chains, reads, profile, profileId, plan, warpWrap)
val groupTag = if (warpAfter) { if (warpBefore) "warp-main" else Tags.WARP_BYPASS } else Tags.PROXY
buildAutoSelectorGroup(state, chains, reads, profile, profileId, plan, groupTag, warpBefore)
if (state.failed) return
if (warpBefore) buildAutoSelectorGroup(state, chains, reads, profile, profileId, plan, Tags.WARP_BYPASS, false, track = false)
if (state.failed) return
if (warpWrap) {
buildWarpInFrontOfSelector(state)
if (warpAfter) {
buildWarpInFrontOfSelector(state, groupTag)
if (state.failed) return
}
} else {
Expand All @@ -630,7 +639,13 @@ class ConfigGenerator @JvmOverloads constructor(
state.error = "The chain has no hops"
return
}
if (warpWrap) hopIds.add(0, WarpHop.PROFILE_ID)
if (warpBefore) {
val bypassTag = chains.buildOutboundChain(ChainRequest(hopIds.toList(), "warp-bypass-chain", origin = origin))
if (state.failed) return
state.outbounds.add(jsonObjectOf("type" to "selector", "tag" to Tags.WARP_BYPASS, "outbounds" to JsonArray.of(bypassTag)))
hopIds.add(WarpHop.ENTRY_PROFILE_ID)
}
if (warpAfter) hopIds.add(0, WarpHop.PROFILE_ID)
chains.buildOutboundChain(
ChainRequest(hopIds, Tags.MAIN_CHAIN_PREFIX, includeProxy = true, warpWrap = warpWrap, origin = origin),
)
Expand Down Expand Up @@ -672,18 +687,17 @@ class ConfigGenerator @JvmOverloads constructor(
*/
private fun buildAutoSelectorGroup(
state: BuildState, chains: ChainBuilder, reads: ProfileProvider, selector: AutoSelector, selectorId: Long,
plan: AutoSelectorPlan, warpWrap: Boolean,
plan: AutoSelectorPlan, groupTag: String, warpBefore: Boolean, track: Boolean = true,
) {
if (!plan.ok) {
state.error = plan.error
return
}
val groupTag = if (warpWrap) Tags.WARP_BYPASS else Tags.PROXY

class PlannedMember(val member: SelectorMember, val hopIds: List<Long>, val bridges: MemberBridges, val origin: HopOrigin)

// One member the core rejects fails the whole group's start, so drop it instead.
val rejected = rejectedMembers(chains, reads, plan.build)
val rejected = rejectedMembers(chains, reads, plan.build).toMutableMap()
val plannedMembers = ArrayList<PlannedMember>()
var bridgeCount = 0
for (id in plan.build) {
Expand All @@ -695,9 +709,17 @@ class ConfigGenerator @JvmOverloads constructor(
if (memberGroup.landingProxyId > 0) hopIds.add(memberGroup.landingProxyId)
hopIds.add(id)
if (memberGroup.frontProxyId > 0) hopIds.add(memberGroup.frontProxyId)
if (warpBefore) hopIds.add(WarpHop.ENTRY_PROFILE_ID)
val origin = HopOrigin(selector, listOf(ChainGroup(memberGroup.name, memberGroup.landingProxyId, memberGroup.frontProxyId)))
if (warpBefore) {
val error = chains.candidateError(hopIds, origin)
if (error.isNotEmpty() && error != OutboundChecks.fragmentSettingsError(buildContext)) {
rejected[id] = error
continue
}
}
val bridges = chains.bridgesFor(hopIds)
bridgeCount += bridges.count
val origin = HopOrigin(selector, listOf(ChainGroup(memberGroup.name, memberGroup.landingProxyId, memberGroup.frontProxyId)))
plannedMembers.add(PlannedMember(member, hopIds, bridges, origin))
}
// One batch for every member: reserving per member can deal the same port twice.
Expand All @@ -714,7 +736,7 @@ class ConfigGenerator @JvmOverloads constructor(
val tag = chains.buildOutboundChain(
ChainRequest(
planned.hopIds,
hopTag(Tags.POOL_CHAIN_PREFIX, idx),
hopTag(if (track) Tags.POOL_CHAIN_PREFIX else "pool-warp-bypass", idx),
singToXrayPort = if (bridges.singToXray) bridgePorts[portIdx++] else -1,
xrayToSingPort = if (bridges.xrayToSing) bridgePorts[portIdx++] else -1,
xrayFullConfigPort = if (bridges.xrayFullConfig) bridgePorts[portIdx++] else -1,
Expand Down Expand Up @@ -777,13 +799,13 @@ class ConfigGenerator @JvmOverloads constructor(
group["balance_interval"] = "${selector.balanceIntervalSec}s"
}
state.outbounds.add(group)
state.autoSelector = AutoSelectorBuild(
if (track) state.autoSelector = AutoSelectorBuild(
groupTag = groupTag,
selectorId = selectorId,
members = members,
rejected = rejected,
intervalSec = selector.intervalSec,
warp = warpWrap,
warp = WarpHop.after(settings),
)
}

Expand All @@ -806,8 +828,8 @@ class ConfigGenerator @JvmOverloads constructor(
return rejected
}

/** buildWarpInFrontOfSelector (:1779-1792): the group holds warp-bypass, so WARP is `proxy` dialing through it. */
private fun buildWarpInFrontOfSelector(state: BuildState) {
/** Exit WARP dials through the selected group, which may itself use entry WARP. */
private fun buildWarpInFrontOfSelector(state: BuildState, detour: String) {
val warp = WarpHop.outbound(settings)
val result = warp.buildChecked(buildContext)
if (!result.ok) {
Expand All @@ -816,7 +838,7 @@ class ConfigGenerator @JvmOverloads constructor(
}
val obj = result.json
obj["tag"] = Tags.PROXY
obj["detour"] = Tags.WARP_BYPASS
obj["detour"] = detour
if (warp.isEndpoint()) state.endpoints.add(obj) else state.outbounds.add(obj)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class GeneratedConfig(
* core status names members by [members] tags, and pinning maps a profile id back to its tag.
*/
class AutoSelectorBuild(
/** The core `auto-selector` outbound: `proxy`, or `warp-bypass` under WARP. */
/** The core auto-selector group, including its WARP placement. */
@JvmField val groupTag: String,
@JvmField val selectorId: Long,
/** Member ingress tag (`pool-<i>-0`) -> member profile id, in pool order: the plan's build minus [rejected]. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,10 @@ data class GeneratorSettings(
// ---- warp (SettingsRepo.h:252-267), the built-in WARP hop of WarpHop
/** enable_warp. */
val enableWarp: Boolean = false,
/** warp_mode: "wireguard" or "masque". */
/** warp_mode: "wireguard", "amnezia" or "masque". */
val warpMode: String = "wireguard",
val warpChainOrder: String = "proxy-first",
val warpEntryConfig: String = "",
/** warp_ep, warp_private_key, warp_public_key, warp_ifc_addrs, warp_reserved (decimal strings). */
val warpEp: String = "",
val warpPrivateKey: String = "",
Expand Down
Loading