mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 01:36:37 +08:00
fix(openflare): Agent 上报 IP 后由 Server 自动解析节点地理位置
- 启动时按 of_options.GeoIPProvider 初始化 pkg/geoip(bootstrap + runtime) - mmdb 模式从内置 GeoLite2 种子到 data/;保存归属方式后热刷新 Provider - Agent/Relay 心跳在服务端根据 IP 写入 geo 字段,尊重 geo_manual_override - ipinfo 归属名称改为 City, Region, Country 可读格式
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
)
|
||||
|
||||
// ApplyNodeGeoFromIP resolves geographic metadata for node when geo is not manually locked.
|
||||
func ApplyNodeGeoFromIP(ctx context.Context, node *model.OpenFlareNode, rawIP string) {
|
||||
if node == nil || node.GeoManualOverride {
|
||||
return
|
||||
}
|
||||
node.GeoName = ""
|
||||
node.GeoLatitude = nil
|
||||
node.GeoLongitude = nil
|
||||
|
||||
ip := net.ParseIP(strings.TrimSpace(rawIP))
|
||||
if ip == nil {
|
||||
return
|
||||
}
|
||||
|
||||
info, err := GeoInfoFromIP(ip)
|
||||
if err != nil {
|
||||
logger.WarnF(ctx, "[GeoIP] resolve node geo failed: ip=%s error=%v", ip.String(), err)
|
||||
return
|
||||
}
|
||||
if info == nil {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(info.Name) != "" {
|
||||
node.GeoName = strings.TrimSpace(info.Name)
|
||||
}
|
||||
if info.Latitude != nil && info.Longitude != nil {
|
||||
node.GeoLatitude = cloneCoordinate(info.Latitude)
|
||||
node.GeoLongitude = cloneCoordinate(info.Longitude)
|
||||
}
|
||||
}
|
||||
|
||||
func cloneCoordinate(value *float64) *float64 {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := *value
|
||||
return &cloned
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/agent/geoipdata"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
pkggeoip "github.com/Rain-kl/Wavelet/pkg/geoip"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
serverMMDBRelativePath = "data/GeoLite2-Country.mmdb"
|
||||
serverMMDBDirPerm = 0o750
|
||||
serverMMDBFilePerm = 0o644
|
||||
)
|
||||
|
||||
var (
|
||||
runtimeOnce sync.Once
|
||||
runtimeInitErr error
|
||||
currentProviderMu sync.RWMutex
|
||||
currentProvider string
|
||||
)
|
||||
|
||||
// EnsureRuntimeProvider loads OpenFlare options once and configures pkg/geoip.
|
||||
func EnsureRuntimeProvider(ctx context.Context) error {
|
||||
runtimeOnce.Do(func() {
|
||||
if err := model.InitOptionMap(ctx); err != nil {
|
||||
runtimeInitErr = err
|
||||
return
|
||||
}
|
||||
runtimeInitErr = applyProviderFromModel()
|
||||
})
|
||||
return runtimeInitErr
|
||||
}
|
||||
|
||||
// RefreshRuntimeProvider reapplies GeoIPProvider after option updates.
|
||||
func RefreshRuntimeProvider(ctx context.Context) error {
|
||||
if err := model.InitOptionMap(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return applyProviderFromModel()
|
||||
}
|
||||
|
||||
func applyProviderFromModel() error {
|
||||
model.OptionMapRWMutex.RLock()
|
||||
provider := strings.TrimSpace(model.GeoIPProvider)
|
||||
model.OptionMapRWMutex.RUnlock()
|
||||
return ApplyProvider(provider)
|
||||
}
|
||||
|
||||
// ApplyProvider switches the process-wide GeoIP backend.
|
||||
func ApplyProvider(provider string) error {
|
||||
normalized := strings.TrimSpace(strings.ToLower(provider))
|
||||
if normalized == "" {
|
||||
normalized = pkggeoip.ProviderDisabled
|
||||
}
|
||||
|
||||
currentProviderMu.Lock()
|
||||
if currentProvider == normalized {
|
||||
currentProviderMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
currentProvider = normalized
|
||||
currentProviderMu.Unlock()
|
||||
|
||||
if normalized == pkggeoip.ProviderMaxMind {
|
||||
path, err := ensureServerMMDB()
|
||||
if err != nil {
|
||||
logger.WarnF(context.Background(), "[GeoIP] seed MaxMind database failed: %v", err)
|
||||
}
|
||||
if path != "" {
|
||||
pkggeoip.GeoIPFilePath = path
|
||||
}
|
||||
}
|
||||
|
||||
pkggeoip.InitGeoIP(normalized)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureServerMMDB() (string, error) {
|
||||
path, err := filepath.Abs(serverMMDBRelativePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path, nil
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data, err := fs.ReadFile(geoipdata.FS, geoipdata.DefaultMMDBName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), serverMMDBDirPerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.WriteFile(path, data, serverMMDBFilePerm); err != nil { //nolint:gosec // world-readable mmdb
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// ResetRuntimeForTest clears lazy-init state for unit tests.
|
||||
func ResetRuntimeForTest() {
|
||||
runtimeOnce = sync.Once{}
|
||||
runtimeInitErr = nil
|
||||
currentProviderMu.Lock()
|
||||
currentProvider = ""
|
||||
currentProviderMu.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
pkggeoip "github.com/Rain-kl/Wavelet/pkg/geoip"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestEnsureRuntimeProviderInitializesConfiguredProvider(t *testing.T) {
|
||||
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := sqliteDB.AutoMigrate(&model.OpenFlareOption{}); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
db.SetDB(sqliteDB)
|
||||
t.Cleanup(func() {
|
||||
db.SetDB(nil)
|
||||
model.ResetOptionMapForTest()
|
||||
ResetRuntimeForTest()
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
model.ResetOptionMapForTest()
|
||||
ResetRuntimeForTest()
|
||||
if err := model.UpdateOpenFlareOption(ctx, "GeoIPProvider", pkggeoip.ProviderIPInfo); err != nil {
|
||||
t.Fatalf("update option: %v", err)
|
||||
}
|
||||
|
||||
if err := EnsureRuntimeProvider(ctx); err != nil {
|
||||
t.Fatalf("EnsureRuntimeProvider error = %v", err)
|
||||
}
|
||||
if pkggeoip.CurrentProvider == nil || pkggeoip.CurrentProvider.Name() == "EmptyProvider" {
|
||||
t.Fatalf("expected ipinfo provider, got %#v", pkggeoip.CurrentProvider)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user