Files
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

205 lines
6.4 KiB
Go

package cli
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/netip"
"os"
"strings"
"time"
"github.com/spf13/cobra"
"gitea.mynodes.xyz/ilker/llmgw/internal/config"
"gitea.mynodes.xyz/ilker/llmgw/internal/firewall"
"gitea.mynodes.xyz/ilker/llmgw/internal/mirror"
)
// firewallStateFile is where a control plane writes what the cluster's
// settings say of the firewall, for llmgw-firewall-sync to apply as root:
// the server itself runs as llmgw and cannot change the ruleset.
const firewallStateFile = "/var/lib/llmgw/firewall.json"
// firewallState is what is written there: the same as the mirror is told
// and keeps beside its models.
type firewallState = mirror.FirewallState
// writeFirewallState records the settings' firewall for this machine's sync
// unit. Without the state directory — a server not run by systemd — there
// is no unit to read it, and nothing is written.
func writeFirewallState(cfg config.Server) error {
if _, err := os.Stat("/var/lib/llmgw"); err != nil {
return nil //nolint:nilerr // not a control plane set up by init
}
b, _ := json.Marshal(firewallState{Enabled: cfg.Firewall.Enabled, Networks: cfg.FirewallNetworks})
if old, err := os.ReadFile(firewallStateFile); err == nil && string(old) == string(b) {
return nil
}
tmp := firewallStateFile + ".new"
if err := os.WriteFile(tmp, b, 0o644); err != nil {
return err
}
return os.Rename(tmp, firewallStateFile)
}
// The units that apply it: a path unit watching the file, and the oneshot it
// starts.
const firewallSyncPathUnit = "llmgw-firewall-sync.path"
// firewallSyncUnits are the two units for a state file: on a control plane
// the one its server writes, on a mirror the one the control plane has the
// mirror write.
func firewallSyncUnits(state string) (service, pathUnit string) {
arg := ""
if state != firewallStateFile {
arg = " --state " + state
}
service = `[Unit]
Description=llmgw firewall: apply the cluster's settings on this machine
[Service]
Type=oneshot
ExecStart=/usr/local/bin/llmgw firewall sync` + arg + `
`
pathUnit = `[Unit]
Description=llmgw firewall: follow the cluster's settings on this machine
[Path]
PathChanged=` + state + `
[Install]
WantedBy=multi-user.target
`
return service, pathUnit
}
func newFirewallCmd() *cobra.Command {
c := &cobra.Command{
Use: "firewall",
Short: "llmgw's firewall on this machine",
Long: "Whether llmgw keeps a firewall is the cluster's setting firewall.enabled\n" +
"(llmgw ctl config set firewall.enabled false). Nodes are told by the\n" +
"control plane; a control plane writes it to " + firewallStateFile + ",\n" +
"which " + firewallSyncPathUnit + " applies with 'llmgw firewall sync'.",
}
var install bool
state := firewallStateFile
sync := &cobra.Command{
Use: "sync",
Short: "Apply what the cluster's settings say of the firewall, here (root)",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
if os.Geteuid() != 0 {
return errors.New("the firewall is changed as root: run it with sudo")
}
if install {
if err := installFirewallSync(cmd.Context(), firewall.Local{}, state); err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), firewallSyncPathUnit+" follows the cluster's settings here")
if _, err := os.Stat(state); err != nil {
return nil // written when the settings are first told
}
}
b, err := os.ReadFile(state)
if err != nil {
return err
}
var st firewallState
if err := json.Unmarshal(b, &st); err != nil {
return fmt.Errorf("%s: %w", state, err)
}
ctx, cancel := context.WithTimeout(cmd.Context(), 2*time.Minute)
defer cancel()
msg, err := syncFirewall(ctx, firewall.Local{}, st)
if err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), msg)
return nil
},
}
sync.Flags().BoolVar(&install, "install", false, "first install the units that run this whenever the settings change")
sync.Flags().StringVar(&state, "state", firewallStateFile,
"the file the settings are written to: a control plane's, or "+mirror.FirewallFile+" in a mirror's directory")
c.AddCommand(sync)
return c
}
// installFirewallSync writes the sync units on a machine and has the path
// unit follow the state file.
func installFirewallSync(ctx context.Context, r firewall.Runner, state string) error {
service, pathUnit := firewallSyncUnits(state)
body := service + "\x00" + pathUnit
out, err := r.Run(ctx, `set -e
t=$(mktemp -d); trap 'rm -rf "$t"' EXIT
cat > "$t/all"
python3 - "$t" <<'PY'
import sys
d = sys.argv[1]
a, b = open(d + "/all", "rb").read().split(b"\x00")
open(d + "/service", "wb").write(a)
open(d + "/path", "wb").write(b)
PY
sudo install -m 644 "$t/service" /etc/systemd/system/llmgw-firewall-sync.service
sudo install -m 644 "$t/path" /etc/systemd/system/`+firewallSyncPathUnit+`
sudo systemctl daemon-reload
sudo systemctl enable --now `+firewallSyncPathUnit+` >/dev/null 2>&1`, []byte(body))
if err != nil {
return fmt.Errorf("installing %s: %v: %s", firewallSyncPathUnit, err, string(out))
}
return nil
}
// syncFirewall puts a firewall state in force on this machine.
func syncFirewall(ctx context.Context, r firewall.Runner, st firewallState) (string, error) {
if !st.Enabled {
had, err := firewall.Disable(ctx, r)
if err != nil {
return "", err
}
if had {
return "llmgw's firewall is off here", nil
}
return "llmgw's firewall was already off here", nil
}
loaded, err := firewall.Enable(ctx, r)
if err != nil {
return "", err
}
if !loaded {
return "no llmgw rules on this machine to put back; 'llmgw server init' writes them", nil
}
var extra []netip.Prefix
for _, n := range st.Networks {
if p, err := netip.ParsePrefix(n); err == nil {
extra = append(extra, p)
} else if a, err := netip.ParseAddr(n); err == nil {
extra = append(extra, netip.PrefixFrom(a, a.BitLen()))
}
}
if len(extra) > 0 {
if _, err := firewall.Install(ctx, r, firewall.Spec{Allow: extra}); err != nil {
return "", err
}
}
// The cluster's machines: a node on a network this one has not let in
// yet, which the mirror's files are for.
if len(st.Toward) > 0 {
added, err := firewall.Admit(ctx, r, st.Toward)
if err != nil {
return "", err
}
if len(added) > 0 {
shown := make([]string, len(added))
for i, p := range added {
shown[i] = p.String()
}
return "llmgw's firewall is on here, and now lets in " + strings.Join(shown, ", "), nil
}
}
return "llmgw's firewall is on here", nil
}