mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-03 23:06:36 +08:00
refactor(edge): unify dynamic IP detection and prioritize IPv4 reporting
- Align agent, relay, and flared to dynamically resolve IP during heartbeat using the nodeip package (when not manually configured). - Update GeoIP outbound IP strategy to prefer IPv4 HTTP client lookup using tcp4 dialer and fall back to dual-stack tcp. - Optimize agent profile fingerprinting to exclude dynamic UptimeSeconds and ReportedAtUnix fields, preventing redundant updates. - Refactor unit tests to prevent outbound network queries during tests.
This commit is contained in:
@@ -28,6 +28,10 @@ sidebar: false
|
|||||||
|
|
||||||
### 变更
|
### 变更
|
||||||
|
|
||||||
|
- 优化并统一 `agent`、`relay`、`flared` 的 IP 探测与上报逻辑,均复用公用 `nodeip` 包;在未指定 `node_ip` 时实现心跳 Tick 动态探测上报。
|
||||||
|
- 优化 `pkg/geoip.GetOutboundIP` 出口 IP 探测策略,优先通过 `tcp4` 建立 HTTP 连接以获得 IPv4 公网地址,并在纯 IPv6/无 IPv4 路由环境下自动降级为双栈 `tcp` 握手。
|
||||||
|
- 优化 `agent` 系统指纹缓存算法,计算指纹时排除 `UptimeSeconds` 和 `ReportedAtUnix` 动态字段,防止周期心跳时不断触发冗余完整的系统 Profile 数据上报。
|
||||||
|
|
||||||
- 彻底移除废弃的 GitHub OAuth 和微信登录相关遗留设置项(包括 `GitHubOAuthEnabled`、`GitHubClientId`、`GitHubClientSecret`、`WeChatAuthEnabled` 等),从公开状态接口 `/api/v1/d/status` 移除这些字段的返回。
|
- 彻底移除废弃的 GitHub OAuth 和微信登录相关遗留设置项(包括 `GitHubOAuthEnabled`、`GitHubClientId`、`GitHubClientSecret`、`WeChatAuthEnabled` 等),从公开状态接口 `/api/v1/d/status` 移除这些字段的返回。
|
||||||
|
|
||||||
- 前端路由调整:将 TLS 证书和 DNS 账号的路由地址移出 `/websites`(分别变更为顶级路由 `/certificates` 和 `/dns-accounts`),将 WAF IP 组的路由地址移出 `/waf`(变更为顶级路由 `/ip-groups`)。
|
- 前端路由调整:将 TLS 证书和 DNS 账号的路由地址移出 `/websites`(分别变更为顶级路由 `/certificates` 和 `/dns-accounts`),将 WAF IP 组的路由地址移出 `/waf`(变更为顶级路由 `/ip-groups`)。
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ func TestRunnerKeepsHeartbeatWhenStartupSyncFails(t *testing.T) {
|
|||||||
AccessToken: "agent-token",
|
AccessToken: "agent-token",
|
||||||
NodeName: "edge-01",
|
NodeName: "edge-01",
|
||||||
NodeIP: "10.0.0.8",
|
NodeIP: "10.0.0.8",
|
||||||
|
NodeIPConfigured: true,
|
||||||
Version: config.Version,
|
Version: config.Version,
|
||||||
ExtVersion: "1.27.1.2",
|
ExtVersion: "1.27.1.2",
|
||||||
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
|
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
|
||||||
@@ -264,6 +265,7 @@ func TestRunnerDoesNotExitOnHeartbeatOrSyncError(t *testing.T) {
|
|||||||
AccessToken: "agent-token",
|
AccessToken: "agent-token",
|
||||||
NodeName: "edge-01",
|
NodeName: "edge-01",
|
||||||
NodeIP: "10.0.0.8",
|
NodeIP: "10.0.0.8",
|
||||||
|
NodeIPConfigured: true,
|
||||||
Version: config.Version,
|
Version: config.Version,
|
||||||
ExtVersion: "1.27.1.2",
|
ExtVersion: "1.27.1.2",
|
||||||
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
|
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
|
||||||
@@ -322,6 +324,7 @@ func TestRunnerReportsOpenrestyHealthAndExecutesRestart(t *testing.T) {
|
|||||||
AccessToken: "agent-token",
|
AccessToken: "agent-token",
|
||||||
NodeName: "edge-01",
|
NodeName: "edge-01",
|
||||||
NodeIP: "10.0.0.8",
|
NodeIP: "10.0.0.8",
|
||||||
|
NodeIPConfigured: true,
|
||||||
Version: config.Version,
|
Version: config.Version,
|
||||||
ExtVersion: "1.27.1.2",
|
ExtVersion: "1.27.1.2",
|
||||||
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
|
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
|
||||||
@@ -375,6 +378,7 @@ func TestRunnerHeartbeatPayloadIncludesObservabilityExtensions(t *testing.T) {
|
|||||||
Config: &config.Config{
|
Config: &config.Config{
|
||||||
NodeName: "edge-observe-1",
|
NodeName: "edge-observe-1",
|
||||||
NodeIP: "10.0.0.51",
|
NodeIP: "10.0.0.51",
|
||||||
|
NodeIPConfigured: true,
|
||||||
Version: config.Version,
|
Version: config.Version,
|
||||||
ExtVersion: "1.27.1.2",
|
ExtVersion: "1.27.1.2",
|
||||||
DataDir: tempDir,
|
DataDir: tempDir,
|
||||||
@@ -458,6 +462,7 @@ func TestRunnerReplaysBufferedObservabilityAfterHeartbeatRecovery(t *testing.T)
|
|||||||
AccessToken: "agent-token",
|
AccessToken: "agent-token",
|
||||||
NodeName: "edge-buffer-01",
|
NodeName: "edge-buffer-01",
|
||||||
NodeIP: "10.0.0.52",
|
NodeIP: "10.0.0.52",
|
||||||
|
NodeIPConfigured: true,
|
||||||
Version: config.Version,
|
Version: config.Version,
|
||||||
ExtVersion: "1.27.1.2",
|
ExtVersion: "1.27.1.2",
|
||||||
DataDir: tempDir,
|
DataDir: tempDir,
|
||||||
@@ -537,6 +542,7 @@ func TestRunnerDiscoveryRegisterUpdatesTokenAndNodeID(t *testing.T) {
|
|||||||
DiscoveryToken: cfg.DiscoveryToken,
|
DiscoveryToken: cfg.DiscoveryToken,
|
||||||
NodeName: cfg.NodeName,
|
NodeName: cfg.NodeName,
|
||||||
NodeIP: cfg.NodeIP,
|
NodeIP: cfg.NodeIP,
|
||||||
|
NodeIPConfigured: cfg.NodeIPConfigured,
|
||||||
Version: config.Version,
|
Version: config.Version,
|
||||||
ExtVersion: "1.27.1.2",
|
ExtVersion: "1.27.1.2",
|
||||||
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
|
HeartbeatInterval: config.MillisecondDuration(10 * time.Millisecond),
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ type Config struct {
|
|||||||
HeartbeatInterval MillisecondDuration `json:"heartbeat_interval"`
|
HeartbeatInterval MillisecondDuration `json:"heartbeat_interval"`
|
||||||
RequestTimeout MillisecondDuration `json:"request_timeout"`
|
RequestTimeout MillisecondDuration `json:"request_timeout"`
|
||||||
configPath string `json:"-"`
|
configPath string `json:"-"`
|
||||||
|
NodeIPConfigured bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type configFile struct {
|
type configFile struct {
|
||||||
@@ -129,6 +130,7 @@ func Load(path string) (*Config, error) {
|
|||||||
}
|
}
|
||||||
cfg.configPath = path
|
cfg.configPath = path
|
||||||
applyEnvOverrides(cfg)
|
applyEnvOverrides(cfg)
|
||||||
|
cfg.NodeIPConfigured = cfg.NodeIP != ""
|
||||||
applyDefaults(cfg, filepath.Dir(path))
|
applyDefaults(cfg, filepath.Dir(path))
|
||||||
if err = validate(cfg); err != nil {
|
if err = validate(cfg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/Rain-kl/Wavelet/internal/apps/agent/state"
|
"github.com/Rain-kl/Wavelet/internal/apps/agent/state"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/agent/updater"
|
"github.com/Rain-kl/Wavelet/internal/apps/agent/updater"
|
||||||
edgeheartbeat "github.com/Rain-kl/Wavelet/internal/apps/edge/heartbeat"
|
edgeheartbeat "github.com/Rain-kl/Wavelet/internal/apps/edge/heartbeat"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/apps/edge/nodeip"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SyncService is the interface used by Cycle to sync active configuration and WAF IP groups.
|
// SyncService is the interface used by Cycle to sync active configuration and WAF IP groups.
|
||||||
@@ -97,10 +98,16 @@ func (c *Cycle) NodePayload(ctx context.Context, nodeID string) protocol.NodePay
|
|||||||
metricSnapshot := observability.BuildSnapshot(c.Config, c.StateStore)
|
metricSnapshot := observability.BuildSnapshot(c.Config, c.StateStore)
|
||||||
openrestyObservation := observability.BuildOpenrestyObservation(managedOpenRestyMetrics)
|
openrestyObservation := observability.BuildOpenrestyObservation(managedOpenRestyMetrics)
|
||||||
healthEvents := observability.BuildHealthEvents(snapshot)
|
healthEvents := observability.BuildHealthEvents(snapshot)
|
||||||
|
|
||||||
|
ip := c.Config.NodeIP
|
||||||
|
if !c.Config.NodeIPConfigured {
|
||||||
|
ip = nodeip.DetectWithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
payload := protocol.NodePayload{
|
payload := protocol.NodePayload{
|
||||||
NodeID: nodeID,
|
NodeID: nodeID,
|
||||||
Name: c.Config.NodeName,
|
Name: c.Config.NodeName,
|
||||||
IP: c.Config.NodeIP,
|
IP: ip,
|
||||||
Version: c.Config.Version,
|
Version: c.Config.Version,
|
||||||
ExtVersion: c.Config.ExtVersion,
|
ExtVersion: c.Config.ExtVersion,
|
||||||
CurrentVersion: snapshot.CurrentVersion,
|
CurrentVersion: snapshot.CurrentVersion,
|
||||||
|
|||||||
@@ -148,7 +148,10 @@ func collectProfile(cfg *config.Config) *protocol.NodeSystemProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fingerprintProfile(profile *protocol.NodeSystemProfile) string {
|
func fingerprintProfile(profile *protocol.NodeSystemProfile) string {
|
||||||
raw, err := json.Marshal(profile)
|
cloned := *profile
|
||||||
|
cloned.UptimeSeconds = 0
|
||||||
|
cloned.ReportedAtUnix = 0
|
||||||
|
raw, err := json.Marshal(&cloned)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type Config struct {
|
|||||||
HeartbeatInterval MillisecondDuration `json:"heartbeat_interval"`
|
HeartbeatInterval MillisecondDuration `json:"heartbeat_interval"`
|
||||||
RequestTimeout MillisecondDuration `json:"request_timeout"`
|
RequestTimeout MillisecondDuration `json:"request_timeout"`
|
||||||
configPath string
|
configPath string
|
||||||
|
NodeIPConfigured bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load reads configuration from path, applying environment overrides and defaults.
|
// Load reads configuration from path, applying environment overrides and defaults.
|
||||||
@@ -54,6 +55,7 @@ func Load(path string) (*Config, error) {
|
|||||||
}
|
}
|
||||||
cfg.configPath = path
|
cfg.configPath = path
|
||||||
applyEnvOverrides(cfg)
|
applyEnvOverrides(cfg)
|
||||||
|
cfg.NodeIPConfigured = cfg.NodeIP != ""
|
||||||
applyDefaults(cfg, filepath.Dir(path))
|
applyDefaults(cfg, filepath.Dir(path))
|
||||||
if err = validate(cfg); err != nil {
|
if err = validate(cfg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
edgeheartbeat "github.com/Rain-kl/Wavelet/internal/apps/edge/heartbeat"
|
edgeheartbeat "github.com/Rain-kl/Wavelet/internal/apps/edge/heartbeat"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/apps/edge/nodeip"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/relay/config"
|
"github.com/Rain-kl/Wavelet/internal/apps/relay/config"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/relay/frps"
|
"github.com/Rain-kl/Wavelet/internal/apps/relay/frps"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/relay/httpclient"
|
"github.com/Rain-kl/Wavelet/internal/apps/relay/httpclient"
|
||||||
@@ -46,6 +47,12 @@ func (s *Service) doHeartbeat(ctx context.Context) {
|
|||||||
slog.Debug("sending heartbeat")
|
slog.Debug("sending heartbeat")
|
||||||
|
|
||||||
runtimeStatus := s.frpsManager.GetRuntimeStatus()
|
runtimeStatus := s.frpsManager.GetRuntimeStatus()
|
||||||
|
|
||||||
|
ip := s.config.NodeIP
|
||||||
|
if !s.config.NodeIPConfigured {
|
||||||
|
ip = nodeip.DetectWithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
payload := service.RelayHeartbeatPayload{
|
payload := service.RelayHeartbeatPayload{
|
||||||
Version: config.Version,
|
Version: config.Version,
|
||||||
ExtVersion: s.frpsManager.GetVersion(ctx),
|
ExtVersion: s.frpsManager.GetVersion(ctx),
|
||||||
@@ -55,7 +62,7 @@ func (s *Service) doHeartbeat(ctx context.Context) {
|
|||||||
FrpsClientCount: runtimeStatus.ClientCount,
|
FrpsClientCount: runtimeStatus.ClientCount,
|
||||||
FrpsProxies: runtimeStatus.Proxies,
|
FrpsProxies: runtimeStatus.Proxies,
|
||||||
Name: s.config.NodeName,
|
Name: s.config.NodeName,
|
||||||
IP: s.config.NodeIP,
|
IP: ip,
|
||||||
Profile: observability.BuildProfile(s.config, s.stateStore),
|
Profile: observability.BuildProfile(s.config, s.stateStore),
|
||||||
Snapshot: observability.BuildSnapshot(s.config, s.stateStore),
|
Snapshot: observability.BuildSnapshot(s.config, s.stateStore),
|
||||||
HealthEvents: observability.BuildHealthEvents(runtimeStatus),
|
HealthEvents: observability.BuildHealthEvents(runtimeStatus),
|
||||||
|
|||||||
+50
-3
@@ -62,10 +62,57 @@ func (s *HTTPOutboundIPStrategy) GetOutboundIP(ctx context.Context) (net.IP, err
|
|||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
return nil, errors.New("context is required")
|
return nil, errors.New("context is required")
|
||||||
}
|
}
|
||||||
client := s.Client
|
|
||||||
if client == nil {
|
if s.Client != nil {
|
||||||
client = &http.Client{Timeout: defaultOutboundIPLookupTimeout}
|
return s.query(ctx, s.Client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialer := &net.Dialer{
|
||||||
|
Timeout: defaultOutboundIPLookupTimeout,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try IPv4 first
|
||||||
|
ipv4Client := &http.Client{
|
||||||
|
Timeout: defaultOutboundIPLookupTimeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) {
|
||||||
|
return dialer.DialContext(ctx, "tcp4", addr)
|
||||||
|
},
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ip, err := s.query(ctx, ipv4Client)
|
||||||
|
if err == nil && ip != nil {
|
||||||
|
if ipv4 := ip.To4(); ipv4 != nil {
|
||||||
|
return ipv4, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to standard client (dual-stack: tcp)
|
||||||
|
fallbackClient := &http.Client{
|
||||||
|
Timeout: defaultOutboundIPLookupTimeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) {
|
||||||
|
return dialer.DialContext(ctx, "tcp", addr)
|
||||||
|
},
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return s.query(ctx, fallbackClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HTTPOutboundIPStrategy) query(ctx context.Context, client *http.Client) (net.IP, error) {
|
||||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, s.Adapter.Endpoint(), nil)
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, s.Adapter.Endpoint(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s create request failed: %w", s.Name(), err)
|
return nil, fmt.Errorf("%s create request failed: %w", s.Name(), err)
|
||||||
|
|||||||
Reference in New Issue
Block a user