diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 573e7e24a074cdf6f19b7f56e8c41f1723c261c0..ad650c1ce12653303948d68c154f9a5246afde19 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -28,8 +28,8 @@ dependencies {
 gradlePlugin {
     plugins {
         create("golang") {
-            id = "library-golang"
-            implementationClass = "LibraryGolangPlugin"
+            id = "clash-build"
+            implementationClass = "com.github.kr328.clash.tools.ClashBuildPlugin"
         }
     }
 }
diff --git a/buildSrc/src/main/java/GolangBuildTask.kt b/buildSrc/src/main/java/GolangBuildTask.kt
deleted file mode 100644
index 734e16ddc25cf5f868978339d681ccec5c77b9bf..0000000000000000000000000000000000000000
--- a/buildSrc/src/main/java/GolangBuildTask.kt
+++ /dev/null
@@ -1,167 +0,0 @@
-import org.apache.tools.ant.taskdefs.condition.Os
-import org.gradle.api.DefaultTask
-import org.gradle.api.GradleScriptException
-import org.gradle.api.file.DirectoryProperty
-import org.gradle.api.provider.Property
-import org.gradle.api.provider.SetProperty
-import org.gradle.api.tasks.*
-import java.io.ByteArrayOutputStream
-import java.io.File
-import java.io.FileNotFoundException
-import java.io.IOException
-import java.util.*
-
-abstract class GolangBuildTask : DefaultTask() {
-    abstract val debug: Property<Boolean>
-        @Input get
-
-    abstract val premium: Property<Boolean>
-        @Input get
-
-    abstract val nativeAbis: SetProperty<String>
-        @Input get
-
-    abstract val minSdkVersion: Property<Int>
-        @Input get
-
-    abstract val ndkDirectory: DirectoryProperty
-        @InputDirectory get
-
-    abstract val cmakeDirectory: DirectoryProperty
-        @InputDirectory get
-
-    abstract val inputDirectory: DirectoryProperty
-        @InputDirectory get
-
-    abstract val outputDirectory: DirectoryProperty
-        @OutputDirectory get
-
-    @TaskAction
-    fun build() {
-        val src = inputDirectory.get().asFile
-
-        val generateCmd = """go run make/make.go bridge native build android %s"""
-
-        val buildCmd = if (debug.get()) {
-            """go build --buildmode=c-shared -trimpath -o "%s" -tags "without_gvisor,without_system,debug${if (premium.get()) ",premium" else ""}"""
-        } else {
-            """go build --buildmode=c-shared -trimpath -o "%s" -tags "without_gvisor,without_system${if (premium.get()) ",premium" else ""}" -ldflags "-s -w""""
-        }
-
-        "go mod tidy".exec(pwd = src)
-
-        nativeAbis.get().parallelStream().forEach {
-            val out = outputDirectory.get().file("$it/libclash.so")
-
-            generateCmd.format(it.toGoArch()).exec(pwd = src.resolve("tun2socket/bridge"), env = generateGolangGenerateEnvironment(it))
-            buildCmd.format(out).exec(pwd = src, env = generateGolangBuildEnvironment(it))
-        }
-    }
-
-    private fun generateGolangGenerateEnvironment(abi: String): Map<String, String> {
-        val path = cmakeDirectory.get().asFile.absolutePath + File.pathSeparator + System.getenv("PATH")
-
-        return mapOf(
-            "PATH" to path,
-            "CMAKE_SYSTEM_NAME" to "Android",
-            "CMAKE_ANDROID_NDK" to ndkDirectory.get().asFile.absolutePath,
-            "CMAKE_ANDROID_ARCH_ABI" to abi,
-            "CMAKE_SYSTEM_VERSION" to minSdkVersion.get().toString()
-        )
-    }
-
-    private fun generateGolangBuildEnvironment(abi: String): Map<String, String> {
-        val (goArch, goArm) = when (abi) {
-            "arm64-v8a" -> "arm64" to ""
-            "armeabi-v7a" -> "arm" to "7"
-            "x86" -> "386" to ""
-            "x86_64" -> "amd64" to ""
-            else -> throw UnsupportedOperationException("unsupported abi: $abi")
-        }
-
-        val compiler = when (abi) {
-            "armeabi-v7a" ->
-                "armv7a-linux-androideabi${minSdkVersion.get()}-clang"
-            "arm64-v8a" ->
-                "aarch64-linux-android${minSdkVersion.get()}-clang"
-            "x86" ->
-                "i686-linux-android${minSdkVersion.get()}-clang"
-            "x86_64" ->
-                "x86_64-linux-android${minSdkVersion.get()}-clang"
-            else ->
-                throw GradleScriptException(
-                    "Unsupported abi $abi",
-                    FileNotFoundException("Unsupported abi $abi")
-                )
-        }
-
-        return mapOf(
-            "CC" to compilerBasePath.resolve(compiler).absolutePath,
-            "GOOS" to "android",
-            "GOARCH" to goArch,
-            "GOARM" to goArm,
-            "CGO_ENABLED" to "1",
-            "CFLAGS" to "-O3 -Werror",
-            "CMAKE_ARGS" to "-DCMAKE_TOOLCHAIN_FILE=${ndkDirectory.get().asFile.absolutePath}/build/cmake/android.toolchain.cmake -DANDROID_ABI=$abi -DANDROID_PLATFORM=android-${minSdkVersion.get()} -DCMAKE_BUILD_TYPE=Release",
-            "PATH" to cmakeDirectory.get().asFile.absolutePath + File.pathSeparator + System.getenv("PATH")
-        )
-    }
-
-    private fun String.toGoArch(): String {
-        return when (this) {
-            "arm64-v8a" -> "arm64"
-            "armeabi-v7a" -> "arm"
-            "x86" -> "386"
-            "x86_64" -> "amd64"
-            else -> throw UnsupportedOperationException("unsupported abi: $this")
-        }
-    }
-
-    private fun String.exec(
-        pwd: File,
-        env: Map<String, String> = System.getenv()
-    ): String {
-        val process = ProcessBuilder().run {
-            if (Os.isFamily(Os.FAMILY_WINDOWS))
-                command("cmd.exe", "/c", this@exec)
-            else
-                command("bash", "-c", this@exec)
-
-            environment().putAll(env)
-            directory(pwd)
-
-            redirectErrorStream(true)
-
-            start()
-        }
-
-        val outputStream = ByteArrayOutputStream()
-        process.inputStream.copyTo(outputStream)
-
-        if (process.waitFor() != 0) {
-            println(outputStream.toString("utf-8"))
-            throw GradleScriptException("Exec $this failure", IOException())
-        }
-
-        return outputStream.toString("utf-8")
-    }
-
-    private val compilerBasePath: File
-        get() {
-            val host = when {
-                Os.isFamily(Os.FAMILY_WINDOWS) ->
-                    "windows"
-                Os.isFamily(Os.FAMILY_MAC) ->
-                    "darwin"
-                Os.isFamily(Os.FAMILY_UNIX) ->
-                    "linux"
-                else ->
-                    throw GradleScriptException(
-                        "Unsupported host",
-                        FileNotFoundException("Unsupported host")
-                    )
-            }
-
-            return ndkDirectory.get().asFile.resolve("toolchains/llvm/prebuilt/$host-x86_64/bin")
-        }
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/java/LibraryGolangPlugin.kt b/buildSrc/src/main/java/LibraryGolangPlugin.kt
deleted file mode 100644
index fb22c8c9f58448d3bee827379bd76b773a015dad..0000000000000000000000000000000000000000
--- a/buildSrc/src/main/java/LibraryGolangPlugin.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-import com.android.build.gradle.LibraryExtension
-import org.apache.tools.ant.taskdefs.condition.Os
-import org.gradle.api.GradleScriptException
-import org.gradle.api.Plugin
-import org.gradle.api.Project
-import java.io.File
-import java.io.FileNotFoundException
-import java.util.*
-
-class LibraryGolangPlugin : Plugin<Project> {
-    override fun apply(target: Project) {
-        target.extensions.getByType(LibraryExtension::class.java).apply {
-            target.afterEvaluate {
-                val properties = Properties().apply {
-                    target.rootProject.file("local.properties").inputStream().use(this::load)
-                }
-                val cmakeDirectory = target.rootProject.file(properties.getProperty("cmake.dir")!!)
-
-                libraryVariants.forEach { variant ->
-                    val abis = defaultConfig.externalNativeBuild.cmake.abiFilters +
-                            defaultConfig.externalNativeBuild.ndkBuild.abiFilters
-
-                    val nameCapitalize = variant.name.capitalize(Locale.getDefault())
-                    val golangBuildDir = target.golangBuild.resolve(variant.name)
-
-                    val task = target.tasks.register(
-                        "externalGolangBuild$nameCapitalize",
-                        GolangBuildTask::class.java
-                    ) {
-                        it.premium.set(variant.flavorName == "premium")
-                        it.debug.set(variant.name == "debug")
-                        it.nativeAbis.set(abis)
-                        it.minSdkVersion.set(defaultConfig.minSdk!!)
-                        it.ndkDirectory.set(ndkDirectory)
-                        it.cmakeDirectory.set(cmakeDirectory)
-                        it.inputDirectory.set(target.golangSource)
-                        it.outputDirectory.set(golangBuildDir)
-                    }
-
-                    sourceSets.named(variant.name) {
-                        it.jniLibs {
-                            srcDir(golangBuildDir)
-                        }
-                    }
-
-                    variant.externalNativeBuildProviders.forEach {
-                        it.get().dependsOn(task)
-                    }
-                    target.tasks.filter { it.name.startsWith("buildCMake") }.forEach {
-                        it.mustRunAfter(task)
-                    }
-                }
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/java/com/github/kr328/clash/tools/BuildConfig.kt b/buildSrc/src/main/java/com/github/kr328/clash/tools/BuildConfig.kt
new file mode 100644
index 0000000000000000000000000000000000000000..24acc5de6ef27c06db943e67db2e7361afac2644
--- /dev/null
+++ b/buildSrc/src/main/java/com/github/kr328/clash/tools/BuildConfig.kt
@@ -0,0 +1,25 @@
+package com.github.kr328.clash.tools
+
+import com.android.build.gradle.BaseExtension
+import com.android.build.gradle.api.BaseVariant
+import java.io.Serializable
+
+data class BuildConfig(
+    val debug: Boolean,
+    val premium: Boolean,
+    val abis: List<NativeAbi>,
+    val minSdkVersion: Int,
+) : Serializable {
+    companion object {
+        fun of(extension: BaseExtension, variant: BaseVariant): BuildConfig {
+            return BuildConfig(
+                debug = variant.buildType.isDebuggable,
+                premium = variant.flavorName == "premium",
+                abis = extension.defaultConfig.externalNativeBuild.cmake.abiFilters
+                    .map { NativeAbi.parse(it) }
+                    .distinct(),
+                minSdkVersion = extension.defaultConfig.minSdkVersion!!.apiLevel
+            )
+        }
+    }
+}
diff --git a/buildSrc/src/main/java/com/github/kr328/clash/tools/ClashBuildPlugin.kt b/buildSrc/src/main/java/com/github/kr328/clash/tools/ClashBuildPlugin.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b6c8c5dbbfd3c2179beaffd601dc4f86a349ee9c
--- /dev/null
+++ b/buildSrc/src/main/java/com/github/kr328/clash/tools/ClashBuildPlugin.kt
@@ -0,0 +1,61 @@
+package com.github.kr328.clash.tools
+
+import com.android.build.gradle.LibraryExtension
+import golangBuild
+import golangSource
+import org.gradle.api.GradleException
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.tasks.Delete
+import java.io.File
+import java.util.*
+
+class ClashBuildPlugin : Plugin<Project> {
+    override fun apply(target: Project) {
+        target.afterEvaluate {
+            val cmakeDirectory = resolveCmakeDir(target)
+
+            target.extensions.getByType(LibraryExtension::class.java).apply {
+                libraryVariants.forEach { variant ->
+                    val config = BuildConfig.of(this, variant)
+                    val buildDir = target.golangBuild.resolve(variant.name)
+                    val capitalize = variant.name.capitalize(Locale.getDefault())
+
+                    val task = target.tasks.register(
+                        "externalGolangBuild$capitalize",
+                        ClashBuildTask::class.java
+                    ) {
+                        it.config.set(config)
+                        it.ndkDirectory.set(ndkDirectory)
+                        it.cmakeDirectory.set(cmakeDirectory)
+                        it.inputDirectory.set(target.golangSource)
+                        it.outputDirectory.set(buildDir)
+                    }
+
+                    sourceSets.named(variant.name) {
+                        it.jniLibs {
+                            srcDir(buildDir)
+                        }
+                    }
+
+                    variant.externalNativeBuildProviders.forEach {
+                        it.get().dependsOn(task)
+                    }
+                    target.tasks.filter { it.name.startsWith("buildCMake") }.forEach {
+                        it.mustRunAfter(task)
+                    }
+                }
+            }
+        }
+    }
+
+    private fun resolveCmakeDir(project: Project): File {
+        val properties = Properties().apply {
+            project.rootProject.file("local.properties").inputStream().use(this::load)
+        }
+
+        return project.rootProject.file(
+            properties.getProperty("cmake.dir") ?: throw GradleException("cmake.dir not found")
+        )
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/java/com/github/kr328/clash/tools/ClashBuildTask.kt b/buildSrc/src/main/java/com/github/kr328/clash/tools/ClashBuildTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d11a5ea183105a56486ce52e530ab840f7ba50a3
--- /dev/null
+++ b/buildSrc/src/main/java/com/github/kr328/clash/tools/ClashBuildTask.kt
@@ -0,0 +1,63 @@
+package com.github.kr328.clash.tools
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+
+abstract class ClashBuildTask : DefaultTask() {
+    abstract val config: Property<BuildConfig>
+        @Input get
+
+    abstract val ndkDirectory: DirectoryProperty
+        @InputDirectory get
+
+    abstract val cmakeDirectory: DirectoryProperty
+        @InputDirectory get
+
+    abstract val inputDirectory: DirectoryProperty
+        @InputDirectory get
+
+    abstract val outputDirectory: DirectoryProperty
+        @OutputDirectory get
+
+    @TaskAction
+    fun build() {
+        val input = inputDirectory.file
+        val output = outputDirectory.file
+
+        val config = config.get()
+        val environment = Environment(ndkDirectory.file, cmakeDirectory.file, config.minSdkVersion)
+
+        val tags = listOf("without_gvisor", "without_system") +
+                (if (config.debug) listOf("debug") else emptyList()) +
+                (if (config.premium) listOf("premium") else emptyList())
+
+        Command.ofGoModuleTidy(input).exec()
+
+        config.abis.forEach {
+            Command.ofGoRun(
+                "make/make.go",
+                listOf("bridge", "native", "build", "android", it.goArch),
+                input.resolve("tun2socket/bridge"),
+                environment.ofLwipBuild(it)
+            ).exec()
+
+            Command.ofGoBuild(
+                "c-shared",
+                output.resolve("${it.value}/libclash.so"),
+                tags,
+                !config.debug,
+                input,
+                environment.ofCoreBuild(it)
+            ).exec()
+        }
+    }
+
+    private val DirectoryProperty.file: File
+        get() = get().asFile
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/java/com/github/kr328/clash/tools/Command.kt b/buildSrc/src/main/java/com/github/kr328/clash/tools/Command.kt
new file mode 100644
index 0000000000000000000000000000000000000000..625efb04693bd6b947488e355023cea97feb893e
--- /dev/null
+++ b/buildSrc/src/main/java/com/github/kr328/clash/tools/Command.kt
@@ -0,0 +1,84 @@
+package com.github.kr328.clash.tools
+
+import org.gradle.api.GradleException
+import java.io.File
+import kotlin.concurrent.thread
+
+class Command(
+    private val command: Array<String>,
+    workingDir: File,
+    environments: Map<String, String>
+) {
+    private val processBuilder: ProcessBuilder = ProcessBuilder(*command)
+        .redirectErrorStream(true)
+        .directory(workingDir)
+        .apply { environment().putAll(environments) }
+
+    fun exec() {
+        val process = processBuilder.start()
+
+        thread {
+            process.inputStream.copyTo(System.out)
+        }
+
+        val result = process.waitFor()
+
+        if (result != 0) {
+            throw GradleException("exec ${command.joinToString(" ")}: exit with $result")
+        }
+    }
+
+    companion object {
+        fun ofGoModuleTidy(workingDir: File): Command {
+            return Command(arrayOf("go", "mod", "tidy"), workingDir, System.getenv())
+        }
+
+        fun ofGoBuild(
+            mode: String,
+            output: File,
+            tags: List<String>,
+            strip: Boolean,
+            workingDir: File,
+            environments: Map<String, String>
+        ): Command {
+            val command = mutableListOf("go", "build")
+
+            // go build mode
+            command += "-buildmode"
+            command += mode
+
+            // output file
+            command += "-o"
+            command += output.absolutePath
+
+            // trim path prefix
+            command += "-trimpath"
+
+            if (tags.isNotEmpty()) {
+                command += "-tags"
+                command += tags.joinToString(",")
+            }
+
+            if (strip) {
+                command += "-ldflags"
+                command += "-s -w"
+            }
+
+            return Command(command.toTypedArray(), workingDir, environments)
+        }
+
+        fun ofGoRun(
+            file: String,
+            args: List<String>,
+            workingDir: File,
+            environments: Map<String, String>
+        ): Command {
+            val command = mutableListOf("go", "run")
+
+            command += file
+            command += args
+
+            return Command(command.toTypedArray(), workingDir, environments)
+        }
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/java/com/github/kr328/clash/tools/Environment.kt b/buildSrc/src/main/java/com/github/kr328/clash/tools/Environment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0403bf36442c1caa82e407001401f9bfa620dd21
--- /dev/null
+++ b/buildSrc/src/main/java/com/github/kr328/clash/tools/Environment.kt
@@ -0,0 +1,48 @@
+package com.github.kr328.clash.tools
+
+import org.apache.tools.ant.taskdefs.condition.Os
+import org.gradle.api.GradleException
+import java.io.File
+
+class Environment(
+    private val ndkDirectory: File,
+    private val cmakeDirectory: File,
+    private val minSdkVersion: Int,
+) {
+    fun ofCoreBuild(abi: NativeAbi): Map<String, String> {
+        val host = when {
+            Os.isFamily(Os.FAMILY_WINDOWS) ->
+                "windows"
+            Os.isFamily(Os.FAMILY_MAC) ->
+                "darwin"
+            Os.isFamily(Os.FAMILY_UNIX) ->
+                "linux"
+            else ->
+                throw GradleException("Unsupported host: ${System.getProperty("os.name")}")
+        }
+
+        val compiler = ndkDirectory.resolve("toolchains/llvm/prebuilt/$host-x86_64/bin")
+            .resolve("${abi.compiler}${minSdkVersion}-clang")
+
+        return mapOf(
+            "CC" to compiler.absolutePath,
+            "GOOS" to "android",
+            "GOARCH" to abi.goArch,
+            "GOARM" to abi.goArm,
+            "CGO_ENABLED" to "1",
+            "CFLAGS" to "-O3 -Werror",
+        )
+    }
+
+    fun ofLwipBuild(abi: NativeAbi): Map<String, String> {
+        val path = "${cmakeDirectory.absolutePath}${File.pathSeparator}${System.getenv("PATH")}"
+
+        return mapOf(
+            "PATH" to path,
+            "CMAKE_SYSTEM_NAME" to "Android",
+            "CMAKE_ANDROID_NDK" to ndkDirectory.absolutePath,
+            "CMAKE_ANDROID_ARCH_ABI" to abi.value,
+            "CMAKE_SYSTEM_VERSION" to minSdkVersion.toString()
+        )
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/java/com/github/kr328/clash/tools/NativeAbi.kt b/buildSrc/src/main/java/com/github/kr328/clash/tools/NativeAbi.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b115833bb249bc8b715424e576d1ab95c0be9380
--- /dev/null
+++ b/buildSrc/src/main/java/com/github/kr328/clash/tools/NativeAbi.kt
@@ -0,0 +1,25 @@
+package com.github.kr328.clash.tools
+
+enum class NativeAbi(
+    val value: String,
+    val compiler: String,
+    val goArch: String,
+    val goArm: String
+) {
+    ArmeabiV7a("armeabi-v7a", "armv7a-linux-androideabi", "arm", "7"),
+    Arm64V8a("arm64-v8a", "aarch64-linux-android", "arm64", ""),
+    X86("x86", "i686-linux-android", "386", ""),
+    X64("x86_64", "x86_64-linux-android", "amd64", "");
+
+    companion object {
+        fun parse(value: String): NativeAbi {
+            return when (value) {
+                ArmeabiV7a.value -> ArmeabiV7a
+                Arm64V8a.value -> Arm64V8a
+                X86.value -> X86
+                X64.value -> X64
+                else -> throw IllegalArgumentException("unsupported abi $value")
+            }
+        }
+    }
+}
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 604333af729b64f1c233c54fc19033af66026ad7..5cef3ed03c2140c82090c7d01442f6b12aabc560 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -6,7 +6,7 @@ plugins {
     id("com.android.library")
     kotlin("android")
     id("kotlinx-serialization")
-    id("library-golang")
+    id("clash-build")
 }
 
 val geoipDatabaseUrl =
diff --git a/core/src/main/cpp/CMakeLists.txt b/core/src/main/cpp/CMakeLists.txt
index 792e21ab04eef54d6df4c6e33811f50b1bdd37d9..2260c1a834ed26a2756744eae4bea76669924f1b 100644
--- a/core/src/main/cpp/CMakeLists.txt
+++ b/core/src/main/cpp/CMakeLists.txt
@@ -16,6 +16,7 @@ endif ()
 
 include_directories("${GO_OUTPUT_BASE}/${CMAKE_ANDROID_ARCH_ABI}")
 include_directories("${GO_SOURCE}")
+include_directories("${GO_SOURCE}/tun2socket/bridge/native")
 
 link_directories("${GO_OUTPUT_BASE}/${CMAKE_ANDROID_ARCH_ABI}")
 
diff --git a/core/src/main/golang/go.mod b/core/src/main/golang/go.mod
index fa47698f84690b997f7340ab17ef978f04647887..6fd8ca33e215b81fcc8ac0f72ffcb05a65fd2210 100644
--- a/core/src/main/golang/go.mod
+++ b/core/src/main/golang/go.mod
@@ -5,8 +5,8 @@ go 1.16
 require (
 	cfa/blob v0.0.0 // local generated
 	github.com/Dreamacro/clash v0.0.0 // local
-	github.com/kr328/tun2socket v0.0.0 // local
 	github.com/dlclark/regexp2 v1.4.0
+	github.com/kr328/tun2socket v0.0.0 // local
 	github.com/miekg/dns v1.1.42
 	github.com/oschwald/geoip2-golang v1.5.0
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c