fix(core): send changed geo resource URLs to the Core without a full apply

The Core only learned geox-url from a full config apply, so after a link
was edited on the resources page both a manual sync and the auto updater
kept downloading from the previous link until the profile was applied
again. The URLs now travel with the update params, which are pushed
whenever the patch config changes. Only a link that changed is written,
because mihomo's updaters read these links without a lock.

Changelog: A changed resource link is now used by the next sync and by auto update
This commit is contained in:
chen08209
2026-10-02 12:08:25 +08:00
parent e55f6d1004
commit ff23f30c31
9 changed files with 138 additions and 31 deletions
+3
View File
@@ -318,6 +318,9 @@ func updateConfig(params *UpdateParams) error {
}
updateListeners(currentConfig)
for geoType, link := range params.GeoXUrl {
setGeoResourceUrl(geoType, link)
}
syncGeoUpdater(params.GeoAutoUpdate, params.GeoUpdateInterval)
if routeChanged {
bumpRouteEpoch()
+56 -4
View File
@@ -122,6 +122,55 @@ func TestUpdateConfigAppliesAllowLan(t *testing.T) {
}
}
func TestUpdateConfigAppliesGeoResourceUrls(t *testing.T) {
withCurrentConfig(t, &config.Config{General: &config.General{}, Controller: &config.Controller{}})
requested := map[string]string{
"mmdb": "https://example.test/geoip.metadb",
"asn": "https://example.test/GeoLite2-ASN.mmdb",
"geoip": "https://example.test/geoip.dat",
"geosite": "https://example.test/geosite.dat",
}
for _, resource := range geoResources {
resource, previous := resource, resource.url()
t.Cleanup(func() { resource.setUrl(previous) })
}
if err := updateConfig(&UpdateParams{GeoXUrl: requested}); err != nil {
t.Fatalf("updateConfig error: %v", err)
}
for key, want := range requested {
if got := geoResources[strings.ToUpper(key)].url(); got != want {
t.Errorf("%s url = %q, want %q; sync and the auto updater would keep the old link", key, got, want)
}
}
}
func TestSetGeoResourceUrlWritesOnlyAChangedLink(t *testing.T) {
const current = "https://example.test/geosite.dat"
var writes []string
previous := geoResources
geoResources = map[string]geoResource{
"GEOSITE": {
url: func() string { return current },
setUrl: func(link string) { writes = append(writes, link) },
},
}
t.Cleanup(func() { geoResources = previous })
setGeoResourceUrl("geosite", current)
setGeoResourceUrl("geosite", "")
setGeoResourceUrl("nope", "https://example.test/nope.dat")
if len(writes) != 0 {
t.Fatalf("writes = %q, want none for an unchanged, empty or unknown link", writes)
}
setGeoResourceUrl("geosite", "https://example.test/next.dat")
if len(writes) != 1 || writes[0] != "https://example.test/next.dat" {
t.Errorf("writes = %q, want the changed link once", writes)
}
}
func TestUpdateConfigAppliesAuthenticationAndClearsLoopbackExemptions(t *testing.T) {
withCurrentConfig(t, &config.Config{General: &config.General{}, Controller: &config.Controller{}})
currentConfig.General.SkipAuthPrefixes = []netip.Prefix{
@@ -359,7 +408,7 @@ func TestHandleUpdateGeoDataRejectsAnUnknownResource(t *testing.T) {
if message := handleUpdateGeoData("NOPE"); message == "" {
t.Error("an unknown geo resource reported success and silently did nothing")
}
if _, exists := geoResourceUpdaters["MMDB"]; !exists {
if _, exists := geoResources["MMDB"]; !exists {
t.Error("MMDB is missing from the geo resource table")
}
}
@@ -415,10 +464,13 @@ func settleMessageBatcher() {
func withGeoResourceUpdaters(t *testing.T, updaters map[string]func() error) {
t.Helper()
previous := geoResourceUpdaters
geoResourceUpdaters = updaters
previous := geoResources
geoResources = make(map[string]geoResource, len(updaters))
for name, update := range updaters {
geoResources[name] = geoResource{update: update}
}
t.Cleanup(func() {
geoResourceUpdaters = previous
geoResources = previous
for name := range updaters {
releaseGeoUpdate(name)
}
+1
View File
@@ -36,6 +36,7 @@ type UpdateParams struct {
Authentication *[]string `json:"authentication"`
GeoAutoUpdate *bool `json:"geo-auto-update"`
GeoUpdateInterval *int `json:"geo-update-interval"`
GeoXUrl map[string]string `json:"geox-url"`
}
type tunSchema struct {
+26 -7
View File
@@ -26,6 +26,7 @@ import (
"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/common/observable"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/geodata"
"github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/component/updater"
"github.com/metacubex/mihomo/config"
@@ -428,11 +429,29 @@ func handleGetExternalProvider(externalProviderName string) *ExternalProvider {
return externalProvider
}
var geoResourceUpdaters = map[string]func() error{
"MMDB": updater.UpdateMMDB,
"ASN": updater.UpdateASN,
"GEOIP": updater.UpdateGeoIp,
"GEOSITE": updater.UpdateGeoSite,
type geoResource struct {
update func() error
url func() string
setUrl func(string)
}
var geoResources = map[string]geoResource{
"MMDB": {update: updater.UpdateMMDB, url: geodata.MmdbUrl, setUrl: geodata.SetMmdbUrl},
"ASN": {update: updater.UpdateASN, url: geodata.ASNUrl, setUrl: geodata.SetASNUrl},
"GEOIP": {update: updater.UpdateGeoIp, url: geodata.GeoIpUrl, setUrl: geodata.SetGeoIpUrl},
"GEOSITE": {update: updater.UpdateGeoSite, url: geodata.GeoSiteUrl, setUrl: geodata.SetGeoSiteUrl},
}
// mihomo's updaters read these links without a lock, so an unchanged one is never rewritten.
func setGeoResourceUrl(geoType string, link string) {
resource, exist := geoResources[strings.ToUpper(geoType)]
if !exist {
log.Warnln("geox-url: unknown geo resource %q", geoType)
return
}
if link != "" && link != resource.url() {
resource.setUrl(link)
}
}
const (
@@ -494,7 +513,7 @@ func releaseGeoUpdateFromHook(geoType string) {
}
func handleUpdateGeoData(geoType string) string {
update, exist := geoResourceUpdaters[geoType]
resource, exist := geoResources[geoType]
if !exist {
logError("updateGeoData: unknown geo resource %q", geoType)
return "unknown geo resource: " + geoType
@@ -504,7 +523,7 @@ func handleUpdateGeoData(geoType string) string {
}
safeGoDetached("updateGeoData("+geoType+")", func() {
defer releaseGeoUpdate(geoType)
if err := update(); err != nil {
if err := resource.update(); err != nil {
logError("updateGeoData(%s) error: %v", geoType, err)
}
})