Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
d6dd27b
ci: add Android CI workflow to build APK
yekaipow Nov 5, 2026
68acc90
ci: restrict build to a single ABI, default to debug
yekaipow Nov 5, 2026
6d6999b
ci: unset empty signing secrets for debug builds too
yekaipow Nov 5, 2026
3036914
feat: add per-profile URL test button next to edit
yekaipow Nov 5, 2026
f7fe023
feat: add per-app proxy (Apps VPN) entry to drawer
yekaipow Nov 5, 2026
dff094b
feat: clear previous test results before starting URL test
yekaipow Nov 5, 2026
91b9c19
ci: stop duplicate workflow runs on main push
yekaipow Nov 5, 2026
24ad2a7
ci: document workflow responsibilities in build-apk.yml
yekaipow Nov 5, 2026
c670e6d
fix: build single ABI via NB4A_ABI env instead of injected property
yekaipow Nov 5, 2026
48a5e44
build: add fixed debug keystore for stable APK signature
yekaipow Nov 5, 2026
eab4e4b
feat: long press menu with multi-select move to group
yekaipow Nov 5, 2026
d1ec422
feat: long press group tab to rename or delete group
yekaipow Nov 5, 2026
dacc3b6
fix: missing string/cancel resource link error
yekaipow Nov 5, 2026
ef69f99
build: sign release with fixed keystore, default to release
yekaipow Nov 5, 2026
d3da6fb
feat: add per-profile URL test button in dual-column layout
yekaipow Nov 5, 2026
1ea9c83
feat: show country flag on profile card after URL test
yekaipow Nov 5, 2026
6837894
fix: geoip record format and flag refresh timing
yekaipow Nov 5, 2026
c26c235
fix: country flag retry and lazy lookup on bind
yekaipow Nov 5, 2026
5f70e34
feat: persist country flags and remove debug logging
yekaipow Nov 5, 2026
a51f4b3
feat: clear country flags together with test results
yekaipow Nov 5, 2026
8856b5d
fix: clear country flags in url test pre-clean step too
yekaipow Nov 5, 2026
478d127
fix: update ping immediately per profile during url test
yekaipow Nov 5, 2026
ca1ef2d
build: allow go module auto update in libcore
yekaipow Nov 5, 2026
7944462
build: add sing-box patch script
yekaipow Nov 5, 2026
c8a42e5
build: point sing-box to v1.14.0 release
yekaipow Nov 5, 2026
f0f6b98
build: fetch sing-box from official repo and apply nekoutils patches
yekaipow Nov 5, 2026
e551ab5
build: bump libcore go directive to 1.25.5 for sing-box 1.14
yekaipow Nov 5, 2026
d6d8a07
build: pre-compile check before gomobile bind
yekaipow Nov 5, 2026
ac4eacf
fix: pass certificate provider registry to box.Context (sing-box 1.14…
yekaipow Nov 5, 2026
7f86cb7
fix: use absolute path when invoking sing-box patch script
yekaipow Nov 5, 2026
4bff656
feat: adapt libcore to sing-box 1.14 (platform interface, boxapi)
yekaipow Nov 5, 2026
14356a4
build: align libcore dependency matrix with sing-box 1.14
yekaipow Nov 5, 2026
6ae6f93
fix: sing-juicity module path moved to exclavenetwork
yekaipow Nov 5, 2026
76ed58d
fix: adapt juicity and boxapi to sing 0.9 APIs
yekaipow Nov 5, 2026
c79c237
fix: adapt remaining libcore interfaces to 1.14
yekaipow Nov 5, 2026
3f9b43c
fix: remove duplicate RegisterMyInterface in interface monitor stub
yekaipow Nov 5, 2026
8eedf19
feat: migrate generated config to sing-box 1.14 formats
yekaipow Nov 5, 2026
2a9e822
fix: migrate TLS fragment to route action for sing-box 1.14
yekaipow Nov 5, 2026
25b34bc
fix: drop route.concurrent_dial and add dedicated DNS direct outbound
yekaipow Nov 5, 2026
9a669b9
fix: align snell outbound with sing-box 1.14 union options
yekaipow Nov 5, 2026
c93fb62
fix: stop routing TLS outbounds through the fragment outbound
yekaipow Nov 5, 2026
fb71455
fix: inject sing-box version at build time
yekaipow Nov 5, 2026
76cb641
chore: drop local signing keystore and personal CI workflow for upstr…
yekaipow Nov 5, 2026
c71a4ce
feat: add group settings entry to group tab long-press menu
yekaipow Nov 6, 2026
d5257be
fix: address review findings
yekaipow Nov 6, 2026
a1c48a5
feat: clear country flag cache together with node removal
yekaipow Nov 6, 2026
5b17b73
feat: long-press group tab opens group settings directly
yekaipow Nov 6, 2026
082c1ea
fix: stop emitting vless encryption field that sing-box 1.14 rejects
yekaipow Nov 6, 2026
65ebf07
feat: port VLESS Encryption (mlkem768x25519plus) into the kernel
yekaipow Nov 6, 2026
ffd77fd
feat: add per-tag url test in a single box instance
yekaipow Nov 6, 2026
39de414
feat: batch url test with a single kernel instance
yekaipow Nov 6, 2026
63db351
fix: compile errors in batch url test
yekaipow Nov 6, 2026
44e2286
refactor: remove the server-ip based country flag system
yekaipow Nov 6, 2026
6003049
feat: show real egress country and ip in the status bar
yekaipow Nov 6, 2026
8f2a1ad
feat: refresh egress info when switching nodes
yekaipow Nov 6, 2026
472c4c1
fix: import androidx.core.view.isVisible/isGone in StatsBar
yekaipow Nov 6, 2026
71c0d38
fix: egress detection via mixed inbound with libcore fallback
yekaipow Nov 6, 2026
ed91880
fix: show detecting placeholder while probing egress
yekaipow Nov 6, 2026
c75192a
fix: isolate invalid profiles in batch url test
yekaipow Nov 8, 2026
0c2f32b
fix: compile errors in batch test instance
yekaipow Nov 8, 2026
0d74529
feat: add xhttp transport via sing-xhttp
yekaipow Nov 8, 2026
d275657
fix: normalize uTLS fingerprint to what the kernel supports
yekaipow Nov 8, 2026
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
7 changes: 2 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
name: Build
on:
# 版本号变更的构建已由 build-apk.yml 覆盖(它的 paths 含 nb4a.properties),
# 这里改为仅手动触发,避免同一次 push 触发两个 workflow
workflow_dispatch:
push:
branches:
- main
paths:
- 'nb4a.properties'
jobs:
libcore:
name: Native Build (LibCore)
Expand Down
7 changes: 2 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
name: CI
on:
push:
tags-ignore:
- 'v*'
branches:
- '*'
# main 分支的构建统一由 build-apk.yml 负责,这里只在 PR 上做校验,避免同一提交跑两遍
pull_request:
workflow_dispatch:
jobs:
libcore:
name: Native Build (LibCore)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package io.nekohasekai.sagernet.bg.proto

import io.nekohasekai.sagernet.BuildConfig
import io.nekohasekai.sagernet.bg.GuardedProcessPool
import io.nekohasekai.sagernet.database.ProxyEntity
import io.nekohasekai.sagernet.fmt.buildConfig
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.ktx.readableMessage
import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
import io.nekohasekai.sagernet.ktx.tryResume
import io.nekohasekai.sagernet.ktx.tryResumeWithException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import libcore.Libcore
import moe.matsuri.nb4a.net.LocalResolverImpl
import org.json.JSONObject
import kotlin.coroutines.suspendCoroutine

/**
* 整组共用一个内核实例的测速器,对标 Clash 的 `/proxies/{name}/delay`:
* 只 init/launch 一次,随后按 outbound tag 并发拨号,测完一个立刻回显。
*
* 原实现([TestInstance])每个节点都要新建并启动一次内核,节点多时开销很大。
*
* 注意:整组共用一个配置,任何一个节点配置非法都会让整个内核起不来。
* 因此这里对单节点做了隔离:decode 失败时按出错的 outbound 定位节点并剔除重试,
* 被剔除的节点记在 [excluded] 里由调用方标记失败,避免整组显示不可用。
*/
class BatchTestInstance(
profiles: List<ProxyEntity>,
val link: String,
private val timeout: Int,
) : BoxInstance(profiles.first()) {

/** 被跳过的节点:profileId -> 原因 */
val excluded = LinkedHashMap<Long, String>()

private var activeProfiles: List<ProxyEntity> = profiles

/** profileId -> outbound tag,由批量配置的 buildChain 生成 */
val tagMap: Map<Long, String>
get() = config.profileTagMap

/**
* 构建配置、初始化并启动内核(插件节点会在此一并拉起)。
* 只需调用一次,之后用 [testTag] 并发测速。
*/
suspend fun prepare() {
// 整组共用一个配置:任何一个节点配置非法都会让整个内核起不来,
// decode 失败时按出错的 outbound 定位到具体节点,剔除后重建配置重试,
// 避免一个坏节点让整组显示不可用。
while (true) {
try {
startOnce()
return
} catch (e: Exception) {
Logs.w("batch test prepare failed: ${e.readableMessage}", e)
val badId = resolveFailedProfile(e)
if (badId == null || activeProfiles.size <= 1) {
throw e
}
Logs.w("batch test: drop profile $badId (${e.readableMessage})")
excluded[badId] = e.readableMessage
activeProfiles = activeProfiles.filterNot { it.id == badId }
releaseKernel()
}
}
}

private suspend fun startOnce() = suspendCoroutine<Unit> { c ->
processes = GuardedProcessPool {
Logs.w(it)
c.tryResumeWithException(it)
}
runOnDefaultDispatcher {
try {
init()
launch()
if (processes.processCount > 0) {
// wait for plugin start
delay(500)
}
c.tryResume(Unit)
} catch (e: Exception) {
c.tryResumeWithException(e)
}
}
}

/** 在已启动的实例内测指定节点,返回 RTT(毫秒),失败抛异常 */
fun testTag(tag: String): Int {
return Libcore.urlTestTag(box, tag, link, timeout)
}

override fun buildConfig() {
// 剔除节点后,主 profile 可能已不在列表里,取当前可用的第一个
config = buildConfig(activeProfiles.first(), true, testProfiles = activeProfiles)
}

override suspend fun loadConfig() {
// don't call destroyAllJsi here
if (BuildConfig.DEBUG) Logs.d(config.config)
box = Libcore.newSingBoxInstance(config.config, LocalResolverImpl)
}

/**
* 从 decode 错误里定位是哪个节点的 outbound 非法。
* sing-box 的报错形如 "decode config: outbounds[20].transport: unknown transport type: xhttp",
* 取该 outbound 的 tag,再反查 profileId。
*/
private fun resolveFailedProfile(e: Throwable): Long? {
val message = generateSequence<Throwable>(e) { it.cause }
.mapNotNull { it.message }
.joinToString(" | ")
// 兼容 "outbounds[20]"(decode 阶段)和 "outbound[1]"(初始化阶段)
val index = Regex("""outbounds?\[(\d+)]""").find(message)
?.groupValues?.get(1)?.toIntOrNull() ?: return null
val tag = runCatching {
JSONObject(config.config).getJSONArray("outbounds").getJSONObject(index).optString("tag")
}.getOrNull()?.takeIf { it.isNotEmpty() } ?: return null
// 优先精确匹配最终 outbound tag
config.profileTagMap.entries.firstOrNull { it.value == tag }?.let { return it.key }
// 链中间节点的 tag 形如 c-<chainId>-<profileId>
return activeProfiles.firstOrNull { tag.endsWith("-${it.id}") }?.id
}

/** 丢弃上一轮已创建但不可用的内核/插件进程 */
private fun releaseKernel() {
// box / processes 是父类的 lateinit 属性,子类不能用 ::x.isInitialized 检查,
// 未初始化时访问会抛 UninitializedPropertyAccessException,runCatching 兜住即可
runCatching { box.close() }
runCatching { processes.close(CoroutineScope(Dispatchers.IO)) }
}
}
Loading