NAT'd agents (cloud 1:1 EIP) only ever report their private interface address in heartbeat telemetry, so the node list showed an unusable RFC1918 address. The master now records the source address of every authenticated agent request (CF-Connecting-IP, else first X-Forwarded-For hop, else RemoteAddr; change-only writes) on the agent row as seen_ipv4/seen_ipv6, and the node list resolves the displayed pair via service.ResolveAgentAddresses: public telemetry wins, else a public observed source, else the reported address as-is. CGNAT 100.64/10 observed addresses are never promoted. Telemetry values are kept verbatim; resolution is read-time only, and agents need no changes.
93 lines
3.2 KiB
Go
93 lines
3.2 KiB
Go
package service
|
|
|
|
import (
|
|
"net"
|
|
"strings"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
)
|
|
|
|
// Public-address resolution for agent nodes.
|
|
//
|
|
// An agent reports the addresses of its own network interfaces in its
|
|
// heartbeat telemetry (P13). On a host behind 1:1 NAT (typical cloud
|
|
// EIP setups) the public address is mapped in the provider's network
|
|
// and never assigned to a local interface, so the agent can only ever
|
|
// report its private address — the node list then shows an unusable
|
|
// RFC1918 address for such nodes. The master, however, observes the
|
|
// source address of every authenticated agent request (recorded on the
|
|
// agent row as SeenIPv4/SeenIPv6 by AgentController.captureSeenIP).
|
|
// ResolveAgentAddresses combines both views into the address pair a
|
|
// client can actually connect to.
|
|
|
|
// IsPublicIPLiteral reports whether s is an IP literal that is globally
|
|
// routable: not loopback, link-local, multicast, unspecified, RFC1918,
|
|
// ULA, or the CGNAT shared range 100.64.0.0/10 (RFC 6598 — not private,
|
|
// but not publicly routable either). Go's IsGlobalUnicast alone is not
|
|
// sufficient for this: it counts RFC1918 addresses as global unicast.
|
|
//
|
|
// Exported for reuse by the subscription/export chain.
|
|
func IsPublicIPLiteral(s string) bool {
|
|
ip := net.ParseIP(strings.TrimSpace(s))
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() ||
|
|
ip.IsMulticast() || ip.IsUnspecified() || ip.IsPrivate() {
|
|
return false
|
|
}
|
|
if v4 := ip.To4(); v4 != nil && v4[0] == 100 && v4[1]&0xC0 == 0x40 {
|
|
return false // 100.64.0.0/10, CGNAT shared address space
|
|
}
|
|
return true
|
|
}
|
|
|
|
// familyLiteral returns s normalized as an IP literal of the wanted
|
|
// family, or "" when s is not a valid literal of that family.
|
|
func familyLiteral(s string, want4 bool) string {
|
|
ip := net.ParseIP(strings.TrimSpace(s))
|
|
if ip == nil {
|
|
return ""
|
|
}
|
|
if want4 {
|
|
if v4 := ip.To4(); v4 != nil {
|
|
return v4.String()
|
|
}
|
|
return ""
|
|
}
|
|
if ip.To4() != nil {
|
|
return ""
|
|
}
|
|
return ip.String()
|
|
}
|
|
|
|
// resolveAddressFamily picks the externally usable address of one
|
|
// family: the agent-reported address when it is public; otherwise the
|
|
// master-observed source address when IT is public; otherwise the
|
|
// reported address as-is (a private telemetry address still beats
|
|
// nothing, e.g. for LAN-only deployments). A non-public observed
|
|
// address is never promoted over the reported one.
|
|
func resolveAddressFamily(reported, seen string, want4 bool) string {
|
|
rep := familyLiteral(reported, want4)
|
|
if IsPublicIPLiteral(rep) {
|
|
return rep
|
|
}
|
|
if sn := familyLiteral(seen, want4); IsPublicIPLiteral(sn) {
|
|
return sn
|
|
}
|
|
return rep
|
|
}
|
|
|
|
// ResolveAgentAddresses returns the externally usable IPv4/IPv6 pair
|
|
// for an agent binding, resolved from its telemetry addresses and the
|
|
// source addresses the master observed on its requests. This is the
|
|
// single resolution point shared by the node list (agent row address)
|
|
// and the subscription/export chain (advertised connect address).
|
|
func ResolveAgentAddresses(ag *model.AgentNode) (ipv4, ipv6 string) {
|
|
if ag == nil {
|
|
return "", ""
|
|
}
|
|
return resolveAddressFamily(ag.IPv4, ag.SeenIPv4, true),
|
|
resolveAddressFamily(ag.IPv6, ag.SeenIPv6, false)
|
|
}
|