Android VPN library that combines xray-core and teapod-tun2socks into a single AAR compiled via gomobile.
Android VpnService (Kotlin)
│
├── Teapodcore.startXray() — xray-core in-process (VLESS, VMess, Trojan, SS)
└── Teapodcore.startTun2Socks() — TUN → SOCKS5 bridge with per-connection validation
Traffic flow: TUN interface → tun2socks → xray-core SOCKS5 inbound → proxy outbound
import teapodcore.Teapodcore
import teapodcore.VpnProtector
import teapodcore.XrayCallback
import teapodcore.TunValidator
// 1. Set asset path (geoip.dat / geosite.dat)
Teapodcore.initCoreEnv(context.filesDir.absolutePath, "")
// 2. Protect xray sockets from routing through the VPN tunnel
Teapodcore.registerVpnProtector(object : VpnProtector {
override fun protect(fd: Long): Boolean = vpnService.protect(fd.toInt())
})
// 3. Start xray-core with a JSON config
Teapodcore.startXray(configJson, object : XrayCallback {
override fun onStatus(status: Long, message: String) {
Log.d("VPN", "[xray] $message")
}
})
// 4. Start TUN-to-SOCKS bridge
val tunFd = vpnBuilder.establish()
val error = Teapodcore.startTun2Socks(
tunFd.fd.toLong(),
socksPort = 10808L,
socksUser = "",
socksPass = "",
validator = null // null = allow all
)
// 5. Stop
Teapodcore.stopTun2Socks()
Teapodcore.stopXray()Pass a TunValidator to startTun2Socks to filter connections by UID (requires API 29+):
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
Teapodcore.startTun2Socks(fd, port, "", "", object : TunValidator {
override fun onValidate(srcIP: String, srcPort: Long,
dstIP: String, dstPort: Long, protocol: Long): Boolean {
val uid = cm.getConnectionOwnerUid(
protocol.toInt(),
InetSocketAddress(srcIP, srcPort.toInt()),
InetSocketAddress(dstIP, dstPort.toInt())
)
return uid in allowedUids
}
})| Function | Description |
|---|---|
initCoreEnv(path, xudpKey) |
Set asset directory and optional XUDP base key |
registerVpnProtector(p) |
Protect xray sockets via VpnService.protect() |
registerXrayProcessFinder(f) |
Enable UID-based routing rules in xray-core |
startXray(config, cb) |
Start xray-core with a JSON config string |
stopXray() |
Stop xray-core |
getXrayVersion() |
Returns the xray-core version string |
queryXrayStats(tag, direction) |
Read and reset traffic counter for an outbound tag |
measureXrayDelay(url) |
Latency test through the running instance (ms) |
measureOutboundDelay(config, url) |
Latency test with a temporary instance |
startTun2Socks(fd, port, user, pass, validator) |
Start TUN bridge |
stopTun2Socks() |
Stop TUN bridge |
isTunRunning() |
Whether the TUN bridge is active |
getTunUploadBytes() |
Bytes sent from device to internet |
getTunDownloadBytes() |
Bytes received from internet to device |
getTunCacheSize() |
Validator LRU cache entry count |
setTunLogEnabled(enabled) |
Toggle verbose TUN logging |
Requirements: Go 1.21+, gomobile, Android NDK 28.
# Install gomobile (once)
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
# Build all ABIs + fat AAR
./build.sh build
# Outputs:
# outputs/teapod-core-1.0.2.aar ← fat AAR (all ABIs, use this)
# outputs/teapod-core-arm64-v8a-1.0.2.aar
# outputs/teapod-core-armeabi-v7a-1.0.2.aar
# outputs/teapod-core-x86_64-1.0.2.aar
# Publish to GitHub
./build.sh pushVersion is read from ../teapod-tun2socks/kotlin/gradle.properties.
Copy teapod-core-{VERSION}.aar to android/app/libs/ and add to build.gradle:
dependencies {
implementation(files("libs/teapod-core-1.0.2.aar"))
}Minimum SDK: 29 (Android 10) — required for ConnectivityManager.getConnectionOwnerUid.