Skip to content

Commit 9969a93

Browse files
committed
feat: Implement WireGuard endpoint migration and validation
1 parent e06a508 commit 9969a93

20 files changed

Lines changed: 1172 additions & 138 deletions

File tree

.github/workflows/ci.yml

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ jobs:
1818
with:
1919
distribution: 'temurin'
2020
java-version: '17'
21+
- name: Install Golang
22+
uses: actions/setup-go@v6
23+
with:
24+
go-version: ^1.24
25+
- name: Verify official sing-box source
26+
run: bash buildScript/lib/core/get_source.sh
2127
- name: Golang Status
2228
run: find buildScript libcore/*.sh nb4a.properties | xargs cat | sha1sum > golang_status
2329
- name: Libcore Status
@@ -29,14 +35,12 @@ jobs:
2935
path: |
3036
app/libs/libcore.aar
3137
key: ${{ hashFiles('.github/workflows/*', 'golang_status', 'libcore_status') }}-ci
32-
- name: Install Golang
33-
if: steps.cache.outputs.cache-hit != 'true'
34-
uses: actions/setup-go@v6
35-
with:
36-
go-version: ^1.24
3738
- name: Native Build
3839
if: steps.cache.outputs.cache-hit != 'true'
3940
run: ./run lib core
41+
- name: WireGuard endpoint schema and box tests
42+
working-directory: libcore
43+
run: go test -v -run 'Test(WireGuardEndpointConfigsCreateAndClose|MissingWireGuardRouteTagDoesNotFallbackToDirect|LegacyWireGuardOutboundIsRejected)$' .
4044
- name: Verify LibCore AAR
4145
run: |
4246
test -s app/libs/libcore.aar
@@ -79,6 +83,12 @@ jobs:
7983
with:
8084
path: ~/.gradle
8185
key: gradle-oss-${{ hashFiles('**/*.gradle.kts') }}-ci
86+
- name: Setup uv
87+
uses: astral-sh/setup-uv@v7
88+
- name: Static WireGuard and repository governance checks
89+
run: |
90+
uv run tools/diagnostics/roo_check_wireguard_endpoint.py
91+
uv run tools/diagnostics/roo_check_repo_governance.py
8292
- name: Gradle Build
8393
env:
8494
BUILD_PLUGIN: none
@@ -87,6 +97,7 @@ jobs:
8797
echo "ndk.dir=${ANDROID_HOME}/ndk/25.0.8775105" >> local.properties
8898
export LOCAL_PROPERTIES="${{ secrets.LOCAL_PROPERTIES }}"
8999
./run init action gradle
100+
./gradlew app:testOssDebugUnitTest
90101
./gradlew app:assembleOssDebug
91102
APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk')
92103
APK=$(dirname $APK)

