merge fix/agent-link-address: agent inbounds export telemetry address (V1.0.1)

# Conflicts:
#	internal/web/service/inbound.go
This commit is contained in:
Muse
2026-10-23 00:22:36 +08:00
4 changed files with 349 additions and 3 deletions
+62 -1
View File
@@ -416,6 +416,21 @@ func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
// Agent-managed nodes carry the create form's placeholder ("agent")
// in nodes.address, which is not a connectable host. For those rows
// NodeAddress must be the agent's telemetry-reported address (IPv4,
// then IPv6; "" when never reported, so the frontend fallback chain
// continues as it does for a local inbound) — otherwise every
// share/QR link and WireGuard endpoint rendered from these options
// points at the literal placeholder. Mirrors the backend export
// chain's agentNodeAddress in internal/sub.
rowNodeIDs := make(map[int]bool)
for _, r := range rows {
if r.NodeId != nil {
rowNodeIDs[*r.NodeId] = true
}
}
agentAddrByNode := agentAdvertisedAddresses(db, rowNodeIDs)
out := make([]InboundOption, 0, len(rows))
for _, r := range rows {
wgPublicKey, wgMtu, wgDns := inboundWireguardHints(r.Protocol, r.Settings)
@@ -423,6 +438,12 @@ func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error)
if shareAddrStrategy == "node" {
shareAddrStrategy = ""
}
nodeAddress := r.NodeAddress
if r.NodeId != nil {
if addr, isAgent := agentAddrByNode[*r.NodeId]; isAgent {
nodeAddress = addr
}
}
out = append(out, InboundOption{
Id: r.Id,
Remark: r.Remark,
@@ -438,7 +459,7 @@ func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error)
MtprotoDomain: inboundMtprotoDomain(r.Protocol, r.Settings),
AwgServer: inboundAmneziaWGServer(r.Protocol, r.Settings),
NodeId: r.NodeId,
NodeAddress: r.NodeAddress,
NodeAddress: nodeAddress,
Listen: r.Listen,
ShareAddr: r.ShareAddr,
ShareAddrStrategy: shareAddrStrategy,
@@ -447,6 +468,46 @@ func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error)
return out, nil
}
// agentAdvertisedAddresses resolves, for each requested node id that is
// agent-managed, the address share/export surfaces should advertise for
// it: the IPv4 from the bound agent's latest heartbeat telemetry,
// falling back to its IPv6 ("" when the agent never reported one). The
// map contains an entry for every agent-managed id in the set —
// presence in the map is the agent-managed signal, mirroring
// IsAgentManaged's binding-existence check — so callers can tell
// "agent node without telemetry" apart from "traditional node" and
// never fall back to the placeholder stored in nodes.address. Latest
// reporter wins when several tokens are bound to one node, mirroring
// annotateAgentTelemetry.
func agentAdvertisedAddresses(db *gorm.DB, nodeIDs map[int]bool) map[int]string {
out := make(map[int]string)
if len(nodeIDs) == 0 {
return out
}
var agents []model.AgentNode
if err := db.Model(&model.AgentNode{}).
Where("node_id IS NOT NULL").
Order("last_seen DESC").
Find(&agents).Error; err != nil {
return out
}
for i := range agents {
ag := &agents[i]
if ag.NodeID == nil || !nodeIDs[*ag.NodeID] {
continue
}
if _, done := out[*ag.NodeID]; done {
continue
}
addr := strings.TrimSpace(ag.IPv4)
if addr == "" {
addr = strings.TrimSpace(ag.IPv6)
}
out[*ag.NodeID] = addr
}
return out
}
func inboundWireguardHints(protocol string, settings string) (string, int, string) {
if protocol != string(model.WireGuard) || strings.TrimSpace(settings) == "" {
return "", 0, ""