package node import ( "context" "encoding/json" "time" "oc-discovery/daemons/node/common" "oc-discovery/daemons/node/indexer" oclib "cloud.o-forge.io/core/oc-lib" "cloud.o-forge.io/core/oc-lib/dbs" "cloud.o-forge.io/core/oc-lib/models/peer" "github.com/libp2p/go-libp2p/core/control" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/network" pp "github.com/libp2p/go-libp2p/core/peer" ma "github.com/multiformats/go-multiaddr" ) // OCConnectionGater enforces two rules on every inbound connection: // 1. If the peer is known locally and blacklisted → reject. // 2. If the peer is unknown locally → ask indexers one by one whether it // exists in the DHT. Accept as soon as one confirms it; reject if none do // (or if no indexers are reachable yet, allow optimistically). // // Outbound connections are always allowed — we chose to dial them. type OCConnectionGater struct { host host.Host } func newOCConnectionGater(h host.Host) *OCConnectionGater { return &OCConnectionGater{host: h} } // InterceptPeerDial — allow all outbound dials. func (g *OCConnectionGater) InterceptPeerDial(_ pp.ID) bool { return true } // InterceptAddrDial — allow all outbound dials. func (g *OCConnectionGater) InterceptAddrDial(_ pp.ID, _ ma.Multiaddr) bool { return true } // InterceptAccept — allow at transport level (PeerID not yet known). func (g *OCConnectionGater) InterceptAccept(_ network.ConnMultiaddrs) bool { return true } // InterceptUpgraded — final gate; always allow (decisions already made in InterceptSecured). func (g *OCConnectionGater) InterceptUpgraded(_ network.Conn) (bool, control.DisconnectReason) { return true, 0 } // InterceptSecured is called after the cryptographic handshake — PeerID is now known. // Only inbound connections are verified; outbound are trusted. func (g *OCConnectionGater) InterceptSecured(dir network.Direction, pid pp.ID, _ network.ConnMultiaddrs) bool { if dir == network.DirOutbound { return true } logger := oclib.GetLogger() // 1. Local DB lookup by PeerID. access := oclib.NewRequestAdmin(oclib.LibDataEnum(oclib.PEER), nil) results := access.Search(&dbs.Filters{ And: map[string][]dbs.Filter{ // search by name if no filters are provided "peer_id": {{Operator: dbs.EQUAL.String(), Value: pid.String()}}, }, }, pid.String(), false) for _, item := range results.Data { p, ok := item.(*peer.Peer) if !ok || p.PeerID != pid.String() { continue } if p.Relation == peer.BLACKLIST { logger.Warn().Str("peer", pid.String()).Msg("[gater] rejected blacklisted peer") return false } // Known, not blacklisted. return true } // 2. Unknown locally — verify via indexers. indexers := common.Indexers.GetAddrs() if len(indexers) == 0 { // No indexers reachable yet — allow optimistically (bootstrap phase). logger.Warn().Str("peer", pid.String()).Msg("[gater] no indexers available, allowing unverified inbound") return true } req := indexer.GetValue{PeerID: pid.String()} // A single DHT GetValue already traverses the entire DHT network, so asking // a second indexer would yield the same result. We only fall through to the // next indexer if the current one is unreachable (transport error), not if // it returns found=false (that answer is already DHT-wide authoritative). for _, ai := range indexers { found, reachable := queryIndexerPeerExists(g.host, *ai.Info, req) if !reachable { continue // indexer down — try next } if !found { logger.Warn().Str("peer", pid.String()).Msg("[gater] peer not found in DHT, rejecting inbound") } return found // definitive DHT answer } // All indexers unreachable — allow optimistically rather than blocking indefinitely. logger.Warn().Str("peer", pid.String()).Msg("[gater] all indexers unreachable, allowing unverified inbound") return true } // queryIndexerPeerExists opens a fresh one-shot stream to ai, sends a GetValue // request, and returns (found, reachable). // reachable=false means the indexer could not be reached (transport error); // the caller should then try another indexer. // reachable=true means the indexer answered — found is the DHT-wide authoritative result. func queryIndexerPeerExists(h host.Host, ai pp.AddrInfo, req indexer.GetValue) (found, reachable bool) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if h.Network().Connectedness(ai.ID) != network.Connected { if err := h.Connect(ctx, ai); err != nil { return false, false } } s, err := h.NewStream(ctx, ai.ID, common.ProtocolGet) if err != nil { return false, false } defer s.Close() s.SetDeadline(time.Now().Add(3 * time.Second)) if err := json.NewEncoder(s).Encode(req); err != nil { return false, false } var resp indexer.GetResponse if err := json.NewDecoder(s).Decode(&resp); err != nil { return false, false } return resp.Found, true }