Files
FlClash/core/lib.go
T

401 lines
9.3 KiB
Go
Raw Normal View History

//go:build android
2024-12-03 21:47:12 +08:00
package core
2025-07-31 17:09:18 +08:00
2024-11-09 20:17:57 +08:00
import (
2025-07-31 17:09:18 +08:00
"core/platform"
t "core/tun"
2025-01-13 19:08:17 +08:00
"encoding/json"
2025-07-31 17:09:18 +08:00
"errors"
"fmt"
"net"
"strings"
"sync"
"sync/atomic"
"syscall"
2025-07-31 17:09:18 +08:00
"github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/process"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/dns"
"github.com/metacubex/mihomo/listener/sing_tun"
"github.com/metacubex/mihomo/log"
2024-11-09 20:17:57 +08:00
)
type TunCallbacks interface {
Protect(fd int) bool
ResolveUid(protocol int, source, target string) int
ResolvePackage(uid int) string
}
var (
eventListenerLock sync.RWMutex
eventListener func([]byte)
listening atomic.Bool
)
2025-07-31 17:09:18 +08:00
type TunHandler struct {
listener *sing_tun.Listener
callbacks TunCallbacks
2025-01-13 19:08:17 +08:00
mu sync.RWMutex
2024-12-03 21:47:12 +08:00
}
2024-11-09 20:17:57 +08:00
func (th *TunHandler) start(fd int, stack, address, dns, options string) bool {
configMu.Lock()
defer configMu.Unlock()
th.mu.Lock()
2025-07-31 17:09:18 +08:00
th.initHook()
th.mu.Unlock()
// t.Start runs outside th.mu on purpose. The hook is live from initHook
// onwards, and a socket opened anywhere inside Start reaches handleProtect
// on this very goroutine — an RLock taken while this one holds the write
// lock deadlocks the start outright. Nothing is lost by dropping it: both
// hooks return early until th.listener is set, which is below.
tunListener := t.Start(fd, stack, address, dns, options)
th.mu.Lock()
defer th.mu.Unlock()
2025-07-31 17:09:18 +08:00
if tunListener != nil {
log.Infoln("TUN address: %v", tunListener.Address())
th.listener = tunListener
return true
2025-07-31 17:09:18 +08:00
}
th.clear()
return false
2024-11-09 20:17:57 +08:00
}
2025-07-31 17:09:18 +08:00
func (th *TunHandler) close() {
th.mu.Lock()
defer th.mu.Unlock()
2025-07-31 17:09:18 +08:00
th.clear()
2024-11-09 20:17:57 +08:00
}
2025-07-31 17:09:18 +08:00
func (th *TunHandler) clear() {
th.removeHook()
if th.listener != nil {
_ = th.listener.Close()
}
th.callbacks = nil
2025-07-31 17:09:18 +08:00
th.listener = nil
}
// A refused protect never fails the dial: mihomo's tunnel retries a failed
// dial ten times, and Android refuses only after the VPN was revoked, when the
// routes are gone and an unprotected socket takes the default network.
func (th *TunHandler) handleProtect(fd int) {
th.mu.RLock()
defer th.mu.RUnlock()
if th.listener == nil || th.callbacks == nil {
return
}
th.callbacks.Protect(fd)
2024-11-09 20:17:57 +08:00
}
func (th *TunHandler) handleResolveProcess(source, target net.Addr) (int, string) {
th.mu.RLock()
defer th.mu.RUnlock()
2025-07-31 17:09:18 +08:00
if th.listener == nil || th.callbacks == nil {
return -1, ""
2025-07-31 17:09:18 +08:00
}
var protocol int
switch source.Network() {
case "udp", "udp4", "udp6":
protocol = syscall.IPPROTO_UDP
case "tcp", "tcp4", "tcp6":
protocol = syscall.IPPROTO_TCP
}
var uid int
if sdkVersion.Load() < 29 {
2025-07-31 17:09:18 +08:00
uid = platform.QuerySocketUidFromProcFs(source, target)
} else {
uid = th.callbacks.ResolveUid(protocol, source.String(), target.String())
}
if uid < 0 {
return -1, ""
2025-07-31 17:09:18 +08:00
}
return uid, th.callbacks.ResolvePackage(uid)
2025-07-31 17:09:18 +08:00
}
var (
installHooksOnce sync.Once
activeTunHandler atomic.Pointer[TunHandler]
)
func installHooks() {
installHooksOnce.Do(func() {
dialer.DefaultSocketHook = func(network, address string, conn syscall.RawConn) error {
if platform.ShouldBlockConnection() {
return errBlocked
}
th := activeTunHandler.Load()
if th == nil {
return nil
}
return conn.Control(func(fd uintptr) {
th.handleProtect(int(fd))
})
2025-07-31 17:09:18 +08:00
}
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
th := activeTunHandler.Load()
if th == nil {
return "", process.ErrInvalidNetwork
}
src, dst := metadata.RawSrcAddr, metadata.RawDstAddr
if src == nil || dst == nil {
return "", process.ErrInvalidNetwork
}
// Everywhere else mihomo fills Uid from its own procfs lookup, the one Android took away.
uid, packageName := th.handleResolveProcess(src, dst)
if uid >= 0 {
metadata.Uid = uint32(uid)
}
return packageName, nil
2025-07-31 17:09:18 +08:00
}
})
2025-07-31 17:09:18 +08:00
}
func (th *TunHandler) initHook() {
installHooks()
activeTunHandler.Store(th)
}
// Swap the handler, never the hook: mihomo nil-checks DefaultSocketHook once and
// dereferences it again when the socket is created, so clearing it mid-dial
// calls a nil func value.
2025-07-31 17:09:18 +08:00
func (th *TunHandler) removeHook() {
activeTunHandler.CompareAndSwap(th, nil)
2025-07-31 17:09:18 +08:00
}
var (
tunLock sync.Mutex
errBlocked = errors.New("blocked: the process is out of file descriptors")
tunHandler *TunHandler
2025-07-31 17:09:18 +08:00
)
func handleStopTun() {
tunLock.Lock()
defer tunLock.Unlock()
stopTunLocked()
}
func stopTunLocked() {
if tunHandler == nil {
return
2025-07-31 17:09:18 +08:00
}
tunHandler.close()
tunHandler = nil
2025-07-31 17:09:18 +08:00
}
func handleStartTun(callbacks TunCallbacks, fd int, stack, address, dns, options string) bool {
2025-07-31 17:09:18 +08:00
tunLock.Lock()
defer tunLock.Unlock()
stopTunLocked()
if fd == 0 {
logError("startTun was handed no tun descriptor")
return false
}
tunHandler = &TunHandler{
callbacks: callbacks,
2025-07-31 17:09:18 +08:00
}
if tunHandler.start(fd, stack, address, dns, options) {
return true
}
// start() already cleared the handler, so nothing protects sockets from
// here on. Android has the routes up regardless, so the caller has to tear
// the VPN down rather than leave the device pointed at a black hole.
tunHandler = nil
return false
2025-07-31 17:09:18 +08:00
}
var (
dnsUpdateMu sync.Mutex
dnsUpdateSeq atomic.Uint64
)
2025-07-31 17:09:18 +08:00
func handleUpdateDns(value string) {
// mihomo turns the [""] that Split returns for no servers into a "udp://:"
// nameserver, which keeps the system resolver off its default fallback.
var addr []string
if value != "" {
addr = strings.Split(value, ",")
}
seq := dnsUpdateSeq.Add(1)
safeGo("updateDns", func() {
dnsUpdateMu.Lock()
defer dnsUpdateMu.Unlock()
if seq != dnsUpdateSeq.Load() {
return
}
2025-07-31 17:09:18 +08:00
log.Infoln("[DNS] updateDns %s", value)
dns.UpdateSystemDNS(addr)
2025-07-31 17:09:18 +08:00
dns.FlushCacheWithDefaultResolver()
})
2024-11-09 20:17:57 +08:00
}
func init() {
registerMethod(updateDnsMethod, withArguments(func(value string) bool {
handleUpdateDns(value)
return true
}))
2025-05-02 02:24:12 +08:00
}
// gobind runs an export on the caller's thread and turns no panic into a Java
// exception, so a panic that leaves one ends the whole application.
func recoverExport(name string) {
if r := recover(); r != nil {
logError("panic in %s: %v\n%s", name, r, stackTrace())
}
}
func InvokeMethod(data string, reply func([]byte)) {
defer recoverExport("invokeMethod")
2026-08-05 10:55:33 +08:00
call := &MethodCall{}
if err := json.Unmarshal([]byte(data), call); err != nil {
reply(encodeResponse(MethodResponse{Error: &MethodError{
Code: "invalid_method_call",
Message: err.Error(),
}}))
2025-01-13 19:08:17 +08:00
return
}
dispatchMethodCall(call, reply)
2024-12-09 01:40:39 +08:00
}
func StartTun(callbacks TunCallbacks, fd int, stack, address, dns, options string) bool {
defer recoverExport("startTun")
started := handleStartTun(callbacks, fd, stack, address, dns, options)
if !started {
return false
}
if !isRunning.Load() {
2025-12-16 11:23:09 +08:00
handleStartListener()
} else {
handleResetConnections()
}
2025-07-31 17:09:18 +08:00
return true
}
func QuickSetup(initParamsString, setupParamsString string, answer func(string)) {
2025-12-16 11:23:09 +08:00
go func() {
defer func() {
if r := recover(); r != nil {
logError("panic in quickSetup: %v\n%s", r, stackTrace())
answer(fmt.Sprintf("internal panic: %v", r))
}
}()
2026-08-05 10:55:33 +08:00
initParams := InitParams{}
if err := json.Unmarshal([]byte(initParamsString), &initParams); err != nil {
answer("init failed")
2025-12-16 11:23:09 +08:00
return
}
handleInitClash(&initParams)
setupParams := &SetupParams{}
if err := json.Unmarshal([]byte(setupParamsString), setupParams); err != nil {
answer(err.Error())
2026-08-05 10:55:33 +08:00
return
}
isRunning.Store(true)
if err := handleSetupConfig(setupParams); err != nil {
answer(err.Error())
return
}
answer("")
2025-12-16 11:23:09 +08:00
}()
}
// The listener goes away with the Flutter engine, which never gets to release
// the route watch it held, while the service and this core run on.
func SetEventListener(listener func([]byte)) {
defer recoverExport("setEventListener")
eventListenerLock.Lock()
2025-07-31 17:09:18 +08:00
eventListener = listener
listening.Store(listener != nil)
eventListenerLock.Unlock()
if listener == nil {
stopRouteWatch()
}
}
func hasEventListener() bool {
return listening.Load()
2025-07-31 17:09:18 +08:00
}
func GetTotalTraffic(onlyStatisticsProxy bool) string {
defer recoverExport("getTotalTraffic")
return marshalResult(handleGetTotalTraffic(onlyStatisticsProxy))
2025-07-31 17:09:18 +08:00
}
func GetTraffic(onlyStatisticsProxy bool) string {
defer recoverExport("getTraffic")
return marshalResult(handleGetTraffic(onlyStatisticsProxy))
2025-07-31 17:09:18 +08:00
}
2026-08-05 10:55:33 +08:00
func marshalResult(value any) string {
data, err := json.Marshal(value)
if err != nil {
logError("Result marshal error: %v", err)
return ""
}
return string(data)
}
func deliverEvent(data []byte) {
eventListenerLock.RLock()
defer eventListenerLock.RUnlock()
2025-07-31 17:09:18 +08:00
if eventListener == nil {
2025-01-13 19:08:17 +08:00
return
}
eventListener(data)
2025-05-02 02:24:12 +08:00
}
func StopTun() {
defer recoverExport("stopTun")
2025-07-31 17:09:18 +08:00
handleStopTun()
if isRunning.Load() {
2025-12-16 11:23:09 +08:00
handleStopListener()
}
2025-07-31 17:09:18 +08:00
}
func StartListener() {
defer recoverExport("startListener")
if !isRunning.Load() {
handleStartListener()
}
}
func StopListener() {
defer recoverExport("stopListener")
if isRunning.Load() {
handleStopListener()
}
}
var forceGCPending atomic.Bool
// Android calls this from onLowMemory and onTrimMemory on the main thread, which
// the Flutter UI shares, and a full collection of a large profile spans frames.
func ForceGC() {
defer recoverExport("forceGC")
if !forceGCPending.CompareAndSwap(false, true) {
return
}
safeGo("forceGC", func() {
defer forceGCPending.Store(false)
handleForceGC()
})
2024-11-09 20:17:57 +08:00
}
func UpdateDns(value string) {
defer recoverExport("updateDns")
handleUpdateDns(value)
}
func ResetNetwork() {
defer recoverExport("resetNetwork")
safeGo("resetNetwork", handleResetNetwork)
2024-11-09 20:17:57 +08:00
}