mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
[功能] 优化节点 IP 解析逻辑,优先使用公网地址并添加相关测试
This commit is contained in:
+2
-1
@@ -131,7 +131,7 @@ Agent 当前支持:
|
||||
| `agent_token` | 节点专属认证 Token | 与 `discovery_token` 二选一 | 空 |
|
||||
| `discovery_token` | 首次自动注册使用的全局 Token | 与 `agent_token` 二选一 | 空 |
|
||||
| `node_name` | 节点名称 | 否 | 自动使用主机名 |
|
||||
| `node_ip` | 节点 IP | 否 | 自动探测 |
|
||||
| `node_ip` | 节点 IP | 否 | 自动探测,优先选择公网 IPv4;仅无公网地址时退回可用内网地址 |
|
||||
| `openresty_path` | 本机 OpenResty 路径 | 否 | 空,未设置时走 Docker 模式 |
|
||||
| `openresty_container_name` | Docker 模式下的容器名 | 否 | `openflare-openresty` |
|
||||
| `openresty_docker_image` | Docker 模式下的镜像 | 否 | `openresty/openresty:alpine` |
|
||||
@@ -155,6 +155,7 @@ Agent 当前支持:
|
||||
* `agent_token` 与 `discovery_token` 不能同时为空
|
||||
* `heartbeat_interval` 与 `request_timeout` 支持毫秒整数或 Go duration 字符串
|
||||
* 未配置 `openresty_path` 时默认使用 Docker OpenResty 模式
|
||||
* Agent 自动探测到私网 `node_ip` 时,Server 会在注册/心跳阶段优先保留 Agent 直连来源的公网地址,避免 NAT/多网卡场景误登记内网网卡地址
|
||||
|
||||
## 3. 维护要求
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
|
||||
### 1.2 Agent
|
||||
|
||||
* Go 1.23+
|
||||
* Go 1.24+
|
||||
* 对 Agent 数据目录有写权限
|
||||
* 本机模式下可执行 `openresty -t` 与 `openresty -s reload`
|
||||
* Docker 模式下具备 Docker 执行权限
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
`openflare_agent` 继续作为 Go 单体程序:
|
||||
|
||||
* Go 1.23+
|
||||
* Go 1.24+
|
||||
* 单二进制
|
||||
* 节点本地执行
|
||||
* `openresty_path` 优先
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
module openflare-agent
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.0
|
||||
|
||||
require openflare v0.0.0
|
||||
|
||||
replace openflare => ../openflare_server
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"openflare/utils/geoip/iputil"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"path/filepath"
|
||||
@@ -331,6 +332,8 @@ func detectNodeIP() string {
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
bestIP := ""
|
||||
bestPriority := -1
|
||||
for _, iface := range interfaces {
|
||||
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
@@ -344,11 +347,27 @@ func detectNodeIP() string {
|
||||
if !ok || ipNet.IP == nil || ipNet.IP.IsLoopback() {
|
||||
continue
|
||||
}
|
||||
ipv4 := ipNet.IP.To4()
|
||||
if ipv4 != nil {
|
||||
return ipv4.String()
|
||||
ipv4 := normalizeIPv4(ipNet.IP)
|
||||
priority := nodeIPPriority(ipv4)
|
||||
if priority > bestPriority {
|
||||
bestIP = ipv4.String()
|
||||
bestPriority = priority
|
||||
}
|
||||
if bestPriority == 2 {
|
||||
return bestIP
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return bestIP
|
||||
}
|
||||
|
||||
func normalizeIPv4(ip net.IP) net.IP {
|
||||
if ip == nil {
|
||||
return nil
|
||||
}
|
||||
return ip.To4()
|
||||
}
|
||||
|
||||
func nodeIPPriority(ip net.IP) int {
|
||||
return iputil.Score(ip)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -326,3 +327,49 @@ func TestInitialAuthToken(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeIPPriority(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ip string
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
name: "public ipv4 preferred",
|
||||
ip: "8.8.8.8",
|
||||
expected: 2,
|
||||
},
|
||||
{
|
||||
name: "private ipv4 fallback",
|
||||
ip: "10.0.0.8",
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "link local ignored",
|
||||
ip: "169.254.1.10",
|
||||
expected: -1,
|
||||
},
|
||||
{
|
||||
name: "loopback ignored",
|
||||
ip: "127.0.0.1",
|
||||
expected: -1,
|
||||
},
|
||||
{
|
||||
name: "nil ignored",
|
||||
ip: "",
|
||||
expected: -1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var parsed net.IP
|
||||
if tt.ip != "" {
|
||||
parsed = net.ParseIP(tt.ip)
|
||||
}
|
||||
if got := nodeIPPriority(parsed); got != tt.expected {
|
||||
t.Fatalf("unexpected priority for %q: got %d want %d", tt.ip, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ func AgentRegister(c *gin.Context) {
|
||||
respondBadRequest(c, "")
|
||||
return
|
||||
}
|
||||
payload.IP = service.ResolveReportedNodeIP(payload.IP, c.Request.RemoteAddr)
|
||||
|
||||
var (
|
||||
result *service.AgentRegistrationResponse
|
||||
@@ -57,6 +58,7 @@ func AgentHeartbeat(c *gin.Context) {
|
||||
respondBadRequest(c, "")
|
||||
return
|
||||
}
|
||||
payload.IP = service.ResolveReportedNodeIP(payload.IP, c.Request.RemoteAddr)
|
||||
|
||||
authNode, ok := c.Get("agent_node")
|
||||
if !ok {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module openflare
|
||||
|
||||
// +heroku goVersion go1.18
|
||||
// +heroku goVersion go1.24
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
|
||||
@@ -212,10 +212,10 @@ func TestPhase2AgentLifecycle(t *testing.T) {
|
||||
"current_version": "",
|
||||
"last_error": "",
|
||||
}
|
||||
resp := performAgentJSONRequestWithToken(t, engine, createdNode.AgentToken, http.MethodPost, "/api/agent/nodes/heartbeat", heartbeatPayload)
|
||||
resp := performAgentJSONRequestWithTokenAndRemote(t, engine, createdNode.AgentToken, http.MethodPost, "/api/agent/nodes/heartbeat", heartbeatPayload, "198.51.100.10:1234")
|
||||
var registeredNode model.Node
|
||||
decodeResponseData(t, resp, ®isteredNode)
|
||||
if registeredNode.IP != "10.0.0.9" || registeredNode.AgentVersion != "0.1.1" || registeredNode.NodeID != createdNode.NodeID {
|
||||
if registeredNode.IP != "198.51.100.10" || registeredNode.AgentVersion != "0.1.1" || registeredNode.NodeID != createdNode.NodeID {
|
||||
t.Fatal("expected heartbeat to update node metadata")
|
||||
}
|
||||
if registeredNode.OpenrestyStatus != service.OpenrestyStatusUnhealthy {
|
||||
@@ -330,6 +330,7 @@ func TestPhase2AgentLifecycle(t *testing.T) {
|
||||
restartHeartbeatReq := httptest.NewRequest(http.MethodPost, "/api/agent/nodes/heartbeat", bytes.NewReader(rawHeartbeatPayload))
|
||||
restartHeartbeatReq.Header.Set("Content-Type", "application/json")
|
||||
restartHeartbeatReq.Header.Set("X-Agent-Token", createdNode.AgentToken)
|
||||
restartHeartbeatReq.RemoteAddr = "198.51.100.10:1234"
|
||||
restartHeartbeatRecorder := httptest.NewRecorder()
|
||||
engine.ServeHTTP(restartHeartbeatRecorder, restartHeartbeatReq)
|
||||
if restartHeartbeatRecorder.Code != http.StatusOK {
|
||||
@@ -533,7 +534,7 @@ func TestPhase2GlobalDiscoveryRegistration(t *testing.T) {
|
||||
t.Fatal("expected global discovery token to be available")
|
||||
}
|
||||
|
||||
resp := performAgentJSONRequestWithToken(t, engine, bootstrap.DiscoveryToken, http.MethodPost, "/api/agent/nodes/register", map[string]any{
|
||||
resp := performAgentJSONRequestWithTokenAndRemote(t, engine, bootstrap.DiscoveryToken, http.MethodPost, "/api/agent/nodes/register", map[string]any{
|
||||
"node_id": "local-node-id",
|
||||
"name": "bulk-edge-1",
|
||||
"ip": "10.0.0.18",
|
||||
@@ -541,7 +542,7 @@ func TestPhase2GlobalDiscoveryRegistration(t *testing.T) {
|
||||
"nginx_version": "1.25.5",
|
||||
"current_version": "",
|
||||
"last_error": "",
|
||||
})
|
||||
}, "203.0.113.18:4321")
|
||||
var registration service.AgentRegistrationResponse
|
||||
decodeResponseData(t, resp, ®istration)
|
||||
if registration.AgentToken == "" || registration.NodeID == "" {
|
||||
@@ -557,9 +558,16 @@ func TestPhase2GlobalDiscoveryRegistration(t *testing.T) {
|
||||
if nodes[0].Name != "bulk-edge-1" || nodes[0].AgentToken != registration.AgentToken || nodes[0].Status != service.NodeStatusOnline {
|
||||
t.Fatal("expected discovered node to be created online with issued agent token")
|
||||
}
|
||||
if nodes[0].IP != "203.0.113.18" {
|
||||
t.Fatalf("expected discovered node to keep public source ip, got %s", nodes[0].IP)
|
||||
}
|
||||
}
|
||||
|
||||
func performAgentJSONRequestWithToken(t *testing.T, engine http.Handler, token string, method string, path string, body any) apiResponse {
|
||||
return performAgentJSONRequestWithTokenAndRemote(t, engine, token, method, path, body, "")
|
||||
}
|
||||
|
||||
func performAgentJSONRequestWithTokenAndRemote(t *testing.T, engine http.Handler, token string, method string, path string, body any, remoteAddr string) apiResponse {
|
||||
t.Helper()
|
||||
var payload []byte
|
||||
var err error
|
||||
@@ -573,6 +581,9 @@ func performAgentJSONRequestWithToken(t *testing.T, engine http.Handler, token s
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
if remoteAddr != "" {
|
||||
req.RemoteAddr = remoteAddr
|
||||
}
|
||||
req.Header.Set("X-Agent-Token", token)
|
||||
recorder := httptest.NewRecorder()
|
||||
engine.ServeHTTP(recorder, req)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"openflare/common"
|
||||
"openflare/model"
|
||||
"openflare/utils/geoip"
|
||||
"openflare/utils/geoip/iputil"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -332,6 +333,29 @@ func cloneCoordinate(value *float64) *float64 {
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func ResolveReportedNodeIP(reportedIP string, remoteAddr string) string {
|
||||
reported := iputil.NormalizeIP(reportedIP)
|
||||
remote := iputil.NormalizeRemoteAddr(remoteAddr)
|
||||
if reported == "" {
|
||||
return remote
|
||||
}
|
||||
if !shouldPreferRemoteNodeIP(reported) {
|
||||
return reported
|
||||
}
|
||||
if isPublicNodeIP(remote) {
|
||||
return remote
|
||||
}
|
||||
return reported
|
||||
}
|
||||
|
||||
func shouldPreferRemoteNodeIP(ip string) bool {
|
||||
return !isPublicNodeIP(ip)
|
||||
}
|
||||
|
||||
func isPublicNodeIP(raw string) bool {
|
||||
return iputil.IsPublicString(raw)
|
||||
}
|
||||
|
||||
func buildNodeAgentReleaseView(node *model.Node, release *githubReleaseResponse, channel ReleaseChannel) *NodeAgentReleaseInfo {
|
||||
currentVersion := strings.TrimSpace(node.AgentVersion)
|
||||
view := &NodeAgentReleaseInfo{
|
||||
@@ -428,6 +452,9 @@ func validateAgentNodePayload(payload AgentNodePayload) error {
|
||||
if payload.IP == "" {
|
||||
return errors.New("ip 不能为空")
|
||||
}
|
||||
if net.ParseIP(payload.IP) == nil {
|
||||
return errors.New("ip 格式无效")
|
||||
}
|
||||
if payload.AgentVersion == "" {
|
||||
return errors.New("agent_version 不能为空")
|
||||
}
|
||||
|
||||
@@ -328,6 +328,27 @@ func TestUpdateNodeCanChangeIPAndAutoResolveGeo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveReportedNodeIPPrefersPublicRemoteAddr(t *testing.T) {
|
||||
resolved := ResolveReportedNodeIP("10.0.0.8", "198.51.100.20:9000")
|
||||
if resolved != "198.51.100.20" {
|
||||
t.Fatalf("expected public remote ip to override private reported ip, got %q", resolved)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveReportedNodeIPKeepsPublicReportedAddr(t *testing.T) {
|
||||
resolved := ResolveReportedNodeIP("8.8.8.8", "198.51.100.20:9000")
|
||||
if resolved != "8.8.8.8" {
|
||||
t.Fatalf("expected reported public ip to be preserved, got %q", resolved)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveReportedNodeIPKeepsPrivateReportedAddrWhenRemoteIsPrivate(t *testing.T) {
|
||||
resolved := ResolveReportedNodeIP("10.0.0.8", "172.16.1.10:9000")
|
||||
if resolved != "10.0.0.8" {
|
||||
t.Fatalf("expected private reported ip to be preserved when remote is also private, got %q", resolved)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeartbeatNodeResolvesGeoMetadataFromIPWhenNotManuallyOverridden(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
withFakeGeoIPProvider(t, &geoip.GeoInfo{
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package iputil
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func NormalizeIP(raw string) string {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
ip := net.ParseIP(trimmed)
|
||||
if ip == nil {
|
||||
return ""
|
||||
}
|
||||
if ipv4 := ip.To4(); ipv4 != nil {
|
||||
return ipv4.String()
|
||||
}
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
func NormalizeRemoteAddr(remoteAddr string) string {
|
||||
trimmed := strings.TrimSpace(remoteAddr)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
if host, _, err := net.SplitHostPort(trimmed); err == nil {
|
||||
return NormalizeIP(host)
|
||||
}
|
||||
return NormalizeIP(trimmed)
|
||||
}
|
||||
|
||||
func IsPublic(ip net.IP) bool {
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
if ipv4 := ip.To4(); ipv4 != nil {
|
||||
ip = ipv4
|
||||
}
|
||||
if !ip.IsGlobalUnicast() || ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsMulticast() || ip.IsUnspecified() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func IsPublicString(raw string) bool {
|
||||
ip := net.ParseIP(strings.TrimSpace(raw))
|
||||
return IsPublic(ip)
|
||||
}
|
||||
|
||||
func Score(ip net.IP) int {
|
||||
if ip == nil {
|
||||
return -1
|
||||
}
|
||||
if ipv4 := ip.To4(); ipv4 != nil {
|
||||
ip = ipv4
|
||||
}
|
||||
if !ip.IsGlobalUnicast() || ip.IsLoopback() || ip.IsMulticast() || ip.IsUnspecified() {
|
||||
return -1
|
||||
}
|
||||
if IsPublic(ip) {
|
||||
return 2
|
||||
}
|
||||
return 1
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package iputil
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeIP(t *testing.T) {
|
||||
if got := NormalizeIP(" 8.8.8.8 "); got != "8.8.8.8" {
|
||||
t.Fatalf("unexpected normalized ipv4: %q", got)
|
||||
}
|
||||
if got := NormalizeIP("[::1]"); got != "" {
|
||||
t.Fatalf("expected invalid bracketed host to be rejected, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRemoteAddr(t *testing.T) {
|
||||
if got := NormalizeRemoteAddr("203.0.113.10:8443"); got != "203.0.113.10" {
|
||||
t.Fatalf("unexpected remote addr normalization: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPublic(t *testing.T) {
|
||||
if !IsPublic(net.ParseIP("8.8.8.8")) {
|
||||
t.Fatal("expected public ip to be detected")
|
||||
}
|
||||
if IsPublic(net.ParseIP("10.0.0.8")) {
|
||||
t.Fatal("expected private ip to be rejected")
|
||||
}
|
||||
if IsPublic(net.ParseIP("127.0.0.1")) {
|
||||
t.Fatal("expected loopback ip to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScore(t *testing.T) {
|
||||
if got := Score(net.ParseIP("8.8.8.8")); got != 2 {
|
||||
t.Fatalf("unexpected score for public ip: %d", got)
|
||||
}
|
||||
if got := Score(net.ParseIP("10.0.0.8")); got != 1 {
|
||||
t.Fatalf("unexpected score for private ip: %d", got)
|
||||
}
|
||||
if got := Score(net.ParseIP("127.0.0.1")); got != -1 {
|
||||
t.Fatalf("unexpected score for loopback ip: %d", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user