The leading control plane tells the mirror over its API (PUT /v1/firewall, kept as .firewall.json beside the models and applied by the same path unit control planes use), and keeps the state in the store's cluster_state for members that run no llmgw, which a root timer reads from the member's own copy. The mirror also lets in the network of every node and control plane the leader knows. Tried on the PoC: off, on and a network added, on every kind of machine. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
147 lines
3.8 KiB
Go
147 lines
3.8 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net"
|
|
"net/netip"
|
|
"net/url"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/config"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/mirror"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
|
|
)
|
|
|
|
// firewallStateKey is where, in the store, the firewall's state is kept
|
|
// for the store's members to read.
|
|
const firewallStateKey = "firewall"
|
|
|
|
// firewallPublisher tells the machines that cannot ask a control plane
|
|
// what the cluster's settings say of their firewall: the mirror, over its
|
|
// API, and the store's members, through the store they hold a copy of.
|
|
// Nodes are told over their agents' streams and control planes by their
|
|
// own servers. On the leader only, at start, at every change to the
|
|
// firewall's settings, and every few minutes for nodes come and gone.
|
|
type firewallPublisher struct {
|
|
settings func() config.Server
|
|
st *store.DB
|
|
nodes func() []string
|
|
leader func() bool
|
|
log *slog.Logger
|
|
|
|
mu sync.Mutex
|
|
mirror *mirror.Client
|
|
kick chan struct{}
|
|
}
|
|
|
|
func newFirewallPublisher(settings func() config.Server, st *store.DB, nodes func() []string,
|
|
leader func() bool, log *slog.Logger) *firewallPublisher {
|
|
return &firewallPublisher{settings: settings, st: st, nodes: nodes, leader: leader, log: log,
|
|
kick: make(chan struct{}, 1)}
|
|
}
|
|
|
|
// setMirror has the mirror told too.
|
|
func (p *firewallPublisher) setMirror(m *mirror.Client) {
|
|
p.mu.Lock()
|
|
p.mirror = m
|
|
p.mu.Unlock()
|
|
p.Kick()
|
|
}
|
|
|
|
// Kick has the state worked out and told again now.
|
|
func (p *firewallPublisher) Kick() {
|
|
select {
|
|
case p.kick <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (p *firewallPublisher) run(ctx context.Context, every time.Duration) {
|
|
t := time.NewTicker(every)
|
|
defer t.Stop()
|
|
for {
|
|
p.once(ctx)
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-t.C:
|
|
case <-p.kick:
|
|
}
|
|
}
|
|
}
|
|
|
|
// state is what the settings say now, with the cluster's machines.
|
|
func (p *firewallPublisher) state(ctx context.Context) mirror.FirewallState {
|
|
cfg := p.settings()
|
|
st := mirror.FirewallState{Enabled: cfg.Firewall.Enabled, Networks: slices.Clone(cfg.FirewallNetworks)}
|
|
var toward []string
|
|
if p.nodes != nil {
|
|
toward = append(toward, p.nodes()...)
|
|
}
|
|
if p.st != nil {
|
|
if cps, err := p.st.ControlPlanes(ctx); err == nil {
|
|
for _, c := range cps {
|
|
toward = append(toward, c.URL)
|
|
}
|
|
}
|
|
}
|
|
for _, t := range toward {
|
|
if a, ok := hostAddr(t); ok {
|
|
st.Toward = append(st.Toward, a)
|
|
}
|
|
}
|
|
slices.Sort(st.Toward)
|
|
st.Toward = slices.Compact(st.Toward)
|
|
return st
|
|
}
|
|
|
|
// hostAddr is the address in an address, host:port or URL; names are not
|
|
// looked up here.
|
|
func hostAddr(s string) (string, bool) {
|
|
if u, err := url.Parse(s); err == nil && u.Host != "" {
|
|
s = u.Hostname()
|
|
} else if h, _, err := net.SplitHostPort(s); err == nil {
|
|
s = h
|
|
}
|
|
a, err := netip.ParseAddr(s)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
return a.Unmap().String(), true
|
|
}
|
|
|
|
func (p *firewallPublisher) once(ctx context.Context) {
|
|
if p.leader != nil && !p.leader() {
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
st := p.state(ctx)
|
|
p.mu.Lock()
|
|
m := p.mirror
|
|
p.mu.Unlock()
|
|
if m != nil {
|
|
changed, err := m.SetFirewall(ctx, st)
|
|
switch {
|
|
case err != nil:
|
|
p.log.Warn("could not tell the mirror what the settings say of its firewall", "err", err)
|
|
case changed:
|
|
p.log.Info("the mirror's firewall follows the cluster's settings", "enabled", st.Enabled)
|
|
}
|
|
}
|
|
if p.st != nil {
|
|
b, _ := json.Marshal(st)
|
|
changed, err := p.st.PutClusterState(ctx, firewallStateKey, string(b))
|
|
switch {
|
|
case err != nil:
|
|
p.log.Warn("could not record the firewall's state for the store's members", "err", err)
|
|
case changed:
|
|
p.log.Info("the store's members' firewall follows the cluster's settings", "enabled", st.Enabled)
|
|
}
|
|
}
|
|
}
|