From e43312d4c6764af0f4d4506bc971a376cc0afced Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 1 Jun 2026 21:16:14 +0800 Subject: [PATCH] =?UTF-8?q?[=E4=BC=98=E5=8C=96]=20=E5=A2=9E=E5=8A=A0=20IP?= =?UTF-8?q?=20=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=BF=83=E8=B7=B3=E8=B4=9F=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openflare_server/service/relay.go | 17 +++++++ openflare_server/service/relay_test.go | 28 ++++++++++ openflared/internal/heartbeat/service.go | 65 ++++++++++++++++++++++++ 3 files changed, 110 insertions(+) diff --git a/openflare_server/service/relay.go b/openflare_server/service/relay.go index 867b39f5..760f1c7c 100644 --- a/openflare_server/service/relay.go +++ b/openflare_server/service/relay.go @@ -191,6 +191,7 @@ func normalizeRelayStatus(status string) string { type FlaredHeartbeatPayload struct { ClientVersion string `json:"client_version"` FrpVersion string `json:"frp_version"` + IP string `json:"ip"` TunnelStatus string `json:"tunnel_status"` ConnectedRelays []FlaredConnectedRelay `json:"connected_relays"` CurrentVersion string `json:"current_version"` @@ -200,6 +201,7 @@ type FlaredHeartbeatPayload struct { func normalizeFlaredHeartbeatPayload(payload FlaredHeartbeatPayload) FlaredHeartbeatPayload { payload.ClientVersion = strings.TrimSpace(payload.ClientVersion) payload.FrpVersion = strings.TrimSpace(payload.FrpVersion) + payload.IP = strings.TrimSpace(payload.IP) payload.TunnelStatus = strings.ToLower(strings.TrimSpace(payload.TunnelStatus)) payload.CurrentVersion = strings.TrimSpace(payload.CurrentVersion) payload.CurrentChecksum = strings.TrimSpace(payload.CurrentChecksum) @@ -255,8 +257,23 @@ func HeartbeatFlared(node *model.Node, payload FlaredHeartbeatPayload) (*FlaredH node.CurrentVersion = payload.CurrentVersion node.LastSeenAt = now node.Status = NodeStatusOnline + + if !node.IPManualOverride && payload.IP != "" && previous.IP != payload.IP { + changes["ip"] = payload.IP + node.IP = payload.IP + } + if !node.GeoManualOverride { applyGeoInfoFromIP(node, node.IP) + if previous.GeoName != node.GeoName { + changes["geo_name"] = node.GeoName + } + if !coordinatesEqual(previous.GeoLatitude, node.GeoLatitude) { + changes["geo_latitude"] = node.GeoLatitude + } + if !coordinatesEqual(previous.GeoLongitude, node.GeoLongitude) { + changes["geo_longitude"] = node.GeoLongitude + } } if len(changes) > 0 { diff --git a/openflare_server/service/relay_test.go b/openflare_server/service/relay_test.go index b131ae27..897960b5 100644 --- a/openflare_server/service/relay_test.go +++ b/openflare_server/service/relay_test.go @@ -150,6 +150,7 @@ func TestHeartbeatFlaredPersistsRuntime(t *testing.T) { resp, err := HeartbeatFlared(node, FlaredHeartbeatPayload{ ClientVersion: " v0.2.0 ", FrpVersion: " 0.61.1 ", + IP: " 192.168.1.10 ", TunnelStatus: " RUNNING ", ConnectedRelays: []FlaredConnectedRelay{ {RelayNodeID: " node-relay-1 ", Status: " HEALTHY ", ProxyCount: 3}, @@ -185,12 +186,39 @@ func TestHeartbeatFlaredPersistsRuntime(t *testing.T) { if updated.ExtVersion != "0.61.1" { t.Fatalf("expected frp_version to be trimmed and stored, got %q", updated.ExtVersion) } + if updated.IP != "192.168.1.10" { + t.Fatalf("expected IP to be trimmed and stored, got %q", updated.IP) + } if updated.CurrentVersion != "v1" { t.Fatalf("expected current_version to be stored, got %q", updated.CurrentVersion) } if updated.LastSeenAt.IsZero() { t.Fatal("expected last_seen_at to be updated") } + + // Test IPManualOverride + updated.IPManualOverride = true + if err := updated.Update(); err != nil { + t.Fatalf("failed to lock IP: %v", err) + } + + _, err = HeartbeatFlared(updated, FlaredHeartbeatPayload{ + ClientVersion: "v0.2.0", + FrpVersion: "0.61.1", + IP: "10.0.0.99", + TunnelStatus: "running", + }) + if err != nil { + t.Fatalf("second HeartbeatFlared failed: %v", err) + } + + lockedNode, err := model.GetNodeByNodeID(node.NodeID) + if err != nil { + t.Fatalf("failed to reload locked node: %v", err) + } + if lockedNode.IP != "192.168.1.10" { + t.Fatalf("expected IP to stay locked at 192.168.1.10, but got %q", lockedNode.IP) + } } func TestHeartbeatFlaredTrimsAndFiltersRelays(t *testing.T) { diff --git a/openflared/internal/heartbeat/service.go b/openflared/internal/heartbeat/service.go index a8907f2b..a565461b 100644 --- a/openflared/internal/heartbeat/service.go +++ b/openflared/internal/heartbeat/service.go @@ -3,12 +3,20 @@ package heartbeat import ( "context" "log/slog" + "net" "time" "openflare-flared/internal/config" "openflare-flared/internal/frpc" "openflare-flared/internal/httpclient" "openflare/service" + "openflare/utils/geoip" + "openflare/utils/geoip/iputil" +) + +var ( + lookupOutboundIP = geoip.GetOutboundIP + lookupLocalIP = detectLocalNodeIP ) type Service struct { @@ -45,9 +53,12 @@ func (s *Service) Run(ctx context.Context) { func (s *Service) doHeartbeat(ctx context.Context) { slog.Debug("sending flared heartbeat") + ip := detectNodeIP() + payload := service.FlaredHeartbeatPayload{ ClientVersion: config.Version, FrpVersion: s.frpcManager.GetVersion(), + IP: ip, TunnelStatus: "running", // TODO implement proper status tracking ConnectedRelays: s.frpcManager.GetConnectedRelays(), CurrentVersion: s.frpcManager.GetCurrentConfigVersion(), @@ -61,3 +72,57 @@ func (s *Service) doHeartbeat(ctx context.Context) { } slog.Debug("flared heartbeat succeeded") } + +func detectNodeIP() string { + if ip := detectOutboundNodeIP(); ip != "" { + return ip + } + return lookupLocalIP() +} + +func detectOutboundNodeIP() string { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + ip, err := lookupOutboundIP(ctx) + if err != nil || ip == nil { + return "" + } + return ip.String() +} + +func detectLocalNodeIP() string { + interfaces, err := net.Interfaces() + if err != nil { + return "" + } + bestIP := "" + bestPriority := -1 + for _, iface := range interfaces { + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + continue + } + addrs, err := iface.Addrs() + if err != nil { + continue + } + for _, addr := range addrs { + ipNet, ok := addr.(*net.IPNet) + if !ok || ipNet.IP == nil || ipNet.IP.IsLoopback() { + continue + } + ipv4 := ipNet.IP.To4() + if ipv4 == nil { + continue + } + priority := iputil.Score(ipv4) + if priority > bestPriority { + bestIP = ipv4.String() + bestPriority = priority + } + if bestPriority == 2 { + return bestIP + } + } + } + return bestIP +}