mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-08 16:00:30 +00:00
Replace database rule synchronization with native rule operations and file-based backup, import, and recovery. Update firewall management views, descriptions, whitelist handling, and translations. Include terminal reconnect fixes and xpack build tags for Darwin build targets. Remove temporary regression test files before committing.
269 lines
8.0 KiB
Go
269 lines
8.0 KiB
Go
package nftables_helper
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/1Panel-dev/1Panel/agent/global"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
|
"net/netip"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const requiredPortComment = "1Panel Port Whitelist"
|
|
|
|
func Cleanup() error {
|
|
commands := make([][]string, 0, 2)
|
|
for _, family := range []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6} {
|
|
tableFamily := TableFamily(family)
|
|
_, exists, err := ReadTable(run, tableFamily, TableName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !exists {
|
|
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 nil
|
|
}
|
|
|
|
func Operate(operation firewall.BaseOperation, requiredPorts []firewall.PortWhitelist) error {
|
|
switch operation {
|
|
case firewall.BaseOperationInit, firewall.BaseOperationBind:
|
|
return enableBase(true, requiredPorts)
|
|
case firewall.BaseOperationBindWithoutInit:
|
|
return enableBase(false, requiredPorts)
|
|
case firewall.BaseOperationUnbind:
|
|
return Unbind()
|
|
default:
|
|
return fmt.Errorf("unsupported nftables base operation %q", operation)
|
|
}
|
|
}
|
|
|
|
func enableBase(prepare bool, requiredPorts []firewall.PortWhitelist) error {
|
|
if prepare {
|
|
if err := ensureBaseChains(); err != nil {
|
|
return err
|
|
}
|
|
if err := initPreRules(requiredPorts); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := Bind(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ensureBaseChains(families ...filter.Family) error {
|
|
if len(families) == 0 {
|
|
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
|
|
}
|
|
commands := make([][]string, 0, 10)
|
|
for _, family := range families {
|
|
tableFamily := TableFamily(family)
|
|
output, tableExists, err := ReadTable(run, tableFamily, TableName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
chains := ParseTableChains(output)
|
|
if !tableExists {
|
|
commands = append(commands, []string{"add", "table", tableFamily, TableName})
|
|
}
|
|
if _, exists := chains[InputChain]; !exists {
|
|
commands = append(commands, []string{
|
|
"add", "chain", tableFamily, TableName, InputChain,
|
|
"{", "type", "filter", "hook", "input", "priority", "0", ";", "policy", "accept", ";", "}",
|
|
})
|
|
}
|
|
for _, nativeChain := range BasicChains() {
|
|
if _, exists := chains[nativeChain]; exists {
|
|
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, rule firewall.SystemPort) []string {
|
|
command := []string{
|
|
"insert", "rule", tableFamily, TableName, BasicBeforeChain,
|
|
}
|
|
if rule.SourceAddress != "" {
|
|
command = append(command, tableFamily, "saddr", rule.SourceAddress)
|
|
}
|
|
return append(command, "meta", "l4proto", rule.Protocol, rule.Protocol, "dport", rule.Port,
|
|
"accept", "comment", `"`+requiredPortComment+`"`)
|
|
}
|
|
|
|
func initPreRules(requiredPorts []firewall.PortWhitelist, families ...filter.Family) error {
|
|
if len(families) == 0 {
|
|
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
|
|
}
|
|
ports, err := firewall.NormalizeRequiredPorts(requiredPorts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rules := firewall.ExpandPortWhitelist(ports)
|
|
var commands [][]string
|
|
for _, family := range families {
|
|
tableFamily := TableFamily(family)
|
|
output, _, err := ReadTable(run, tableFamily, TableName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
chains := ParseTableChains(output)
|
|
existing := make(map[string]map[string]bool)
|
|
for _, chain := range []string{BasicBeforeChain, BasicAfterChain} {
|
|
existing[chain] = make(map[string]bool)
|
|
for _, line := range strings.Split(chains[chain], "\n") {
|
|
existing[chain][canonicalRequiredPortRule(line)] = true
|
|
}
|
|
}
|
|
candidates := [][]string{
|
|
{"add", "rule", tableFamily, TableName, BasicBeforeChain, "iifname", `"lo"`, "accept", "comment", `"Loopback Whitelist"`},
|
|
{"add", "rule", tableFamily, TableName, BasicBeforeChain, "ct", "state", "{", "established,related", "}", "accept", "comment", `"ESTABLISHED Whitelist"`},
|
|
{"add", "rule", tableFamily, TableName, BasicAfterChain, "meta", "l4proto", "tcp", "drop"},
|
|
{"add", "rule", tableFamily, TableName, BasicAfterChain, "meta", "l4proto", "udp", "drop"},
|
|
}
|
|
for _, rule := range rules {
|
|
if rule.Family == string(family) {
|
|
candidates = append(candidates, requiredPortCommand(tableFamily, rule))
|
|
}
|
|
}
|
|
for _, command := range candidates {
|
|
chain := command[4]
|
|
expression := strings.Join(command[5:], " ")
|
|
key := canonicalRequiredPortRule(expression)
|
|
if !existing[chain][key] {
|
|
existing[chain][key] = true
|
|
commands = append(commands, command)
|
|
}
|
|
}
|
|
}
|
|
return runBatch(commands...)
|
|
}
|
|
|
|
func canonicalRequiredPortRule(line string) string {
|
|
line, _, _ = strings.Cut(line, " comment ")
|
|
line, _, _ = strings.Cut(line, " # handle ")
|
|
for _, protocol := range []string{"tcp", "udp"} {
|
|
line = strings.ReplaceAll(line, "meta l4proto "+protocol+" "+protocol+" ", protocol+" ")
|
|
}
|
|
line = strings.NewReplacer("{", "", "}", "", ", ", ",", " ,", ",").Replace(line)
|
|
fields := strings.Fields(line)
|
|
for index, field := range fields {
|
|
if index >= 2 && fields[index-2] == "ct" && fields[index-1] == "state" {
|
|
states := strings.Split(field, ",")
|
|
for i, state := range states {
|
|
value, err := strconv.ParseUint(state, 0, 64)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
switch value {
|
|
case 1:
|
|
states[i] = "invalid"
|
|
case 2:
|
|
states[i] = "established"
|
|
case 4:
|
|
states[i] = "related"
|
|
case 8:
|
|
states[i] = "new"
|
|
case 64:
|
|
states[i] = "untracked"
|
|
}
|
|
}
|
|
slices.Sort(states)
|
|
fields[index] = strings.Join(states, ",")
|
|
}
|
|
if prefix, err := netip.ParsePrefix(field); err == nil {
|
|
prefix = prefix.Masked()
|
|
fields[index] = prefix.String()
|
|
if prefix.Bits() == prefix.Addr().BitLen() {
|
|
fields[index] = prefix.Addr().String()
|
|
}
|
|
}
|
|
}
|
|
return strings.Join(fields, " ")
|
|
}
|
|
|
|
func Bind(families ...filter.Family) error {
|
|
if len(families) == 0 {
|
|
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
|
|
}
|
|
for _, family := range families {
|
|
tableFamily := TableFamily(family)
|
|
initialized, _, err := LoadFamilyInitStatus(family, "base")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !initialized {
|
|
return fmt.Errorf("1Panel nftables %s chains are not initialized", tableFamily)
|
|
}
|
|
}
|
|
commands := make([][]string, 0, 8)
|
|
for _, family := range families {
|
|
tableFamily := TableFamily(family)
|
|
commands = append(commands, []string{"add", "table", tableFamily, TableName})
|
|
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(families...)
|
|
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(families ...filter.Family) error {
|
|
if len(families) == 0 {
|
|
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
|
|
}
|
|
commands := make([][]string, 0, 2)
|
|
for _, family := range families {
|
|
commands = append(commands, []string{"flush", "chain", TableFamily(family), TableName, InputChain})
|
|
}
|
|
return runBatch(commands...)
|
|
}
|
|
|
|
func OperateFamily(family filter.Family, initialize bool, ports []firewall.PortWhitelist) error {
|
|
if family != filter.FamilyIPv4 && family != filter.FamilyIPv6 {
|
|
return fmt.Errorf("unsupported nftables family %q", family)
|
|
}
|
|
if initialize {
|
|
if err := ensureBaseChains(family); err != nil {
|
|
return err
|
|
}
|
|
if err := initPreRules(ports, family); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return Bind(family)
|
|
}
|