Files
FlClash/core/hub.go
T
chen08209 1e9955dec6 fix(core,android): stop suspending the Core in Doze
The suspended tunnel rejected the traffic Doze still lets through, which
kept FCM from reconnecting, and the re-probe on resume tested every node,
select groups included.

Changelog: Push notifications reconnect in Doze
2026-10-02 20:32:42 +08:00

859 lines
20 KiB
Go

package main
import (
"cmp"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"io"
"maps"
"net"
"net/url"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"slices"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/common/observable"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/constant/features"
cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/dns"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/listener"
"github.com/metacubex/mihomo/log"
rp "github.com/metacubex/mihomo/rules/provider"
"github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic"
)
var (
logMu sync.Mutex
logSubscriber observable.Subscription[log.Event]
logCancel context.CancelFunc
)
func handleInitClash(params *InitParams) bool {
configMu.Lock()
defer configMu.Unlock()
sdkVersion.Store(int32(params.Version))
constant.SetHomeDir(params.HomeDir)
initOwnership(params.HomeDir)
isInit.Store(true)
return true
}
func handleStartListener() bool {
configMu.Lock()
defer configMu.Unlock()
isRunning.Store(true)
updateListeners(currentConfig)
resolver.ResetConnection()
refreshRoute()
return true
}
func handleStopListener() bool {
configMu.Lock()
defer configMu.Unlock()
isRunning.Store(false)
listener.StopListener()
resolver.ResetConnection()
return true
}
func handleGetIsInit() bool {
return isInit.Load()
}
func handleForceGC() {
log.Infoln("[APP] request force GC")
tunnel.InvalidateAllProxies()
runtime.GC()
if features.Android {
debug.FreeOSMemory()
}
}
func handleShutdown() bool {
handleStopLog()
stopRouteWatch()
configMu.Lock()
isRunning.Store(false)
listener.StopListener()
updater.StopGeoUpdater()
executor.Shutdown()
currentConfig = nil
isInit.Store(false)
configMu.Unlock()
handleForceGC()
return true
}
func handleValidateConfig(path string) string {
buf, err := os.ReadFile(path)
if err != nil {
return err.Error()
}
if _, err = config.UnmarshalRawConfig(buf); err != nil {
return err.Error()
}
return ""
}
// Tailscale and EasyTier register a DNS client under the proxy name when built
// and drop that name's entry on Close, so a probe under the real name would
// take the resolver from a running proxy of the same name.
const validationNameSuffix = "\x00validate"
// The per-proxy parser the config load runs; checks across proxies, such as
// duplicate names, are left to the load itself.
func handleValidateProxies(mappings []map[string]any) []string {
results := make([]string, len(mappings))
for i, mapping := range mappings {
name, _ := mapping["name"].(string)
probe := mapping
switch mapping["type"] {
case "tailscale", "easytier":
probe = maps.Clone(mapping)
probe["name"] = name + validationNameSuffix
}
proxy, err := adapter.ParseProxy(probe)
if err != nil {
results[i] = strings.ReplaceAll(err.Error(), validationNameSuffix, "")
continue
}
_ = proxy.Close()
}
return results
}
const globalProxyName = "GLOBAL"
func isProxyGroupType(adapterType constant.AdapterType) bool {
switch adapterType {
case constant.Selector, constant.URLTest, constant.Fallback, constant.Relay, constant.LoadBalance:
return true
default:
return false
}
}
func proxyGroupNames(
nameList []string,
typeOf func(name string) (constant.AdapterType, bool),
) []string {
hasGlobal := false
names := make([]string, 0, len(nameList)+1)
for _, name := range nameList {
if name == globalProxyName {
hasGlobal = true
}
adapterType, ok := typeOf(name)
if !ok || !isProxyGroupType(adapterType) {
continue
}
names = append(names, name)
}
if !hasGlobal {
if adapterType, ok := typeOf(globalProxyName); ok && isProxyGroupType(adapterType) {
names = append([]string{globalProxyName}, names...)
}
}
return names
}
func handleGetProxies() ProxiesData {
proxies := tunnel.AllProxies()
allNames := proxyGroupNames(config.GetProxyNameList(), func(name string) (constant.AdapterType, bool) {
p, ok := proxies[name]
if !ok || p == nil {
return 0, false
}
return p.Type(), true
})
views := make(map[string]any, len(proxies))
for name, proxy := range proxies {
views[name] = proxyView(proxy)
}
return ProxiesData{
All: allNames,
Proxies: views,
}
}
// Proxy.MarshalJSON encodes each node twice, with history the host never reads.
func proxyView(proxy constant.Proxy) any {
node := nodeView{Name: proxy.Name(), Type: proxy.Type().String()}
if !isProxyGroupType(proxy.Type()) {
return node
}
data, err := proxy.Adapter().MarshalJSON()
view := map[string]any{}
if err == nil {
err = json.Unmarshal(data, &view)
}
if err != nil {
log.Warnln("[APP] encode group %s: %v", node.Name, err)
return node
}
view["name"] = node.Name
return view
}
var (
errGroupNotFound = errors.New("Not found group")
errGroupInvalidType = errors.New("Group has invalid proxy type")
errGroupNotSelect = errors.New("Group is not selectable")
)
func lookupProxy(name string) constant.Proxy {
return tunnel.AllProxies()[name]
}
func selectableGroup(groupName string) (pickableGroup, error) {
group := lookupProxy(groupName)
if group == nil {
return nil, errGroupNotFound
}
adapterProxy, ok := group.(*adapter.Proxy)
if !ok {
return nil, errGroupInvalidType
}
selector, ok := adapterProxy.ProxyAdapter.(pickableGroup)
if !ok {
return nil, errGroupNotSelect
}
return selector, nil
}
func handleChangeProxy(params *ChangeProxyParams) *ChangeProxyResult {
selectMu.Lock()
defer selectMu.Unlock()
selector, err := selectableGroup(params.GroupName)
if err != nil {
return &ChangeProxyResult{Message: err.Error()}
}
before := selector.Now()
if params.ProxyName == "" {
selector.ForceSet(params.ProxyName)
} else if err := selector.Set(params.ProxyName); err != nil {
return &ChangeProxyResult{Message: err.Error()}
}
changed := selector.Now() != before
refreshRouteLocked(false)
return &ChangeProxyResult{Changed: changed}
}
func handleGetTraffic(onlyStatisticsProxy bool) Traffic {
up, down := statistic.DefaultManager.NowTraffic(onlyStatisticsProxy)
return Traffic{
Up: up,
Down: down,
}
}
func handleGetTotalTraffic(onlyStatisticsProxy bool) Traffic {
up, down := statistic.DefaultManager.TotalTraffic(onlyStatisticsProxy)
return Traffic{
Up: up,
Down: down,
}
}
func handleResetTraffic() {
statistic.DefaultManager.ResetStatistic()
}
func delayValue(delay uint16) int32 {
if delay == 0 {
return -1
}
return int32(delay)
}
var anyDelayTestStatus utils.IntRanges[uint16]
func delayTestTimeout(milliseconds int64) time.Duration {
if milliseconds <= 0 {
return defaultDelayTestTimeout
}
return time.Duration(milliseconds) * time.Millisecond
}
var missingDelayTestProxyAt atomic.Int64
// A delay test against a name the tunnel does not know is what an apply that
// fell back to the default config looks like from the outside: the profile
// still lists every node and every one of them reports Timeout. Say so, once a
// second rather than once per node, so the log names the real failure.
func reportMissingDelayTestProxy(name string) {
now := time.Now().UnixNano()
last := missingDelayTestProxyAt.Load()
if last != 0 && now-last < int64(time.Second) {
return
}
if !missingDelayTestProxyAt.CompareAndSwap(last, now) {
return
}
logError("delay test: %q is not part of the applied config", name)
}
func handleTestDelay(params *TestDelayParams) *Delay {
url := params.TestUrl
if url == "" {
url = currentTestURL()
}
delayData := &Delay{
Name: params.ProxyName,
Url: url,
Value: -1,
}
proxy := lookupProxy(params.ProxyName)
if proxy == nil {
reportMissingDelayTestProxy(params.ProxyName)
return delayData
}
timeout := delayTestTimeout(params.Timeout)
// Queueing for a slot and probing the node each get the full timeout.
// Sharing one deadline meant a node that waited four seconds behind a
// saturated semaphore had one second left to connect, so a bulk test of a
// large subscription reported Timeout for whatever happened to be at the
// back of the queue.
queueCtx, cancelQueue := context.WithTimeout(context.Background(), timeout)
granted := acquireDelayTestSlot(queueCtx)
cancelQueue()
if !granted {
return nil
}
defer releaseDelayTestSlot()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
delay, err := proxy.URLTest(ctx, url, anyDelayTestStatus)
if err != nil {
return delayData
}
delayData.Value = delayValue(delay)
return delayData
}
func handleGetConnections() *statistic.Snapshot {
return statistic.DefaultManager.Snapshot()
}
func handleGetConnectionCount() int {
count := 0
statistic.DefaultManager.Range(func(statistic.Tracker) bool {
count++
return true
})
return count
}
func handleCloseConnections() bool {
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
_ = c.Close()
return true
})
return true
}
func handleResetConnections() bool {
resolver.ResetConnection()
return true
}
func handleCloseConnection(connectionId string) bool {
c := statistic.DefaultManager.Get(connectionId)
if c == nil {
return false
}
_ = c.Close()
return true
}
func handleGetExternalProviders() []ExternalProvider {
providers := externalProviders()
eps := make([]ExternalProvider, 0, len(providers))
for _, p := range providers {
externalProvider, err := toExternalProvider(p)
if err != nil {
continue
}
eps = append(eps, *externalProvider)
}
slices.SortFunc(eps, func(a, b ExternalProvider) int {
return cmp.Compare(a.Name, b.Name)
})
return eps
}
func handleGetExternalProvider(externalProviderName string) *ExternalProvider {
p, exist := lookupExternalProvider(externalProviderName)
if !exist {
return nil
}
externalProvider, err := toExternalProvider(p)
if err != nil {
return nil
}
return externalProvider
}
var geoResourceUpdaters = map[string]func() error{
"MMDB": updater.UpdateMMDB,
"ASN": updater.UpdateASN,
"GEOIP": updater.UpdateGeoIp,
"GEOSITE": updater.UpdateGeoSite,
}
const (
geoUpdateScope = "geo:"
providerUpdateScope = "provider:"
)
var (
updateMu sync.Mutex
updateInFlight = map[string]bool{}
geoHookClaims = map[string]bool{}
)
func claimUpdate(key string) bool {
updateMu.Lock()
defer updateMu.Unlock()
if updateInFlight[key] {
return false
}
updateInFlight[key] = true
return true
}
func releaseUpdate(key string) {
updateMu.Lock()
defer updateMu.Unlock()
delete(updateInFlight, key)
delete(geoHookClaims, key)
}
func claimGeoUpdate(geoType string) bool {
return claimUpdate(geoUpdateScope + geoType)
}
func releaseGeoUpdate(geoType string) {
releaseUpdate(geoUpdateScope + geoType)
}
func claimGeoUpdateFromHook(geoType string) {
key := geoUpdateScope + geoType
updateMu.Lock()
defer updateMu.Unlock()
if updateInFlight[key] {
return
}
updateInFlight[key] = true
geoHookClaims[key] = true
}
func releaseGeoUpdateFromHook(geoType string) {
key := geoUpdateScope + geoType
updateMu.Lock()
defer updateMu.Unlock()
if !geoHookClaims[key] {
return
}
delete(geoHookClaims, key)
delete(updateInFlight, key)
}
func handleUpdateGeoData(geoType string) string {
update, exist := geoResourceUpdaters[geoType]
if !exist {
logError("updateGeoData: unknown geo resource %q", geoType)
return "unknown geo resource: " + geoType
}
if !claimGeoUpdate(geoType) {
return "geo update already in progress: " + geoType
}
safeGoDetached("updateGeoData("+geoType+")", func() {
defer releaseGeoUpdate(geoType)
if err := update(); err != nil {
logError("updateGeoData(%s) error: %v", geoType, err)
}
})
return ""
}
type providerRequestFailure struct {
code string
reason string
statusCode int
}
// The reason values are matched by lib/common/network_error.dart.
func classifyProviderRequestError(err error) (providerRequestFailure, bool) {
// mihomo reports a non-2xx fetch as errors.New(resp.Status).
message := err.Error()
if len(message) >= 4 && message[3] == ' ' {
status, parseErr := strconv.Atoi(message[:3])
if parseErr == nil && status >= 100 && status <= 599 {
return providerRequestFailure{
code: "request_bad_response",
statusCode: status,
}, true
}
}
var urlError *url.Error
var networkError net.Error
if reason := providerRequestFailureReason(err); reason != "" ||
errors.As(err, &urlError) ||
errors.As(err, &networkError) {
return providerRequestFailure{code: "request_error", reason: reason}, true
}
return providerRequestFailure{}, false
}
func providerRequestFailureReason(err error) string {
var unknownAuthority x509.UnknownAuthorityError
var hostname x509.HostnameError
var invalidCertificate x509.CertificateInvalidError
var certificateVerification *tls.CertificateVerificationError
var recordHeader tls.RecordHeaderError
var alert tls.AlertError
var dnsError *net.DNSError
var networkError net.Error
var opError *net.OpError
switch {
case errors.As(err, &unknownAuthority),
errors.As(err, &hostname),
errors.As(err, &invalidCertificate),
errors.As(err, &certificateVerification),
errors.As(err, &recordHeader),
errors.As(err, &alert):
return "tls"
case errors.As(err, &dnsError), errors.Is(err, resolver.ErrIPNotFound):
return "dns"
case errors.Is(err, context.DeadlineExceeded),
errors.Is(err, os.ErrDeadlineExceeded),
errors.As(err, &networkError) && networkError.Timeout():
return "timeout"
case errors.As(err, &opError),
errors.Is(err, syscall.ECONNREFUSED),
errors.Is(err, syscall.ECONNRESET),
errors.Is(err, io.EOF),
errors.Is(err, io.ErrUnexpectedEOF):
return "connection"
}
return ""
}
func providerMethodError(code, providerName string, err error) *MethodError {
return &MethodError{
Code: code,
Message: err.Error(),
Details: map[string]any{"providerName": providerName},
}
}
func providerRequestMethodError(
failure providerRequestFailure,
providerName string,
err error,
) *MethodError {
details := map[string]any{"providerName": providerName}
if failure.reason != "" {
details["reason"] = failure.reason
}
if failure.statusCode != 0 {
details["statusCode"] = failure.statusCode
}
return &MethodError{
Code: failure.code,
Message: err.Error(),
Details: details,
}
}
func handleUpdateExternalProvider(providerName string) *MethodError {
p, exist := lookupExternalProvider(providerName)
if !exist {
return providerMethodError(
"provider_not_found",
providerName,
errors.New("external provider does not exist"),
)
}
key := providerUpdateScope + providerName
if !claimUpdate(key) {
return providerMethodError(
"provider_updating",
providerName,
errors.New("external provider is updating"),
)
}
defer releaseUpdate(key)
if err := p.Update(); err != nil {
if failure, ok := classifyProviderRequestError(err); ok {
return providerRequestMethodError(failure, providerName, err)
}
return providerMethodError("provider_update_error", providerName, err)
}
refreshRoute()
return nil
}
func handleSideLoadExternalProvider(providerName string, data []byte) *MethodError {
p, exist := lookupExternalProvider(providerName)
if !exist {
return providerMethodError(
"provider_not_found",
providerName,
errors.New("external provider does not exist"),
)
}
key := providerUpdateScope + providerName
if !claimUpdate(key) {
return providerMethodError(
"provider_updating",
providerName,
errors.New("external provider is updating"),
)
}
defer releaseUpdate(key)
if err := sideUpdateExternalProvider(p, data); err != nil {
return providerMethodError("provider_update_error", providerName, err)
}
refreshRoute()
return nil
}
func handleStartLog() {
logMu.Lock()
if logCancel != nil {
logCancel()
logCancel = nil
}
if logSubscriber != nil {
log.UnSubscribe(logSubscriber)
logSubscriber = nil
}
ctx, cancel := context.WithCancel(context.Background())
subscriber := log.Subscribe()
logSubscriber = subscriber
logCancel = cancel
logMu.Unlock()
go func() {
defer func() {
logMu.Lock()
if logSubscriber == subscriber {
log.UnSubscribe(subscriber)
logSubscriber = nil
logCancel = nil
}
logMu.Unlock()
}()
for {
select {
case <-ctx.Done():
return
case logData, ok := <-subscriber:
if !ok {
return
}
if logData.LogLevel < log.Level() {
continue
}
sendMessage(Message{
Type: LogMessage,
Data: logData,
})
}
}
}()
}
func handleStopLog() {
logMu.Lock()
defer logMu.Unlock()
if logCancel != nil {
logCancel()
logCancel = nil
}
if logSubscriber != nil {
log.UnSubscribe(logSubscriber)
logSubscriber = nil
}
}
// HeapIdle still counts spans the runtime has already handed back to the OS,
// so the retained-but-unused figure subtracts HeapReleased.
func handleGetMemoryStats() MemoryStats {
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
return MemoryStats{
Rss: statistic.DefaultManager.Memory(),
HeapInuse: stats.HeapInuse,
HeapIdle: stats.HeapIdle - stats.HeapReleased,
StackInuse: stats.StackInuse,
RuntimeOther: stats.MSpanInuse + stats.MCacheInuse + stats.BuckHashSys + stats.GCSys + stats.OtherSys,
}
}
func handleGetConfig(path string) (*config.RawConfig, error) {
buf, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return config.UnmarshalRawConfig(buf)
}
func handleDumpRuleSet(path string) (string, error) {
buf, err := os.ReadFile(path)
if err != nil {
return "", err
}
mrsBehaviors := []cp.RuleBehavior{cp.Domain, cp.IPCIDR}
var errs []error
for _, behavior := range mrsBehaviors {
var text strings.Builder
err := rp.ConvertToMrs(buf, behavior, cp.MrsRule, &text)
if err == nil {
return text.String(), nil
}
errs = append(errs, err)
}
return "", errors.Join(errs...)
}
func handleCrash() {
panic("handle invoke crash")
}
func handleUpdateConfig(params *UpdateParams) string {
if err := updateConfig(params); err != nil {
return err.Error()
}
return ""
}
// The profile ID is an int64 rendered through strconv, so the last element can
// never carry a separator or a `..` — that is what keeps handleClearEffect from
// becoming a general-purpose privileged file deletion API.
func providerPaths(homeDir string, profileId int64) (root string, target string) {
root = filepath.Join(homeDir, "profiles", "providers")
return root, filepath.Join(root, strconv.FormatInt(profileId, 10))
}
// handleClearEffect derives the provider directory from a profile ID so the
// method cannot be used as a general-purpose privileged file deletion API.
func handleClearEffect(profileId int64) string {
if !isInit.Load() {
return "not initialized"
}
if profileId <= 0 {
return "invalid profile id"
}
providersRoot, providersPath := providerPaths(constant.Path.HomeDir(), profileId)
if err := os.RemoveAll(providersPath); err != nil {
return err.Error()
}
_ = os.Remove(providersRoot)
return ""
}
var setupConfig = applyConfig
func handleSetupConfig(params *SetupParams) string {
if !isInit.Load() {
return "not initialized"
}
if err := setupConfig(params); err != nil {
return err.Error()
}
return ""
}
func init() {
adapter.UrlTestHook = func(url string, name string, delay uint16) {
sendMessage(Message{
Type: DelayMessage,
Data: &Delay{
Url: url,
Name: name,
Value: delayValue(delay),
},
})
}
statistic.DefaultRequestNotify = func(c statistic.Tracker) {
notifyProbeRoute(c)
sendMessage(Message{
Type: RequestMessage,
Data: c,
})
}
dns.DefaultQueryNotify = func(record dns.QueryRecord) {
sendMessage(Message{
Type: DnsMessage,
Data: newDnsQuery(record),
})
}
executor.DefaultProviderLoadedHook = func(providerName string) {
scheduleReclaimOwnership()
sendMessage(Message{
Type: LoadedMessage,
Data: providerName,
})
}
updater.GeoUpdateHook = func(geoType string, updating bool, skipped bool, updateErr error) {
if updating {
claimGeoUpdateFromHook(geoType)
} else {
releaseGeoUpdateFromHook(geoType)
scheduleReclaimOwnership()
if !skipped && updateErr == nil {
bumpRouteEpoch()
}
}
status := GeoUpdateStatus{
Type: geoType,
Updating: updating,
Skipped: skipped,
}
if updateErr != nil {
status.Error = updateErr.Error()
}
sendMessage(Message{
Type: GeoUpdateMessage,
Data: status,
})
}
}