merge fix/agent-public-addr: master-observed public IP resolution (V1.0.1)
This commit is contained in:
@@ -6,7 +6,9 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/config"
|
||||
@@ -100,6 +102,69 @@ func (a *AgentController) findAgent(token string) (*model.AgentNode, bool) {
|
||||
return &agent, true
|
||||
}
|
||||
|
||||
// requestSourceIP extracts the real source IP of an agent request.
|
||||
// Agents reach the master through Cloudflare, so the TCP peer is a CF
|
||||
// edge and the agent's own address travels in CF-Connecting-IP; the
|
||||
// first X-Forwarded-For hop is the next best signal, and the transport
|
||||
// RemoteAddr is the direct-connection fallback. Only literal IPs are
|
||||
// accepted — anything unparsable falls through to the next source.
|
||||
func requestSourceIP(c *gin.Context) net.IP {
|
||||
var candidates []string
|
||||
if v := c.GetHeader("CF-Connecting-IP"); v != "" {
|
||||
candidates = append(candidates, v)
|
||||
}
|
||||
if v := c.GetHeader("X-Forwarded-For"); v != "" {
|
||||
candidates = append(candidates, strings.Split(v, ",")[0])
|
||||
}
|
||||
if host, _, err := net.SplitHostPort(c.Request.RemoteAddr); err == nil {
|
||||
candidates = append(candidates, host)
|
||||
} else {
|
||||
candidates = append(candidates, c.Request.RemoteAddr)
|
||||
}
|
||||
for _, cand := range candidates {
|
||||
if ip := net.ParseIP(strings.TrimSpace(cand)); ip != nil {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// captureSeenIP records the source address of an authenticated agent
|
||||
// request on the agent row (SeenIPv4/SeenIPv6). This is the master's
|
||||
// view of the agent's public side: for a NAT'd agent it is the only
|
||||
// place the public address ever appears. Writes are change-only —
|
||||
// traffic reports land every ~10s and the address almost never moves,
|
||||
// so an unchanged address must not turn into a DB write per request.
|
||||
func (a *AgentController) captureSeenIP(c *gin.Context, agent *model.AgentNode) {
|
||||
ip := requestSourceIP(c)
|
||||
if ip == nil || agent == nil {
|
||||
return
|
||||
}
|
||||
updates := map[string]any{"seen_ip_at": time.Now().UnixMilli()}
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
if agent.SeenIPv4 == v4.String() {
|
||||
return
|
||||
}
|
||||
updates["seen_ipv4"] = v4.String()
|
||||
} else {
|
||||
if agent.SeenIPv6 == ip.String() {
|
||||
return
|
||||
}
|
||||
updates["seen_ipv6"] = ip.String()
|
||||
}
|
||||
if err := database.GetDB().Model(&model.AgentNode{}).
|
||||
Where("id = ?", agent.Id).Updates(updates).Error; err != nil {
|
||||
return
|
||||
}
|
||||
if v, ok := updates["seen_ipv4"]; ok {
|
||||
agent.SeenIPv4 = v.(string)
|
||||
}
|
||||
if v, ok := updates["seen_ipv6"]; ok {
|
||||
agent.SeenIPv6 = v.(string)
|
||||
}
|
||||
agent.SeenIPAt = updates["seen_ip_at"].(int64)
|
||||
}
|
||||
|
||||
func (a *AgentController) touchAgent(id uint, name string) {
|
||||
updates := map[string]any{"last_seen": time.Now().UnixMilli()}
|
||||
if name != "" {
|
||||
@@ -133,6 +198,7 @@ func (a *AgentController) register(c *gin.Context) {
|
||||
c.JSON(401, gin.H{"message": "unknown token"})
|
||||
return
|
||||
}
|
||||
a.captureSeenIP(c, agent)
|
||||
a.touchAgent(uint(agent.Id), req.Name)
|
||||
logger.Infof("Agent registered: %s (token %s...)", req.Name, agent.TokenPrefix)
|
||||
jsonObj(c, gin.H{"message": "registered"}, nil)
|
||||
@@ -257,6 +323,7 @@ func (a *AgentController) heartbeat(c *gin.Context) {
|
||||
c.JSON(401, gin.H{"message": "unauthorized"})
|
||||
return
|
||||
}
|
||||
a.captureSeenIP(c, agent)
|
||||
// Telemetry is optional and best-effort: a body that fails to parse
|
||||
// (or no body at all) must never fail the heartbeat itself.
|
||||
var req struct {
|
||||
@@ -286,6 +353,7 @@ func (a *AgentController) getConfig(c *gin.Context) {
|
||||
c.JSON(401, gin.H{"message": "unauthorized"})
|
||||
return
|
||||
}
|
||||
a.captureSeenIP(c, agent)
|
||||
if agent.NodeID == nil {
|
||||
c.JSON(409, gin.H{"message": "agent token is not bound to a node"})
|
||||
return
|
||||
@@ -344,6 +412,7 @@ func (a *AgentController) reportTraffic(c *gin.Context) {
|
||||
c.JSON(401, gin.H{"message": "unauthorized"})
|
||||
return
|
||||
}
|
||||
a.captureSeenIP(c, agent)
|
||||
var req agentTrafficReport
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
jsonMsg(c, "Invalid request", err)
|
||||
|
||||
Reference in New Issue
Block a user