self-detection elides self url at startup, handles multiple DNS pointers
Some checks failed
Go / build-and-release (push) Has been cancelled

This commit is contained in:
2025-11-25 13:26:37 +00:00
parent 1522bfab2e
commit a03af8e05a
4 changed files with 221 additions and 92 deletions

View File

@@ -25,6 +25,7 @@ type ClusterManager struct {
db *database.D
adminNpubs []string
relayIdentityPubkey string // Our relay's identity pubkey (hex)
selfURLs map[string]bool // URLs discovered to be ourselves (for fast lookups)
members map[string]*ClusterMember // keyed by relay URL
membersMux sync.RWMutex
pollTicker *time.Ticker
@@ -78,6 +79,7 @@ func NewClusterManager(ctx context.Context, db *database.D, adminNpubs []string,
db: db,
adminNpubs: adminNpubs,
relayIdentityPubkey: relayPubkey,
selfURLs: make(map[string]bool),
members: make(map[string]*ClusterMember),
pollDone: make(chan struct{}),
propagatePrivilegedEvents: propagatePrivilegedEvents,
@@ -265,48 +267,47 @@ func (cm *ClusterManager) UpdateMembership(relayURLs []string) {
}
}
// Add new members
// Add new members (filter out self once at this point)
for _, url := range relayURLs {
// Skip if this is our own relay (check via NIP-11 pubkey)
if cm.isSelfRelay(url) {
log.D.F("skipping cluster member (self): %s (pubkey matches our relay identity)", url)
// Skip if already exists
if _, exists := cm.members[url]; exists {
continue
}
if _, exists := cm.members[url]; !exists {
// For simplicity, assume HTTP and WebSocket URLs are the same
// In practice, you'd need to parse these properly
member := &ClusterMember{
HTTPURL: url,
WebSocketURL: url, // TODO: Convert to WebSocket URL
LastSerial: 0,
Status: "unknown",
}
cm.members[url] = member
log.I.F("added cluster member: %s", url)
// Fast path: check if we already know this URL is ours
if cm.selfURLs[url] {
log.I.F("removed self from cluster members (known URL): %s", url)
continue
}
// Slow path: check via NIP-11 pubkey
if cm.relayIdentityPubkey != "" {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
peerPubkey, err := cm.nip11Cache.GetPubkey(ctx, url)
cancel()
if err != nil {
log.D.F("couldn't fetch NIP-11 for %s, adding to cluster anyway: %v", url, err)
} else if peerPubkey == cm.relayIdentityPubkey {
log.I.F("removed self from cluster members (discovered): %s (pubkey: %s)", url, cm.relayIdentityPubkey)
// Cache this URL as ours for future fast lookups
cm.selfURLs[url] = true
continue
}
}
// Add member
member := &ClusterMember{
HTTPURL: url,
WebSocketURL: url, // TODO: Convert to WebSocket URL
LastSerial: 0,
Status: "unknown",
}
cm.members[url] = member
log.I.F("added cluster member: %s", url)
}
}
// isSelfRelay checks if a relay URL is actually ourselves by comparing NIP-11 pubkeys
func (cm *ClusterManager) isSelfRelay(relayURL string) bool {
// If we don't have a relay identity pubkey, can't compare
if cm.relayIdentityPubkey == "" {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
peerPubkey, err := cm.nip11Cache.GetPubkey(ctx, relayURL)
if err != nil {
log.D.F("couldn't fetch NIP-11 for %s to check if self: %v", relayURL, err)
return false
}
return peerPubkey == cm.relayIdentityPubkey
}
// HandleMembershipEvent processes a cluster membership event (Kind 39108)
func (cm *ClusterManager) HandleMembershipEvent(event *event.E) error {
// Verify the event is signed by a cluster admin
@@ -352,18 +353,37 @@ func (cm *ClusterManager) HandleLatestSerial(w http.ResponseWriter, r *http.Requ
}
// Check if request is from ourselves by examining the Referer or Origin header
// Note: Self-members are already filtered out, but this catches edge cases
origin := r.Header.Get("Origin")
referer := r.Header.Get("Referer")
if origin != "" && cm.isSelfRelay(origin) {
log.D.F("rejecting cluster latest request from self (origin: %s)", origin)
http.Error(w, "Cannot sync with self", http.StatusBadRequest)
return
}
if referer != "" && cm.isSelfRelay(referer) {
log.D.F("rejecting cluster latest request from self (referer: %s)", referer)
http.Error(w, "Cannot sync with self", http.StatusBadRequest)
return
if cm.relayIdentityPubkey != "" && (origin != "" || referer != "") {
checkURL := origin
if checkURL == "" {
checkURL = referer
}
// Fast path: check known self-URLs
if cm.selfURLs[checkURL] {
log.D.F("rejecting cluster latest request from self (known URL): %s", checkURL)
http.Error(w, "Cannot sync with self", http.StatusBadRequest)
return
}
// Slow path: verify via NIP-11
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
peerPubkey, err := cm.nip11Cache.GetPubkey(ctx, checkURL)
cancel()
if err == nil && peerPubkey == cm.relayIdentityPubkey {
log.D.F("rejecting cluster latest request from self (discovered): %s", checkURL)
// Cache for future fast lookups
cm.membersMux.Lock()
cm.selfURLs[checkURL] = true
cm.membersMux.Unlock()
http.Error(w, "Cannot sync with self", http.StatusBadRequest)
return
}
}
// Get the latest serial from database by querying for the highest serial
@@ -390,18 +410,37 @@ func (cm *ClusterManager) HandleEventsRange(w http.ResponseWriter, r *http.Reque
}
// Check if request is from ourselves by examining the Referer or Origin header
// Note: Self-members are already filtered out, but this catches edge cases
origin := r.Header.Get("Origin")
referer := r.Header.Get("Referer")
if origin != "" && cm.isSelfRelay(origin) {
log.D.F("rejecting cluster events request from self (origin: %s)", origin)
http.Error(w, "Cannot sync with self", http.StatusBadRequest)
return
}
if referer != "" && cm.isSelfRelay(referer) {
log.D.F("rejecting cluster events request from self (referer: %s)", referer)
http.Error(w, "Cannot sync with self", http.StatusBadRequest)
return
if cm.relayIdentityPubkey != "" && (origin != "" || referer != "") {
checkURL := origin
if checkURL == "" {
checkURL = referer
}
// Fast path: check known self-URLs
if cm.selfURLs[checkURL] {
log.D.F("rejecting cluster events request from self (known URL): %s", checkURL)
http.Error(w, "Cannot sync with self", http.StatusBadRequest)
return
}
// Slow path: verify via NIP-11
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
peerPubkey, err := cm.nip11Cache.GetPubkey(ctx, checkURL)
cancel()
if err == nil && peerPubkey == cm.relayIdentityPubkey {
log.D.F("rejecting cluster events request from self (discovered): %s", checkURL)
// Cache for future fast lookups
cm.membersMux.Lock()
cm.selfURLs[checkURL] = true
cm.membersMux.Unlock()
http.Error(w, "Cannot sync with self", http.StatusBadRequest)
return
}
}
// Parse query parameters

View File

@@ -26,6 +26,7 @@ type Manager struct {
nodeID string
relayURL string
peers []string
selfURLs map[string]bool // URLs discovered to be ourselves (for fast lookups)
currentSerial uint64
peerSerials map[string]uint64 // peer URL -> latest serial seen
relayGroupMgr *RelayGroupManager
@@ -72,6 +73,7 @@ func NewManager(ctx context.Context, db *database.D, nodeID, relayURL string, pe
nodeID: nodeID,
relayURL: relayURL,
peers: peers,
selfURLs: make(map[string]bool),
currentSerial: 0,
peerSerials: make(map[string]uint64),
relayGroupMgr: relayGroupMgr,
@@ -79,6 +81,44 @@ func NewManager(ctx context.Context, db *database.D, nodeID, relayURL string, pe
policyManager: policyManager,
}
// Add our configured relay URL to self-URLs cache if provided
if m.relayURL != "" {
m.selfURLs[m.relayURL] = true
}
// Remove self from peer list once at startup if we have a nodeID
if m.nodeID != "" {
filteredPeers := make([]string, 0, len(m.peers))
for _, peerURL := range m.peers {
// Fast path: check if we already know this URL is ours
if m.selfURLs[peerURL] {
log.I.F("removed self from sync peer list (known URL): %s", peerURL)
continue
}
// Slow path: check via NIP-11 pubkey
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
peerPubkey, err := m.nip11Cache.GetPubkey(ctx, peerURL)
cancel()
if err != nil {
log.D.F("couldn't fetch NIP-11 for %s, keeping in peer list: %v", peerURL, err)
filteredPeers = append(filteredPeers, peerURL)
continue
}
if peerPubkey == m.nodeID {
log.I.F("removed self from sync peer list (discovered): %s (pubkey: %s)", peerURL, m.nodeID)
// Cache this URL as ours for future fast lookups
m.selfURLs[peerURL] = true
continue
}
filteredPeers = append(filteredPeers, peerURL)
}
m.peers = filteredPeers
}
// Start sync routine
go m.syncRoutine()
@@ -173,36 +213,13 @@ func (m *Manager) syncRoutine() {
// syncWithPeersSequentially syncs with all configured peers one at a time
func (m *Manager) syncWithPeersSequentially() {
for _, peerURL := range m.peers {
// Check if this peer is ourselves via NIP-11 pubkey
if m.isSelfPeer(peerURL) {
log.D.F("skipping sync with self: %s (pubkey matches our relay identity)", peerURL)
continue
}
// Self-peers are already filtered out during initialization/update
m.syncWithPeer(peerURL)
// Small delay between peers to avoid overwhelming
time.Sleep(100 * time.Millisecond)
}
}
// isSelfPeer checks if a peer URL is actually ourselves by comparing NIP-11 pubkeys
func (m *Manager) isSelfPeer(peerURL string) bool {
// If we don't have a nodeID, can't compare
if m.nodeID == "" {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
peerPubkey, err := m.nip11Cache.GetPubkey(ctx, peerURL)
if err != nil {
log.D.F("couldn't fetch NIP-11 for %s to check if self: %v", peerURL, err)
return false
}
return peerPubkey == m.nodeID
}
// syncWithPeer syncs with a specific peer
func (m *Manager) syncWithPeer(peerURL string) {
// Get the peer's current serial
@@ -417,6 +434,13 @@ func (m *Manager) HandleCurrentRequest(w http.ResponseWriter, r *http.Request) {
// Reject requests from ourselves (same nodeID)
if req.NodeID != "" && req.NodeID == m.nodeID {
log.D.F("rejecting sync current request from self (nodeID: %s)", req.NodeID)
// Cache the requesting relay URL as ours for future fast lookups
if req.RelayURL != "" {
m.mutex.Lock()
m.selfURLs[req.RelayURL] = true
m.mutex.Unlock()
log.D.F("cached self-URL from inbound request: %s", req.RelayURL)
}
http.Error(w, "Cannot sync with self", http.StatusBadRequest)
return
}
@@ -447,6 +471,13 @@ func (m *Manager) HandleEventIDsRequest(w http.ResponseWriter, r *http.Request)
// Reject requests from ourselves (same nodeID)
if req.NodeID != "" && req.NodeID == m.nodeID {
log.D.F("rejecting sync event-ids request from self (nodeID: %s)", req.NodeID)
// Cache the requesting relay URL as ours for future fast lookups
if req.RelayURL != "" {
m.mutex.Lock()
m.selfURLs[req.RelayURL] = true
m.mutex.Unlock()
log.D.F("cached self-URL from inbound request: %s", req.RelayURL)
}
http.Error(w, "Cannot sync with self", http.StatusBadRequest)
return
}