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>
177 lines
5.4 KiB
Go
177 lines
5.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/netip"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/config"
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/firewall"
|
|
)
|
|
|
|
// 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.
|
|
type firewallState struct {
|
|
Enabled bool `json:"enabled"`
|
|
Networks []string `json:"networks,omitempty"`
|
|
}
|
|
|
|
// 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"
|
|
firewallSyncUnitFile = `[Unit]
|
|
Description=llmgw firewall: apply the cluster's settings on this control plane
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=/usr/local/bin/llmgw firewall sync
|
|
`
|
|
firewallSyncPathFile = `[Unit]
|
|
Description=llmgw firewall: follow the cluster's settings on this control plane
|
|
|
|
[Path]
|
|
PathChanged=` + firewallStateFile + `
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
`
|
|
)
|
|
|
|
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
|
|
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{}); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintln(cmd.OutOrStdout(), firewallSyncPathUnit+" follows the cluster's settings here")
|
|
if _, err := os.Stat(firewallStateFile); err != nil {
|
|
return nil // the server writes it when it starts
|
|
}
|
|
}
|
|
b, err := os.ReadFile(firewallStateFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var st firewallState
|
|
if err := json.Unmarshal(b, &st); err != nil {
|
|
return fmt.Errorf("%s: %w", firewallStateFile, 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")
|
|
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) error {
|
|
body := firewallSyncUnitFile + "\x00" + firewallSyncPathFile
|
|
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
|
|
}
|
|
}
|
|
return "llmgw's firewall is on here", nil
|
|
}
|