Files

317 lines
10 KiB
Go

package nftables_helper
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
)
const requiredPortComment = "1Panel Port Whitelist"
type Manager struct {
UpdateSetting func(key, value string) error
LoadRequiredPorts func() ([]firewall.PortWhitelist, error)
}
func (m *Manager) Cleanup() error {
commands := make([][]string, 0, 2)
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family)
if _, err := run("list", "table", tableFamily, TableName); err != nil {
continue
}
commands = append(commands, []string{"delete", "table", tableFamily, TableName})
}
if err := runBatch(commands...); err != nil {
return err
}
file := filepath.Join(global.Dir.FirewallDir, RulesFile)
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
return m.updateSetting("IptablesStatus", constant.StatusDisable)
}
func (m *Manager) Operate(operation firewall.BaseOperation) error {
switch operation {
case firewall.BaseOperationInit, firewall.BaseOperationBind:
return m.enableBase(true)
case firewall.BaseOperationBindWithoutInit:
return m.enableBase(false)
case firewall.BaseOperationUnbind:
return m.disableBase()
default:
return fmt.Errorf("unsupported nftables base operation %q", operation)
}
}
func (m *Manager) enableBase(prepare bool) error {
if prepare {
if err := m.ensureBaseChains(); err != nil {
return err
}
if err := m.initPreRules(); err != nil {
return err
}
}
if err := Bind(); err != nil {
return err
}
return m.updateSetting("IptablesStatus", constant.StatusEnable)
}
func (m *Manager) disableBase() error {
if err := Unbind(); err != nil {
return err
}
return m.updateSetting("IptablesStatus", constant.StatusDisable)
}
func (m *Manager) ensureBaseChains() error {
commands := make([][]string, 0, 10)
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family)
tableExists := true
if _, err := run("list", "table", tableFamily, TableName); err != nil {
tableExists = false
commands = append(commands, []string{"add", "table", tableFamily, TableName})
}
if !tableExists {
commands = append(commands, []string{
"add", "chain", tableFamily, TableName, InputChain,
"{", "type", "filter", "hook", "input", "priority", "filter", ";", "policy", "accept", ";", "}",
})
} else if _, err := run("list", "chain", tableFamily, TableName, InputChain); err != nil {
commands = append(commands, []string{
"add", "chain", tableFamily, TableName, InputChain,
"{", "type", "filter", "hook", "input", "priority", "filter", ";", "policy", "accept", ";", "}",
})
}
for _, nativeChain := range BasicChains() {
if tableExists {
if _, err := run("list", "chain", tableFamily, TableName, nativeChain); err == nil {
continue
}
}
commands = append(commands, []string{"add", "chain", tableFamily, TableName, nativeChain})
}
}
if err := runBatch(commands...); err != nil {
return fmt.Errorf("batch create 1Panel nftables base chains: %w", err)
}
return nil
}
func requiredPortCommand(tableFamily string, port firewall.PortWhitelist) []string {
return []string{
"add", "rule", tableFamily, TableName, BasicBeforeChain,
"meta", "l4proto", port.Protocol, port.Protocol, "dport", port.Port,
"accept", "comment", `"` + requiredPortComment + `"`,
}
}
func (m *Manager) initPreRules() error {
ports, err := m.loadRequiredPorts()
if err != nil {
return err
}
ports, err = validateRequiredPorts(ports)
if err != nil {
return err
}
commands := make([][]string, 0, 12+len(ports)*2)
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family)
commands = append(commands,
[]string{"flush", "chain", tableFamily, TableName, BasicBeforeChain},
[]string{"add", "rule", tableFamily, TableName, BasicBeforeChain, "iifname", `"lo"`, "accept", "comment", `"Loopback Whitelist"`},
[]string{"add", "rule", tableFamily, TableName, BasicBeforeChain, "ct", "state", "{", "established,related", "}", "accept", "comment", `"ESTABLISHED Whitelist"`},
)
for _, port := range ports {
commands = append(commands, requiredPortCommand(tableFamily, port))
}
commands = append(commands,
[]string{"flush", "chain", tableFamily, TableName, BasicAfterChain},
[]string{"add", "rule", tableFamily, TableName, BasicAfterChain, "meta", "l4proto", "tcp", "drop"},
[]string{"add", "rule", tableFamily, TableName, BasicAfterChain, "meta", "l4proto", "udp", "drop"},
)
}
return runBatch(commands...)
}
func (m *Manager) SyncRequiredPorts() error {
ports, err := m.loadRequiredPorts()
if err != nil {
return err
}
ports, err = validateRequiredPorts(ports)
if err != nil {
return err
}
commands := make([][]string, 0)
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family)
stdout, err := run("-n", "-a", "list", "chain", tableFamily, TableName, BasicBeforeChain)
if err != nil {
return err
}
existing := requiredPortRules(stdout)
missing, staleHandles := requiredPortChanges(existing, ports)
for _, port := range missing {
commands = append(commands, requiredPortCommand(tableFamily, port))
}
for _, handle := range staleHandles {
commands = append(commands, []string{"delete", "rule", tableFamily, TableName, BasicBeforeChain, "handle", handle})
}
}
if err := runBatch(commands...); err != nil {
return err
}
if err := PersistRuleset(context.Background()); err != nil {
return err
}
return nil
}
func validateRequiredPorts(ports []firewall.PortWhitelist) ([]firewall.PortWhitelist, error) {
result := make([]firewall.PortWhitelist, 0, len(ports))
for _, port := range ports {
port.Protocol = strings.ToLower(strings.TrimSpace(port.Protocol))
if port.Protocol != "tcp" && port.Protocol != "udp" {
return nil, fmt.Errorf("unsupported required firewall port protocol %q", port.Protocol)
}
portNumber, err := strconv.Atoi(strings.TrimSpace(port.Port))
if err != nil || portNumber < 1 || portNumber > 65535 {
return nil, fmt.Errorf("invalid required firewall port %q", port.Port)
}
port.Port = strconv.Itoa(portNumber)
result = append(result, port)
}
return firewall.NormalizePortWhitelist(result), nil
}
type requiredPortRule struct {
Key string
Handle string
}
func requiredPortChanges(existing []requiredPortRule, desiredPorts []firewall.PortWhitelist) ([]firewall.PortWhitelist, []string) {
desired := firewall.PortWhitelistMap(desiredPorts)
existingKeys := make(map[string]struct{}, len(existing))
for _, rule := range existing {
existingKeys[rule.Key] = struct{}{}
}
missing := make([]firewall.PortWhitelist, 0)
for _, port := range desiredPorts {
if _, exists := existingKeys[firewall.PortWhitelistKey(port)]; !exists {
missing = append(missing, port)
}
}
kept := make(map[string]struct{}, len(existing))
staleHandles := make([]string, 0)
for _, rule := range existing {
if _, wanted := desired[rule.Key]; wanted {
if _, alreadyKept := kept[rule.Key]; !alreadyKept {
kept[rule.Key] = struct{}{}
continue
}
}
staleHandles = append(staleHandles, rule.Handle)
}
return missing, staleHandles
}
func requiredPortRules(output string) []requiredPortRule {
rules := make([]requiredPortRule, 0)
marker := `comment "` + requiredPortComment + `"`
for _, line := range strings.Split(output, "\n") {
if !strings.Contains(line, marker) {
continue
}
handleIndex := strings.LastIndex(line, "# handle ")
if handleIndex < 0 {
continue
}
handle := strings.TrimSpace(line[handleIndex+len("# handle "):])
if _, err := strconv.ParseUint(handle, 10, 64); err != nil {
continue
}
fields := strings.Fields(line[:handleIndex])
for index := 0; index+2 < len(fields); index++ {
protocol := fields[index]
if (protocol != "tcp" && protocol != "udp") || fields[index+1] != "dport" {
continue
}
port, err := strconv.Atoi(fields[index+2])
if err != nil || port < 1 || port > 65535 {
break
}
rules = append(rules, requiredPortRule{
Key: firewall.PortWhitelistKey(firewall.PortWhitelist{Protocol: protocol, Port: strconv.Itoa(port)}), Handle: handle,
})
break
}
}
return rules
}
func (m *Manager) updateSetting(key, value string) error {
if m != nil && m.UpdateSetting != nil {
return m.UpdateSetting(key, value)
}
return nil
}
func (m *Manager) loadRequiredPorts() ([]firewall.PortWhitelist, error) {
if m != nil && m.LoadRequiredPorts != nil {
return m.LoadRequiredPorts()
}
return nil, fmt.Errorf("load required firewall ports is not configured")
}
func Bind() error {
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family)
if _, err := run("list", "chain", tableFamily, TableName, InputChain); err != nil {
return fmt.Errorf("1Panel nftables %s input chain is not initialized: %w", tableFamily, err)
}
}
commands := make([][]string, 0, 8)
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
tableFamily := TableFamily(family)
commands = append(commands, []string{"flush", "chain", tableFamily, TableName, InputChain})
for _, chain := range BasicChains() {
commands = append(commands, []string{"add", "rule", tableFamily, TableName, InputChain, "jump", chain})
}
}
if err := runBatch(commands...); err != nil {
cleanupErr := flushInputChains()
return errors.Join(err, cleanupErr)
}
return PersistRuleset(context.Background())
}
func Unbind() error {
if err := flushInputChains(); err != nil {
return err
}
return PersistRuleset(context.Background())
}
func flushInputChains() error {
commands := make([][]string, 0, 2)
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
commands = append(commands, []string{"flush", "chain", TableFamily(family), TableName, InputChain})
}
return runBatch(commands...)
}