mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-26 16:00:55 +00:00
317 lines
10 KiB
Go
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...)
|
|
}
|