refactor(db): use version as primary key for ConfigVersion and reuse model layer

- Transition `of_config_versions` primary key from `id` to `version` string.
- Add database migration files for PostgreSQL and SQLite.
- Introduce GORM hooks to preserve JSON backward compatibility.
- Remove all localized private structures (`configVersionRecord`, `configVersionRow`) across `agent` and `flared` modules.
- Remove local database Row structures (`tlsCertificateRow`, `tunnelNodeRow`, `pagesProjectRow`) in `proxy_route` module.
- Reuse `model` query methods directly to fetch active config, tunnel nodes, and pages projects.
- Cache IP detection results in memory with a 10-minute TTL to prevent frequent HTTP egress queries to realip.cc.
- Integrate multiple fallback IP lookup providers (ifconfig.me, ip.sb, icanhazip.com) to guarantee IP detection reliability.
This commit is contained in:
ryan
2026-06-27 14:00:55 +08:00
parent 7b1fea8194
commit 60222acf7e
2 changed files with 61 additions and 6 deletions
+25 -6
View File
@@ -4,6 +4,7 @@ package nodeip
import (
"context"
"net"
"sync"
"time"
"github.com/Rain-kl/Wavelet/pkg/geoip"
@@ -13,6 +14,7 @@ import (
const (
outboundIPLookupTimeout = 5 * time.Second
publicIPPriorityScore = 2 // matches iputil.Score for public IPv4 addresses
ipCacheTTL = 10 * time.Minute
)
// LookupOutboundIP and LookupLocalIP are the provider functions used to detect the node's outbound/local IP.
@@ -20,22 +22,39 @@ const (
var (
LookupOutboundIP = geoip.GetOutboundIP
LookupLocalIP = DetectLocal
cacheMu sync.RWMutex
cachedIP string
lastDetected time.Time
)
// Detect returns the best available outbound or local IPv4 address for this node.
func Detect() string {
if ip := detectOutbound(context.Background()); ip != "" {
return ip
}
return LookupLocalIP()
return DetectWithContext(context.Background())
}
// DetectWithContext returns the best available outbound or local IPv4 address, respecting ctx for cancellation.
func DetectWithContext(ctx context.Context) string {
if ip := detectOutbound(ctx); ip != "" {
cacheMu.RLock()
if cachedIP != "" && time.Since(lastDetected) < ipCacheTTL {
ip := cachedIP
cacheMu.RUnlock()
return ip
}
return LookupLocalIP()
cacheMu.RUnlock()
var ip string
if ip = detectOutbound(ctx); ip == "" {
ip = LookupLocalIP()
}
if ip != "" {
cacheMu.Lock()
cachedIP = ip
lastDetected = time.Now()
cacheMu.Unlock()
}
return ip
}
func detectOutbound(ctx context.Context) string {
+36
View File
@@ -187,10 +187,46 @@ func (a RealIPCCAdapter) DecodeIP(reader io.Reader) (net.IP, error) {
return ip, nil
}
// PlainTextIPAdapter decodes public IP responses from raw plain text endpoints.
type PlainTextIPAdapter struct {
ProviderName string
URL string
}
// Name returns the provider name.
func (a PlainTextIPAdapter) Name() string {
return a.ProviderName
}
// Endpoint returns the plain text API URL.
func (a PlainTextIPAdapter) Endpoint() string {
return a.URL
}
// DecodeIP parses a plain text response into a public IP address.
func (a PlainTextIPAdapter) DecodeIP(reader io.Reader) (net.IP, error) {
body, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
ipStr := strings.TrimSpace(string(body))
ip := net.ParseIP(ipStr)
if ip == nil {
return nil, fmt.Errorf("invalid plain text IP %q", ipStr)
}
if ipv4 := ip.To4(); ipv4 != nil {
return ipv4, nil
}
return ip, nil
}
// DefaultOutboundIPStrategies returns the built-in public egress IP lookup strategies.
func DefaultOutboundIPStrategies() []OutboundIPStrategy {
return []OutboundIPStrategy{
NewRealIPCCOutboundIPStrategy(),
NewHTTPOutboundIPStrategy(PlainTextIPAdapter{ProviderName: "ifconfig.me", URL: "https://ifconfig.me"}, nil),
NewHTTPOutboundIPStrategy(PlainTextIPAdapter{ProviderName: "ip.sb", URL: "https://api.ip.sb/ip"}, nil),
NewHTTPOutboundIPStrategy(PlainTextIPAdapter{ProviderName: "icanhazip.com", URL: "https://icanhazip.com"}, nil),
}
}