@@ -22,10 +22,12 @@ import (
2222 "sync"
2323 "time"
2424
25+ "github.com/libp2p/go-libp2p"
2526 pubsub "github.com/libp2p/go-libp2p-pubsub"
2627 "github.com/libp2p/go-libp2p/core/host"
28+ "github.com/libp2p/go-libp2p/core/network"
2729 "github.com/libp2p/go-libp2p/core/peer"
28- "github.com/libp2p /go-libp2p/core/peerstore "
30+ "github.com/multiformats /go-multiaddr "
2931 "google.golang.org/protobuf/proto"
3032
3133 "github.com/google/sam/api"
@@ -87,42 +89,161 @@ func (n *NopMeshAdapter) Close() error {
8789 return nil
8890}
8991
90- // P2PMeshAdapter implements MeshAdapter using a libp2p Host and GossipSub subscriber/publisher.
92+ // P2PMeshAdapter publishes control plane events on the mesh's gossip topic.
93+ //
94+ // It is the control plane's whole presence on the mesh, and it is one-way:
95+ // events go out so a ban, a key rotation or a policy change reaches routers
96+ // and nodes the moment it happens, and nothing is read back. Every consumer
97+ // still pulls /keys, /info and /policies on its own schedule, so a missed
98+ // event is a delay, never a divergence. Where the adapter runs on a host of
99+ // its own (NewMeshPublisher) that host has no listen address, no DHT, no
100+ // relay and no stream handlers: it can dial routers and nothing can dial it.
91101type P2PMeshAdapter struct {
92102 host host.Host
93- ps * pubsub.PubSub
94103 topic * pubsub.Topic
95104 store storage.Store
96105 mu sync.Mutex
106+ // close tears down what NewMeshPublisher built; nil when the host and
107+ // topic belong to someone else (sam-one's embedded router).
108+ close func () error
109+ }
110+
111+ // NewP2PMeshAdapter publishes on an existing host's topic. The caller owns
112+ // both and closes them; Close on the adapter is a no-op.
113+ func NewP2PMeshAdapter (h host.Host , topic * pubsub.Topic , store storage.Store ) (* P2PMeshAdapter , error ) {
114+ if h == nil || topic == nil || store == nil {
115+ return nil , fmt .Errorf ("host, topic, and store cannot be nil" )
116+ }
117+ if topic .String () != api .GossipEvents {
118+ return nil , fmt .Errorf ("topic %q is not the mesh events topic %q" , topic .String (), api .GossipEvents )
119+ }
120+ return & P2PMeshAdapter {host : h , topic : topic , store : store }, nil
97121}
98122
99- func NewP2PMeshAdapter (h host.Host , ps * pubsub.PubSub , store storage.Store ) (* P2PMeshAdapter , error ) {
100- if h == nil || ps == nil || store == nil {
101- return nil , fmt .Errorf ("host, pubsub, and store cannot be nil" )
123+ // RouterDialTimeout bounds each attempt to reach a leased router.
124+ const RouterDialTimeout = 10 * time .Second
125+
126+ // DefaultMeshReconnectInterval is how often the publisher re-reads the lease
127+ // table; a router that just enrolled waits at most this long for events. It
128+ // is one query and at most a few dials per tick, so it is kept short.
129+ const DefaultMeshReconnectInterval = 30 * time .Second
130+
131+ // NewMeshPublisher builds the control plane's own publish-only peer and
132+ // keeps it connected to every router holding a lease, re-checking the lease
133+ // table every reconnect. The lease table is all the control plane needs to
134+ // know about the mesh's shape, and it already has it. Close stops the loop
135+ // and the host.
136+ func NewMeshPublisher (ctx context.Context , store storage.Store , reconnect time.Duration ) (* P2PMeshAdapter , error ) {
137+ if store == nil {
138+ return nil , fmt .Errorf ("store cannot be nil" )
139+ }
140+ if reconnect <= 0 {
141+ return nil , fmt .Errorf ("reconnect interval must be positive, got %s" , reconnect )
142+ }
143+ h , err := libp2p .New (libp2p .NoListenAddrs , libp2p .DisableRelay ())
144+ if err != nil {
145+ return nil , fmt .Errorf ("mesh publisher host: %w" , err )
146+ }
147+ loopCtx , cancel := context .WithCancel (ctx )
148+ // StrictSign is the default; pinned because routers and nodes key their
149+ // per-author rate limit on the signed sender.
150+ ps , err := pubsub .NewGossipSub (loopCtx , h , pubsub .WithMessageSignaturePolicy (pubsub .StrictSign ))
151+ if err != nil {
152+ cancel ()
153+ _ = h .Close ()
154+ return nil , fmt .Errorf ("mesh publisher gossipsub: %w" , err )
102155 }
103156 topic , err := ps .Join (api .GossipEvents )
104157 if err != nil {
105- return nil , fmt .Errorf ("failed to join gossip events topic %s: %w" , api .GossipEvents , err )
158+ cancel ()
159+ _ = h .Close ()
160+ return nil , fmt .Errorf ("join %s: %w" , api .GossipEvents , err )
161+ }
162+ p := & P2PMeshAdapter {host : h , topic : topic , store : store }
163+ var wg sync.WaitGroup
164+ wg .Add (1 )
165+ go func () {
166+ defer wg .Done ()
167+ p .keepRoutersConnected (loopCtx , reconnect )
168+ }()
169+ p .close = func () error {
170+ cancel ()
171+ wg .Wait ()
172+ _ = topic .Close ()
173+ return h .Close ()
106174 }
175+ logger .Infof ("[Mesh] Publishing control plane events as %s" , h .ID ())
176+ return p , nil
177+ }
107178
108- return & P2PMeshAdapter {
109- host : h ,
110- ps : ps ,
111- topic : topic ,
112- store : store ,
113- }, nil
179+ // keepRoutersConnected dials, now and every interval, each leased router the
180+ // host is not connected to. A publish only reaches peers that have announced
181+ // the topic, so connections are kept warm ahead of the events rather than
182+ // made when one is due.
183+ func (p * P2PMeshAdapter ) keepRoutersConnected (ctx context.Context , interval time.Duration ) {
184+ ticker := time .NewTicker (interval )
185+ defer ticker .Stop ()
186+ for {
187+ p .connectRouters (ctx )
188+ select {
189+ case <- ctx .Done ():
190+ return
191+ case <- ticker .C :
192+ }
193+ }
194+ }
195+
196+ func (p * P2PMeshAdapter ) connectRouters (ctx context.Context ) {
197+ routers , err := p .store .GetActiveRouters (ctx )
198+ if err != nil {
199+ logger .Warnf ("[Mesh] Cannot list routers to publish to: %v" , err )
200+ return
201+ }
202+ for _ , r := range routers {
203+ info , err := routerAddrInfo (r )
204+ if err != nil {
205+ logger .Warnf ("[Mesh] Skipping router lease %q: %v" , r .PeerID , err )
206+ continue
207+ }
208+ if p .host .Network ().Connectedness (info .ID ) == network .Connected {
209+ continue
210+ }
211+ dialCtx , cancel := context .WithTimeout (ctx , RouterDialTimeout )
212+ err = p .host .Connect (dialCtx , info )
213+ cancel ()
214+ if err != nil {
215+ logger .Warnf ("[Mesh] Router %s unreachable for event publishing: %v" , info .ID , err )
216+ continue
217+ }
218+ logger .Infof ("[Mesh] Connected to router %s" , info .ID )
219+ }
114220}
115221
116- func (p * P2PMeshAdapter ) ConnectPeer (ctx context.Context , targetAddr string ) error {
117- info , err := peer .AddrInfoFromString (targetAddr )
222+ // routerAddrInfo is the dial target for a lease. Routers announce their
223+ // addresses with a trailing /p2p/<id>; the dialer wants the id once and the
224+ // addresses bare, and an address naming a different peer is not this
225+ // router's.
226+ func routerAddrInfo (r storage.RouterLease ) (peer.AddrInfo , error ) {
227+ pid , err := peer .Decode (r .PeerID )
118228 if err != nil {
119- return fmt .Errorf ("invalid peer address string %q : %w" , targetAddr , err )
229+ return peer. AddrInfo {}, fmt .Errorf ("invalid peer ID : %w" , err )
120230 }
121- p .host .Peerstore ().AddAddrs (info .ID , info .Addrs , peerstore .PermanentAddrTTL )
122- if err := p .host .Connect (ctx , * info ); err != nil {
123- return fmt .Errorf ("failed to connect to peer %s: %w" , info .ID , err )
231+ info := peer.AddrInfo {ID : pid }
232+ for _ , s := range r .Addresses {
233+ ma , err := multiaddr .NewMultiaddr (s )
234+ if err != nil {
235+ continue
236+ }
237+ addr , id := peer .SplitAddr (ma )
238+ if addr == nil || (id != "" && id != pid ) {
239+ continue
240+ }
241+ info .Addrs = append (info .Addrs , addr )
124242 }
125- return nil
243+ if len (info .Addrs ) == 0 {
244+ return peer.AddrInfo {}, fmt .Errorf ("no dialable address in %v" , r .Addresses )
245+ }
246+ return info , nil
126247}
127248
128249func (p * P2PMeshAdapter ) PublishEvent (ctx context.Context , eventType api.MeshEvent_Type , peerID string , payload []byte ) error {
@@ -218,11 +339,8 @@ func (p *P2PMeshAdapter) GetNodeStatus(ctx context.Context, peerID string) (*Nod
218339func (p * P2PMeshAdapter ) Close () error {
219340 p .mu .Lock ()
220341 defer p .mu .Unlock ()
221- if p .topic ! = nil {
222- _ = p . topic . Close ()
342+ if p .close = = nil {
343+ return nil
223344 }
224- if p .host != nil {
225- return p .host .Close ()
226- }
227- return nil
345+ return p .close ()
228346}
0 commit comments