firewall.enabled (default true) is a cluster setting, applied live: the leader sends a Firewall message to every agent when it registers and when the setting or firewall_networks changes, and the agent (root) takes llmgw's nft table out or installs it. A control plane writes the state to /var/lib/llmgw/firewall.json, which llmgw-firewall-sync.path applies as root with `llmgw firewall sync`; server init and server add install the units, `llmgw firewall sync --install` on one set up before. node add and server add put no firewall on a machine while the setting is off. `llmgw wipe` removes llmgw from a machine after listing what goes and asking for its name: units, engines' processes, the nft table, /etc/llmgw, /var/lib/llmgw, the agent's state, the sudo grant, the CA from the trust store, keepalived's configuration when llmgw wrote it, and the users. --models, --store (Patroni member, etcd, or the local database) and --all take more. The console has it under Do. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
140 lines
4.2 KiB
Go
140 lines
4.2 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/config"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/firewall"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/pgha"
|
|
)
|
|
|
|
// ensureFirewall keeps this control plane's internal ports to the networks
|
|
// the cluster is on: the agents' stream, the metrics, the mirror's files,
|
|
// the engines and etcd when they are here. The client API is not among
|
|
// them; it is meant to be reached.
|
|
func (in *serverInit) ensureFirewall(ctx context.Context) error {
|
|
cfg, err := loadEffectiveConfig(in.configPath())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// What applies the cluster's setting here when it changes: the server
|
|
// writes it, as llmgw; this applies it, as root.
|
|
if err := in.ensureFirewallSync(ctx); err != nil {
|
|
return err
|
|
}
|
|
if !cfg.Firewall.Enabled {
|
|
in.kept("firewall: off, as the cluster's settings say (firewall.enabled)")
|
|
return nil
|
|
}
|
|
ports := controlPlanePorts(cfg)
|
|
if !in.noMirror && in.mirrorURL == "" {
|
|
ports = append(ports, firewall.MirrorFiles)
|
|
}
|
|
if !in.noNode {
|
|
from, to := cfg.EnginePorts()
|
|
ports = append(ports, firewall.Engines(from, to), firewall.NodeExporter)
|
|
}
|
|
if _, err := os.Stat("/etc/default/etcd"); err == nil {
|
|
// The store's third etcd vote.
|
|
ports = append(ports, firewall.EtcdClient, firewall.EtcdPeer)
|
|
}
|
|
self, err := ipv4Of(in.reachableAddr())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nets, err := firewall.Networks(ctx, pgha.Local{}, []string{self})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
extra, _ := cfg.FirewallAllow()
|
|
nets = append(nets, extra...)
|
|
changed, err := firewall.Install(ctx, pgha.Local{}, firewall.Spec{Ports: ports, Allow: nets})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
shown := make([]string, len(nets))
|
|
for i, n := range nets {
|
|
shown[i] = n.String()
|
|
}
|
|
if changed {
|
|
in.done("firewall: llmgw's internal ports answer %s only (%s)", strings.Join(shown, ", "), firewall.RulesFile)
|
|
} else {
|
|
in.kept("firewall: llmgw's internal ports answer %s only", strings.Join(shown, ", "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// storeFirewall keeps each store member's database, Patroni and etcd ports to
|
|
// the networks it reaches this control plane and the other members on. A
|
|
// member it cannot be set on is said, and left as it was.
|
|
func (in *serverInit) storeFirewall(ctx context.Context, members []pgha.Node, self string, extra []netip.Prefix) {
|
|
ports := []firewall.Port{firewall.Postgres, firewall.PatroniAPI, firewall.EtcdClient, firewall.EtcdPeer,
|
|
firewall.NodeExporter}
|
|
for _, m := range members {
|
|
toward := []string{self}
|
|
for _, o := range members {
|
|
if o.IP != m.IP {
|
|
toward = append(toward, o.IP)
|
|
}
|
|
}
|
|
nets, err := firewall.Networks(ctx, m.Remote, toward)
|
|
if err == nil {
|
|
nets = append(nets, extra...)
|
|
var changed bool
|
|
if changed, err = firewall.Install(ctx, m.Remote, firewall.Spec{Ports: ports, Allow: nets}); err == nil {
|
|
if changed {
|
|
in.done("firewall on %s: the store's ports answer the cluster's networks only", m.Name)
|
|
} else {
|
|
in.kept("firewall on %s", m.Name)
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
fmt.Fprintf(in.out, " !! firewall on %s: %v\n", m.Name, err)
|
|
}
|
|
}
|
|
|
|
// controlPlanePorts are a control plane's internal ports, from its
|
|
// configuration: the agents' stream, and the metrics when they listen
|
|
// beyond this machine.
|
|
func controlPlanePorts(cfg config.Server) []firewall.Port {
|
|
var ports []firewall.Port
|
|
if p := portOf(cfg.AgentListen); p > 0 {
|
|
ports = append(ports, firewall.Single(p, "control plane: the agents' stream"))
|
|
}
|
|
if p := portOf(cfg.MetricsListen); p > 0 && !loopbackListen(cfg.MetricsListen) {
|
|
ports = append(ports, firewall.Single(p, "control plane: metrics, for Prometheus"))
|
|
}
|
|
return ports
|
|
}
|
|
|
|
func portOf(listen string) int {
|
|
_, port, err := net.SplitHostPort(listen)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
p, _ := strconv.Atoi(port)
|
|
return p
|
|
}
|
|
|
|
func loopbackListen(listen string) bool {
|
|
host, _, err := net.SplitHostPort(listen)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
ip := net.ParseIP(host)
|
|
return host == "localhost" || (ip != nil && ip.IsLoopback())
|
|
}
|
|
|
|
// ensureFirewallSync installs the units that apply the cluster's firewall
|
|
// setting on this control plane.
|
|
func (in *serverInit) ensureFirewallSync(ctx context.Context) error {
|
|
return installFirewallSync(ctx, firewall.Local{})
|
|
}
|