复用现有 k3s 调度和 Job 生命周期,增加 worker 接入与批准、受保护节点身份、归档传输、持久迁移锁及活动 PVC 切换;同步管理员 API、CLI、面板和隔离规则。分布式模式默认关闭,保持单机兼容。 验证:Go 全量测试与 vet;面板 874 个测试、lint/build;Linux VM 安装器测试、清单服务端 dry-run、网络命名空间防火墙实测。A/B/C 三机 WireGuard、Velocity 和迁移验收仍待完成。
188 lines
8.4 KiB
Go
188 lines
8.4 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func nodeFirewall(args []string, stdout, stderr io.Writer) int {
|
|
fs := flag.NewFlagSet("node firewall", flag.ContinueOnError)
|
|
fs.SetOutput(stderr)
|
|
peers := fs.String("peers", "", "comma-separated exact node IP CIDRs")
|
|
controller := fs.String("controller-ip", "", "controller address (optionally :6443)")
|
|
main := fs.Bool("controller", false, "A hosts the public API NodePort")
|
|
pod := fs.String("pod-cidr", "10.42.0.0/16", "cluster Pod CIDR")
|
|
dryRun := fs.Bool("dry-run", false, "print firewall scripts without installing")
|
|
controlNS := fs.String("control-namespace", "felis", "controller namespace")
|
|
minecraftNS := fs.String("namespace", "minecraft", "game namespace")
|
|
apiIP := fs.String("api-service-ip", "10.43.0.1", "Kubernetes API Service IP")
|
|
port := fs.Int("node-port", 30443, "API NodePort to block on workers before DNAT")
|
|
if err := fs.Parse(args); err != nil {
|
|
return 2
|
|
}
|
|
if host, _, err := net.SplitHostPort(*controller); err == nil {
|
|
*controller = host
|
|
}
|
|
if net.ParseIP(*apiIP) == nil || net.ParseIP(*controller) == nil || *port < 30000 || *port > 32767 {
|
|
fmt.Fprintln(stderr, "node firewall: controller IP and NodePort required")
|
|
return 2
|
|
}
|
|
if _, _, err := net.ParseCIDR(*pod); err != nil {
|
|
fmt.Fprintln(stderr, err)
|
|
return 2
|
|
}
|
|
var v4, v6 []string
|
|
for _, p := range strings.Split(*peers, ",") {
|
|
ip, n, err := net.ParseCIDR(p)
|
|
if err != nil {
|
|
fmt.Fprintln(stderr, "node firewall: invalid peer CIDR")
|
|
return 2
|
|
}
|
|
ones, bits := n.Mask.Size()
|
|
if ones != bits {
|
|
fmt.Fprintln(stderr, "node firewall: only exact peer addresses accepted")
|
|
return 2
|
|
}
|
|
if ip.To4() != nil {
|
|
v4 = append(v4, n.String())
|
|
} else {
|
|
v6 = append(v6, n.String())
|
|
}
|
|
}
|
|
script := "#!/bin/bash\nset -euo pipefail\n"
|
|
for _, f := range []struct {
|
|
bin string
|
|
peers []string
|
|
metadata string
|
|
}{{"iptables", v4, "169.254.0.0/16"}, {"ip6tables", v6, "fe80::/10"}} {
|
|
if f.bin == "ip6tables" && len(f.peers) == 0 {
|
|
continue
|
|
}
|
|
b := f.bin + " -w 10"
|
|
script += b + " -N FELIS-HOST 2>/dev/null || true\n" + b + " -F FELIS-HOST\n"
|
|
script += b + " -N FELIS-FORWARD 2>/dev/null || true\n" + b + " -F FELIS-FORWARD\n"
|
|
script += b + " -t raw -N FELIS-NODEPORT 2>/dev/null || true\n" + b + " -t raw -F FELIS-NODEPORT\n"
|
|
for _, c := range []struct{ table, parent, chain string }{{"filter", "INPUT", "FELIS-HOST"}, {"filter", "FORWARD", "FELIS-FORWARD"}, {"raw", "PREROUTING", "FELIS-NODEPORT"}} {
|
|
script += "while " + b + " -t " + c.table + " -D " + c.parent + " -j " + c.chain + " 2>/dev/null; do :; done\n" + b + " -t " + c.table + " -I " + c.parent + " 1 -j " + c.chain + "\n"
|
|
}
|
|
script += b + " -A FELIS-HOST -i lo -j RETURN\n"
|
|
if *main {
|
|
script += b + " -N FELIS-CONTROL 2>/dev/null || true\n" + b + " -A FELIS-HOST -j FELIS-CONTROL\n"
|
|
script += b + " -t raw -N FELIS-CONTROL 2>/dev/null || true\n" + b + " -t raw -A FELIS-NODEPORT -j FELIS-CONTROL\n"
|
|
}
|
|
script += b + " -A FELIS-HOST -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN\n" + b + " -A FELIS-FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN\n"
|
|
family := 4
|
|
if f.bin == "ip6tables" {
|
|
family = 6
|
|
}
|
|
podIP, _, _ := net.ParseCIDR(*pod)
|
|
podFamily := 6
|
|
if podIP.To4() != nil {
|
|
podFamily = 4
|
|
}
|
|
if family == podFamily {
|
|
script += b + " -A FELIS-HOST -s " + *pod + " -j DROP\n"
|
|
// raw precedes DNAT and kube-router's filter ACCEPT rules. Block new
|
|
// host connections while preserving established Velocity/RCON replies.
|
|
script += b + " -t raw -A FELIS-NODEPORT -s " + *pod + " -m addrtype --dst-type LOCAL -p tcp --syn -j DROP\n"
|
|
script += b + " -t raw -A FELIS-NODEPORT -s " + *pod + " -m addrtype --dst-type LOCAL -p udp -j DROP\n"
|
|
if (net.ParseIP(*apiIP).To4() != nil) == (family == 4) {
|
|
script += b + " -t raw -A FELIS-NODEPORT -s " + *pod + " -d " + *apiIP + " -p tcp --dport 443 --syn -j DROP\n"
|
|
}
|
|
|
|
script += b + " -t raw -A FELIS-NODEPORT -s " + *pod + " -m addrtype --dst-type LOCAL -p tcp --dport " + strconv.Itoa(*port) + " -j DROP\n"
|
|
script += b + " -A FELIS-FORWARD -s " + *pod + " -d " + f.metadata + " -j DROP\n"
|
|
script += b + " -t raw -A FELIS-NODEPORT -s " + *pod + " -d " + f.metadata + " -j DROP\n"
|
|
for _, p := range f.peers {
|
|
script += b + " -A FELIS-FORWARD -s " + *pod + " -d " + p + " -j DROP\n"
|
|
script += b + " -t raw -A FELIS-NODEPORT -s " + *pod + " -d " + p + " -p tcp --syn -j DROP\n"
|
|
script += b + " -t raw -A FELIS-NODEPORT -s " + *pod + " -d " + p + " -p udp -j DROP\n"
|
|
}
|
|
}
|
|
if !*main {
|
|
script += b + " -t raw -A FELIS-NODEPORT -m addrtype --dst-type LOCAL -p tcp --dport " + strconv.Itoa(*port) + " -j DROP\n"
|
|
}
|
|
for _, p := range f.peers {
|
|
script += b + " -A FELIS-HOST -s " + p + " -p udp --dport 51820:51821 -j RETURN\n"
|
|
}
|
|
script += b + " -A FELIS-HOST -p udp --dport 51820:51821 -j DROP\n"
|
|
ctrlIP := net.ParseIP(*controller)
|
|
ctrlFamily := 6
|
|
if ctrlIP.To4() != nil {
|
|
ctrlFamily = 4
|
|
}
|
|
if *main {
|
|
for _, p := range f.peers {
|
|
script += b + " -A FELIS-HOST -s " + p + " -p tcp --dport 6443 -j RETURN\n"
|
|
}
|
|
}
|
|
if ctrlFamily == family {
|
|
script += b + " -A FELIS-HOST -s " + ctrlIP.String() + " -p tcp --dport 10250 -j RETURN\n"
|
|
}
|
|
script += b + " -A FELIS-HOST -p tcp -m multiport --dports 6443,6444,10250,10255,2379,2380,5000,5001,15432 -j DROP\n"
|
|
}
|
|
if *dryRun {
|
|
fmt.Fprint(stdout, script)
|
|
if *main {
|
|
fmt.Fprint(stdout, controlFirewallScript(*controlNS, *minecraftNS))
|
|
}
|
|
return 0
|
|
}
|
|
if err := os.MkdirAll("/etc/felis", 0700); err != nil {
|
|
fmt.Fprintln(stderr, err)
|
|
return 1
|
|
}
|
|
if err := os.WriteFile("/etc/felis/node-firewall.sh", []byte(script), 0700); err != nil {
|
|
fmt.Fprintln(stderr, err)
|
|
return 1
|
|
}
|
|
unit := "[Unit]\nDescription=Felis host and NodePort isolation\nAfter=network-online.target firewalld.service ufw.service\nBefore=k3s.service k3s-agent.service\n[Service]\nType=oneshot\nExecStart=/etc/felis/node-firewall.sh\nRemainAfterExit=yes\n[Install]\nWantedBy=multi-user.target\n"
|
|
if err := os.WriteFile("/etc/systemd/system/felis-node-firewall.service", []byte(unit), 0644); err != nil {
|
|
fmt.Fprintln(stderr, err)
|
|
return 1
|
|
}
|
|
if *main {
|
|
refresh := controlFirewallScript(*controlNS, *minecraftNS)
|
|
if err := os.WriteFile("/etc/felis/control-firewall.sh", []byte(refresh), 0700); err != nil {
|
|
fmt.Fprintln(stderr, err)
|
|
return 1
|
|
}
|
|
svc := "[Unit]\nDescription=Refresh exact controller Pod access to the apiserver\nAfter=k3s.service\n[Service]\nType=oneshot\nExecStart=/etc/felis/control-firewall.sh\n"
|
|
timer := "[Unit]\nDescription=Track trusted controller Pods after rescheduling\n[Timer]\nOnBootSec=5s\nOnUnitActiveSec=5s\n[Install]\nWantedBy=timers.target\n"
|
|
if err := os.WriteFile("/etc/systemd/system/felis-control-firewall.service", []byte(svc), 0644); err != nil {
|
|
return 1
|
|
}
|
|
if err := os.WriteFile("/etc/systemd/system/felis-control-firewall.timer", []byte(timer), 0644); err != nil {
|
|
return 1
|
|
}
|
|
}
|
|
for _, cmd := range []*exec.Cmd{exec.Command("systemctl", "daemon-reload"), exec.Command("systemctl", "enable", "felis-node-firewall.service"), exec.Command("bash", "/etc/felis/node-firewall.sh")} {
|
|
cmd.Stdout = stdout
|
|
cmd.Stderr = stderr
|
|
if err := cmd.Run(); err != nil {
|
|
fmt.Fprintln(stderr, err)
|
|
return 1
|
|
}
|
|
}
|
|
if *main {
|
|
cmd := exec.Command("systemctl", "enable", "--now", "felis-control-firewall.timer")
|
|
cmd.Stdout = stdout
|
|
cmd.Stderr = stderr
|
|
if err := cmd.Run(); err != nil {
|
|
fmt.Fprintln(stderr, err)
|
|
return 1
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func controlFirewallScript(controlNS, minecraftNS string) string {
|
|
return "#!/bin/bash\nset -euo pipefail\niptables -w 10 -F FELIS-CONTROL\niptables -w 10 -t raw -F FELIS-CONTROL\n/usr/local/bin/k3s kubectl --kubeconfig /etc/rancher/k3s/k3s.yaml get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{\" \"}{.spec.serviceAccountName}{\" \"}{.status.podIP}{\"\\n\"}{end}' | while read -r ns sa ip; do\ncase \"$ns/$sa\" in " + shellQuote(controlNS+"/felis-api") + "|" + shellQuote(controlNS+"/felis-operator") + "|" + shellQuote(minecraftNS+"/felis-reaper") + "|kube-system/*) ;; *) continue;; esac\n[[ $ip =~ ^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$ ]] || continue\niptables -w 10 -A FELIS-CONTROL -s \"$ip/32\" -p tcp --dport 6443 -j ACCEPT\niptables -w 10 -t raw -A FELIS-CONTROL -s \"$ip/32\" -p tcp -m multiport --dports 443,6443 -j ACCEPT\niptables -w 10 -t raw -A FELIS-CONTROL -s \"$ip/32\" -p udp --dport 53 -j ACCEPT\niptables -w 10 -A FELIS-CONTROL -s \"$ip/32\" -p udp --dport 53 -j ACCEPT\ndone\n"
|
|
}
|