Go core (mihomo + sing-box + xray-core, glued by gomobile) packaged as
an Android .aar. Consumed by the Everywhere Android app's
VpnService via Gradle. Sibling of
EverywhereCore
(iOS/macOS xcframework) and EverywhereCore-Windows (c-shared DLL) —
same wrapper design, same upstream pins.
Each core runs its own TUN inbound directly on the file descriptor
returned by VpnService.Builder.establish() — there is no userland
tun→socks shim in between.
The three core dependencies are not vendored. go/go.mod pins them
by Go-module-compatible semver; the GitHub Actions workflow at
.github/workflows/upstream-watch.yml polls the Go proxy daily and
auto-cuts a new release whenever any upstream moves.
go/ gomobile entry package; *.go + go.mod
Scripts/build.sh gomobile bind → EverywhereCore.aar
.github/workflows/
upstream-watch.yml daily upstream poll + auto-release
git clone https://github.com/NodePassProject/EverywhereCore-Android
cd EverywhereCore-Android
Scripts/build.sh # gomobile bind, deps fetched from Go proxyRequires an Android SDK with an NDK (ANDROID_HOME or
ANDROID_NDK_HOME). Produces ./EverywhereCore.aar with arm64-v8a,
armeabi-v7a, x86_64, and x86 slices, built against
-androidapi 26 (override with ANDROID_API=<n>).
Drop the .aar (from a local build or a GitHub Release asset) into
app/libs/ and add:
dependencies {
implementation(files("libs/EverywhereCore.aar"))
}The bindings land in the com.argsment.everywhere.evcore package as
a static-method facade named Evcore.
class EverywhereVpnService : VpnService() {
private inner class ServiceProtector : Protector {
override fun protect(fd: Int): Boolean = this@EverywhereVpnService.protect(fd)
}
private var tunFd: ParcelFileDescriptor? = null
fun startTunnel(coreType: String, config: String) {
val builder = Builder()
.setMtu(MTU)
.addAddress("172.19.0.1", 30)
.addRoute("0.0.0.0", 0)
.addDnsServer("172.19.0.2")
val pfd = builder.establish() ?: error("VPN permission revoked")
tunFd = pfd
Evcore.setSocketProtector(ServiceProtector()) // BEFORE startCore
Evcore.setResourcesPath(noBackupFilesDir.absolutePath)
Evcore.startCore(coreType, config, pfd.fd.toLong(), MTU.toLong())
}
fun stopTunnel() {
Evcore.stopAll()
tunFd?.close() // unblocks any reader still inside the cores
tunFd = null
}
}coreType is "xray", "singbox", or "mihomo". The config string
must declare a TUN inbound for the active core (the app-side
ConfigNormalizer takes care of that): a tun inbound for Xray, a
type: tun inbound for sing-box, an enabled tun: block for mihomo.
The fd itself is injected by this framework — never write it into the
config text.
setSocketProtector is mandatory. Android routes every socket the
VPN process creates back into its own TUN unless
VpnService.protect(fd) is called on it — without the protector, core
outbound traffic loops through the tunnel and nothing connects. (iOS
has no equivalent because an NE's sockets bypass the tunnel by
design.)
Feed network changes from a ConnectivityManager.NetworkCallback:
override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
val link = connectivityManager.getLinkProperties(network) ?: return
val index = NetworkInterface.getByName(link.interfaceName)?.index ?: -1
Evcore.updateDefaultInterface(
link.interfaceName ?: "", index,
!caps.hasCapability(NET_CAPABILITY_NOT_METERED), false)
}
override fun onLost(network: Network) {
Evcore.updateDefaultInterface("", -1, false, false)
}.github/workflows/upstream-watch.yml runs daily at 08:00 UTC and on
manual workflow_dispatch. Each run:
- Queries
proxy.golang.org/<module>/@latestfor mihomo, sing-box, and xray-core (stable tags only — no pre-releases). - Compares each against the version currently pinned in
go/go.mod. - If at least one is newer (or if dispatched with
force_release: true):go geteach to its latest,go mod tidyScripts/build.shto build the aar- Compute SHA256, commit + tag
vYYYY.MM.DD - Append
.1,.2, … to the tag if multiple runs land same day - Push tag + main;
gh release createwith the aar attached
- Otherwise: no-op (logged as a notice).
To bootstrap the first release after pushing this repo, run the
workflow from the Actions tab with force_release: true.
Edit go/go.mod, push to main. The next cron run will detect the
manual pin is current (or stale) and act accordingly. If you want a
release for a manual bump immediately, dispatch the workflow with
force_release: true.