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 }