138 lines
4.8 KiB
Go
138 lines
4.8 KiB
Go
|
|
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
|
||
|
|
}
|