Skip to content

Commit da4d140

Browse files
authored
Merge pull request #454 from aojea/control-plane-mesh-publisher
control-plane: publish mesh events for real
2 parents c6e012c + c2ea967 commit da4d140

7 files changed

Lines changed: 333 additions & 31 deletions

File tree

cmd/sam-control-plane/main.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ var (
4646
biscuitTTL time.Duration
4747
oidcSessionTTL time.Duration
4848
nodeRetention time.Duration
49+
meshReconnectInterval time.Duration
4950
adminTokenPath string
5051
insecureSkipTLSVerify bool
5152
logLevel string
@@ -142,6 +143,19 @@ func main() {
142143
}
143144
}()
144145

146+
// Bans, key rotations and policy updates reach the mesh as they
147+
// happen; every consumer also pulls, so this is speed, not truth.
148+
mesh, err := controlplane.NewMeshPublisher(cmd.Context(), store, meshReconnectInterval)
149+
if err != nil {
150+
logger.Fatalf("Failed to start mesh event publisher: %v", err)
151+
}
152+
defer func() {
153+
if err := mesh.Close(); err != nil {
154+
logger.Errorf("Failed to stop mesh event publisher: %v", err)
155+
}
156+
}()
157+
srv.SetMeshAdapter(mesh)
158+
145159
if err := srv.Start(); err != nil {
146160
logger.Fatalf("Failed to start control plane: %v", err)
147161
}
@@ -164,6 +178,7 @@ func main() {
164178
rootCmd.Flags().DurationVar(&biscuitTTL, "biscuit-ttl", api.BiscuitTokenTTL, "Lifespan minted into every issued Biscuit's expiration fact. Capped to the OIDC token's own expiry when shorter.")
165179
rootCmd.Flags().DurationVar(&oidcSessionTTL, "oidc-session-ttl", api.OIDCSessionTTL, "How long an OIDC enrollment stays refreshable before the identity must re-authenticate with the OIDC provider. Shorter values keep the provider authoritative for offboarding at the cost of more frequent interactive re-enrollment.")
166180
rootCmd.Flags().DurationVar(&nodeRetention, "node-retention", controlplane.DefaultNodeRetention, "How long an enrolled node's record is kept after its session expired before it is deleted. Banned nodes are always kept. 0 keeps every record forever.")
181+
rootCmd.Flags().DurationVar(&meshReconnectInterval, "mesh-reconnect-interval", controlplane.DefaultMeshReconnectInterval, "How often the event publisher re-reads the router leases and dials any router it is not connected to.")
167182
rootCmd.Flags().StringVar(&adminTokenPath, "admin-token-path", "", "Path to file containing the token for authenticating policy REST API requests (or env SAM_ADMIN_TOKEN)")
168183
rootCmd.Flags().BoolVar(&insecureSkipTLSVerify, "insecure-skip-tls-verify", false, "Skip TLS verification for OIDC providers")
169184
rootCmd.Flags().StringVar(&logLevel, "log-level", "info", "Log level (debug, info, warn, error)")

internal/controlplane/mesh.go

Lines changed: 144 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
91101
type 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

128249
func (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
218339
func (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

Comments
 (0)