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>
205 lines
6.4 KiB
Go
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
|
|
}
|