app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ dependencies {
6060
implementation("com.google.android.material:material:1.8.0")
6161
implementation("com.google.code.gson:gson:2.9.0")
6262

63+
testImplementation("junit:junit:4.13.2")
64+
6365
implementation("com.github.jenly1314:zxing-lite:2.1.1")
6466
implementation("com.blacksquircle.ui:editorkit:2.6.0")
6567
implementation("com.blacksquircle.ui:language-base:2.6.0")

app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import io.nekohasekai.sagernet.database.DataStore
88
import io.nekohasekai.sagernet.database.ProxyEntity
99
import io.nekohasekai.sagernet.fmt.ConfigBuildResult
1010
import io.nekohasekai.sagernet.fmt.buildConfig
11+
import io.nekohasekai.sagernet.fmt.diagnosticTagId
1112
import io.nekohasekai.sagernet.fmt.hysteria.HysteriaBean
1213
import io.nekohasekai.sagernet.fmt.hysteria.buildHysteria1Config
1314
import io.nekohasekai.sagernet.fmt.mieru.MieruBean
@@ -36,6 +37,15 @@ abstract class BoxInstance(
3637
val externalInstances = hashMapOf<Int, AbstractInstance>()
3738
open lateinit var processes: GuardedProcessPool
3839
private var cacheFiles = ArrayList<File>()
40+
41+
protected open val instanceKind = "production"
42+
43+
private fun endpointSummary(): String {
44+
val tags = if (::config.isInitialized) config.endpointTags else emptyList()
45+
return "instance=$instanceKind endpointCount=${tags.size} " +
46+
"endpointTagIds=${tags.joinToString(",") { diagnosticTagId(it) }}"
47+
}
48+
3949
fun isInitialized(): Boolean {
4050
return ::config.isInitialized && ::box.isInitialized
4151
}
@@ -45,12 +55,40 @@ abstract class BoxInstance(
4555
}
4656

4757
protected open fun buildConfig() {
48-
config = buildConfig(profile)
49-
DataStore.mixedInboundAuthed = DataStore.mixedInboundNeedsAuth
58+
val started = SystemClock.elapsedRealtime()
59+
try {
60+
config = buildConfig(profile)
61+
DataStore.mixedInboundAuthed = DataStore.mixedInboundNeedsAuth
62+
Logs.d(
63+
"Box lifecycle profileId=${profile.id} ${endpointSummary()} stage=build-config " +
64+
"elapsed=${SystemClock.elapsedRealtime() - started}ms"
65+
)
66+
} catch (error: Exception) {
67+
Logs.d(
68+
"Box lifecycle profileId=${profile.id} instance=$instanceKind stage=build-config " +
69+
"elapsed=${SystemClock.elapsedRealtime() - started}ms " +
70+
"errorType=${error.javaClass.simpleName}"
71+
)
72+
throw error
73+
}
5074
}
5175

5276
protected open suspend fun loadConfig() {
53-
box = Libcore.newSingBoxInstance(config.config, LocalResolverImpl)
77+
val started = SystemClock.elapsedRealtime()
78+
try {
79+
box = Libcore.newSingBoxInstance(config.config, LocalResolverImpl)
80+
Logs.d(
81+
"Box lifecycle profileId=${profile.id} ${endpointSummary()} stage=create " +
82+
"elapsed=${SystemClock.elapsedRealtime() - started}ms"
83+
)
84+
} catch (error: Exception) {
85+
Logs.d(
86+
"Box lifecycle profileId=${profile.id} ${endpointSummary()} stage=create " +
87+
"elapsed=${SystemClock.elapsedRealtime() - started}ms " +
88+
"errorType=${error.javaClass.simpleName}"
89+
)
90+
throw error
91+
}
5492
}
5593

5694
open suspend fun init() {
@@ -207,11 +245,26 @@ abstract class BoxInstance(
207245
}
208246
}
209247

210-
box.start()
248+
val started = SystemClock.elapsedRealtime()
249+
try {
250+
box.start()
251+
Logs.d(
252+
"Box lifecycle profileId=${profile.id} ${endpointSummary()} stage=start " +
253+
"elapsed=${SystemClock.elapsedRealtime() - started}ms"
254+
)
255+
} catch (error: Exception) {
256+
Logs.d(
257+
"Box lifecycle profileId=${profile.id} ${endpointSummary()} stage=start " +
258+
"elapsed=${SystemClock.elapsedRealtime() - started}ms " +
259+
"errorType=${error.javaClass.simpleName}"
260+
)
261+
throw error
262+
}
211263
}
212264

213265
@Suppress("EXPERIMENTAL_API_USAGE")
214266
override fun close() {
267+
val started = SystemClock.elapsedRealtime()
215268
for (instance in externalInstances.values) {
216269
runCatching {
217270
instance.close()
@@ -225,6 +278,10 @@ abstract class BoxInstance(
225278
if (::box.isInitialized) {
226279
box.close()
227280
}
281+
Logs.d(
282+
"Box lifecycle profileId=${profile.id} ${endpointSummary()} stage=close " +
283+
"elapsed=${SystemClock.elapsedRealtime() - started}ms"
284+
)
228285
}
229286

230287
}

app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
package io.nekohasekai.sagernet.bg.proto
22

3-
import io.nekohasekai.sagernet.BuildConfig
43
import io.nekohasekai.sagernet.bg.BaseService
54
import io.nekohasekai.sagernet.bg.ServiceNotification
65
import io.nekohasekai.sagernet.database.ProxyEntity
6+
import io.nekohasekai.sagernet.fmt.diagnosticTagId
77
import io.nekohasekai.sagernet.ktx.Logs
88
import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
99
import kotlinx.coroutines.runBlocking
10-
import moe.matsuri.nb4a.utils.JavaUtil
1110

1211
class ProxyInstance(profile: ProxyEntity, var service: BaseService.Interface? = null) :
1312
BoxInstance(profile) {
@@ -23,9 +22,14 @@ class ProxyInstance(profile: ProxyEntity, var service: BaseService.Interface? =
2322
override fun buildConfig() {
2423
super.buildConfig()
2524
lastSelectorGroupId = super.config.selectorGroupId
26-
//
27-
if (notTmp) Logs.d(config.config)
28-
if (notTmp && BuildConfig.DEBUG) Logs.d(JavaUtil.gson.toJson(config.trafficMap))
25+
if (notTmp) {
26+
Logs.d(
27+
"Production config profileId=${profile.id} " +
28+
"endpointCount=${config.endpointTags.size} " +
29+
"endpointTagIds=${config.endpointTags.joinToString(",") { diagnosticTagId(it) }} " +
30+
"trafficTagCount=${config.trafficMap.size}"
31+
)
32+
}
2933
}
3034

3135
// only use this in temporary instance

app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
package io.nekohasekai.sagernet.bg.proto
22

33
import android.os.SystemClock
4-
import io.nekohasekai.sagernet.BuildConfig
54
import io.nekohasekai.sagernet.SagerNet
65
import io.nekohasekai.sagernet.bg.GuardedProcessPool
76
import io.nekohasekai.sagernet.database.DataStore
87
import io.nekohasekai.sagernet.database.ProxyEntity
98
import io.nekohasekai.sagernet.fmt.buildConfig
9+
import io.nekohasekai.sagernet.fmt.diagnosticTagId
1010
import io.nekohasekai.sagernet.ktx.Logs
11-
import io.nekohasekai.sagernet.ktx.readableMessage
1211
import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
1312
import io.nekohasekai.sagernet.ktx.tryResume
1413
import io.nekohasekai.sagernet.ktx.tryResumeWithException
@@ -21,18 +20,19 @@ import kotlin.coroutines.suspendCoroutine
2120
class TestInstance(profile: ProxyEntity, val link: String, private val timeout: Int) :
2221
BoxInstance(profile) {
2322

23+
override val instanceKind = "url-test"
24+
2425
private val traceId = traceSequence.incrementAndGet()
25-
private val traceName = profile.displayName()
2626

2727
private fun trace(stage: String, message: String) {
28-
Logs.d("URLTestTrace ktId=$traceId profileId=${profile.id} profile=$traceName stage=$stage $message")
28+
Logs.d("URLTestTrace ktId=$traceId profileId=${profile.id} stage=$stage $message")
2929
}
3030

3131
suspend fun doTest(): Int {
3232
return suspendCoroutine { c ->
3333
val totalStarted = SystemClock.elapsedRealtime()
3434
processes = GuardedProcessPool {
35-
Logs.w("URLTestTrace ktId=$traceId profileId=${profile.id} profile=$traceName stage=plugin-exit elapsed=${SystemClock.elapsedRealtime() - totalStarted}ms error=${it.readableMessage}")
35+
Logs.w("URLTestTrace ktId=$traceId profileId=${profile.id} stage=plugin-exit elapsed=${SystemClock.elapsedRealtime() - totalStarted}ms errorType=${it.javaClass.simpleName}")
3636
c.tryResumeWithException(it)
3737
}
3838
runOnDefaultDispatcher {
@@ -43,7 +43,7 @@ class TestInstance(profile: ProxyEntity, val link: String, private val timeout:
4343
"mode=isolated-box currentProfile=${DataStore.currentProfile} " +
4444
"isCurrent=${profile.id == DataStore.currentProfile} " +
4545
"serviceState=${DataStore.serviceState} network=${SagerNet.underlyingNetwork} " +
46-
"link=$link timeout=${timeout}ms thread=${Thread.currentThread().name}"
46+
"timeout=${timeout}ms thread=${Thread.currentThread().name}"
4747
)
4848
var started = SystemClock.elapsedRealtime()
4949
init()
@@ -78,18 +78,18 @@ class TestInstance(profile: ProxyEntity, val link: String, private val timeout:
7878
c.tryResume(latency)
7979
} catch (e: Exception) {
8080
Logs.w(
81-
"URLTestTrace ktId=$traceId profileId=${profile.id} profile=$traceName " +
81+
"URLTestTrace ktId=$traceId profileId=${profile.id} " +
8282
"stage=$stage failed totalElapsed=${SystemClock.elapsedRealtime() - totalStarted}ms " +
83-
"error=${e.readableMessage}"
83+
"errorType=${e.javaClass.simpleName}"
8484
)
8585
c.tryResumeWithException(e)
8686
} finally {
8787
val closeStarted = SystemClock.elapsedRealtime()
8888
runCatching { close() }
8989
.onFailure {
9090
Logs.w(
91-
"URLTestTrace ktId=$traceId profileId=${profile.id} profile=$traceName " +
92-
"stage=close failed error=${it.readableMessage}"
91+
"URLTestTrace ktId=$traceId profileId=${profile.id} " +
92+
"stage=close failed errorType=${it.javaClass.simpleName}"
9393
)
9494
}
9595
trace(
@@ -114,12 +114,27 @@ class TestInstance(profile: ProxyEntity, val link: String, private val timeout:
114114

115115
override suspend fun loadConfig() {
116116
// don't call destroyAllJsi here
117-
if (BuildConfig.DEBUG) Logs.d(config.config)
118117
// 测速实例用 NewTestSingBoxInstance:不注册 PlatformLogWriter,
119118
// 官方内核不再强制创建 CacheFile/ClashServer(见 libcore/box.go 批注)。
120119
val started = SystemClock.elapsedRealtime()
121-
box = Libcore.newTestSingBoxInstance(config.config, LocalResolverImpl)
122-
trace("create-box", "ok elapsed=${SystemClock.elapsedRealtime() - started}ms")
120+
try {
121+
box = Libcore.newTestSingBoxInstance(config.config, LocalResolverImpl)
122+
trace(
123+
"create-box",
124+
"ok endpointCount=${config.endpointTags.size} " +
125+
"endpointTagIds=${config.endpointTags.joinToString(",") { diagnosticTagId(it) }} " +
126+
"elapsed=${SystemClock.elapsedRealtime() - started}ms"
127+
)
128+
} catch (error: Exception) {
129+
trace(
130+
"create-box",
131+
"failed endpointCount=${config.endpointTags.size} " +
132+
"endpointTagIds=${config.endpointTags.joinToString(",") { diagnosticTagId(it) }} " +
133+
"elapsed=${SystemClock.elapsedRealtime() - started}ms " +
134+
"errorType=${error.javaClass.simpleName}"
135+
)
136+
throw error
137+
}
123138
}
124139

125140
private companion object {

0 commit comments

Comments
 (0)