Files
llmgw/internal/cli/firewallpub.go
Ilker ManapandClaude Opus 5.5 8df78a4d41 The firewall setting reaches the mirror machine and the store's members
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>
2026-10-09 08:53:17 +02:00

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)
}
}
}