Files
1Panel/agent/utils/firewall/port_whitelist.go
T
ssongliu e119239d3d refactor: manage firewall rules through runtime state and file backups (#13998)
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.
2026-10-08 18:19:30 +08:00

325 lines
9.3 KiB
Go

package firewall
import (
"fmt"
"net/netip"
"sort"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
)
type PortWhitelist = filter.PortWhitelist
const (
PortWhitelistTypePanel = "panel"
PortWhitelistTypeSSH = "ssh"
)
func ValidatePortWhitelist(rules []PortWhitelist) ([]PortWhitelist, error) {
if rules == nil {
return nil, fmt.Errorf("firewall port whitelist must be an array")
}
result := make([]PortWhitelist, 0, len(rules))
seen := make(map[string]bool, len(rules))
for _, rule := range rules {
rule.Type = strings.ToLower(strings.TrimSpace(rule.Type))
rule.Protocol = strings.ToLower(strings.TrimSpace(rule.Protocol))
if rule.Type != "" && rule.Protocol == "" {
rule.Protocol = "tcp"
}
if rule.Protocol != "tcp" && rule.Protocol != "udp" {
return nil, fmt.Errorf("invalid firewall port whitelist protocol: %s", rule.Protocol)
}
if rule.Type != "" {
if rule.Type != PortWhitelistTypePanel && rule.Type != PortWhitelistTypeSSH {
return nil, fmt.Errorf("invalid firewall port whitelist type: %s", rule.Type)
}
port, err := parseWhitelistPort(rule.Port)
if err != nil {
return nil, err
}
rule.Port = strconv.Itoa(port)
} else {
var err error
rule.Port, err = normalizeWhitelistPort(rule.Port)
if err != nil {
return nil, err
}
}
if len(rule.Sources) == 0 {
return nil, fmt.Errorf("firewall port whitelist requires at least one source")
}
var err error
rule.Sources, err = NormalizeWhitelistSources("", rule.Sources)
if err != nil {
return nil, err
}
rule.Family = ""
key := rule.Type + "/" + rule.Protocol + "/" + rule.Port
if rule.Type != "" {
key = rule.Type + "/" + rule.Protocol
}
if seen[key] {
return nil, fmt.Errorf("duplicate firewall port whitelist: %s", key)
}
seen[key] = true
result = append(result, rule)
}
return result, nil
}
func normalizeWhitelistPort(value string) (string, error) {
value = strings.TrimSpace(value)
separator := ""
if strings.Contains(value, "-") {
separator = "-"
} else if strings.Contains(value, ":") {
separator = ":"
}
if separator == "" {
port, err := parseWhitelistPort(value)
if err != nil {
return "", err
}
return strconv.Itoa(port), nil
}
parts := strings.Split(value, separator)
if len(parts) != 2 {
return "", fmt.Errorf("invalid firewall port whitelist range: %s", value)
}
start, err := parseWhitelistPort(parts[0])
if err != nil {
return "", err
}
end, err := parseWhitelistPort(parts[1])
if err != nil || start > end {
return "", fmt.Errorf("invalid firewall port whitelist range: %s", value)
}
if start == end {
return strconv.Itoa(start), nil
}
return fmt.Sprintf("%d-%d", start, end), nil
}
func parseWhitelistPort(value string) (int, error) {
port, err := strconv.Atoi(strings.TrimSpace(value))
if err != nil || port < 1 || port > 65535 {
return 0, fmt.Errorf("invalid firewall port whitelist: %s", value)
}
return port, nil
}
func NormalizePortWhitelist(items []PortWhitelist) []PortWhitelist {
ports := make([]PortWhitelist, 0, len(items))
for _, item := range items {
if item.Port == "" {
continue
}
baseKey := strings.Join([]string{item.Port, item.Protocol, strings.Join(item.Sources, ",")}, "/")
duplicate := false
for _, current := range ports {
currentBaseKey := strings.Join([]string{current.Port, current.Protocol, strings.Join(current.Sources, ",")}, "/")
if currentBaseKey == baseKey && (current.Family == "" || current.Family == item.Family) {
duplicate = true
break
}
}
if duplicate {
continue
}
if item.Family == "" {
filtered := ports[:0]
for _, current := range ports {
currentBaseKey := strings.Join([]string{current.Port, current.Protocol, strings.Join(current.Sources, ",")}, "/")
if currentBaseKey != baseKey {
filtered = append(filtered, current)
}
}
ports = filtered
}
ports = append(ports, item)
}
return ports
}
func NormalizeWhitelistSources(family string, sources []string) ([]string, error) {
result := make([]string, 0, len(sources))
seen := make(map[string]struct{}, len(sources))
for _, source := range sources {
source = strings.TrimSpace(source)
prefix, err := netip.ParsePrefix(source)
if err != nil {
address, err := netip.ParseAddr(source)
if err != nil {
return nil, err
}
prefix = netip.PrefixFrom(address, address.BitLen())
}
sourceFamily := family
if sourceFamily == "" {
sourceFamily = constant.FirewallFamilyIPv6
if prefix.Addr().Unmap().Is4() {
sourceFamily = constant.FirewallFamilyIPv4
}
}
rule, err := filter.NormalizeRule(RuleForSystemPort(filter.ProviderIptables, SystemPort{
Family: sourceFamily, Port: "1", Protocol: "tcp", SourceAddress: source,
}))
if err != nil {
return nil, err
}
if rule.SourceAddress == "" {
rule.SourceAddress = "0.0.0.0/0"
if sourceFamily == constant.FirewallFamilyIPv6 {
rule.SourceAddress = "::/0"
}
}
if _, exists := seen[rule.SourceAddress]; !exists {
seen[rule.SourceAddress] = struct{}{}
result = append(result, rule.SourceAddress)
}
}
return result, nil
}
type SystemPort struct {
Family string
Port string
Protocol string
SourceAddress string
}
func ExpandPortWhitelist(ports []PortWhitelist) []SystemPort {
result := make([]SystemPort, 0, len(ports))
for _, port := range ports {
sources := port.Sources
if len(sources) == 0 {
sources = []string{"0.0.0.0/0", "::/0"}
}
for _, source := range sources {
family := constant.FirewallFamilyIPv4
if strings.Contains(source, ":") {
family = constant.FirewallFamilyIPv6
}
if port.Family != "" && port.Family != family {
continue
}
if source == "0.0.0.0/0" || source == "::/0" {
source = ""
}
result = append(result, SystemPort{Family: family, Port: port.Port, Protocol: port.Protocol, SourceAddress: source})
}
}
return result
}
func RuleForSystemPort(provider filter.Provider, port SystemPort) filter.FirewallRule {
scope := filter.Scope{Provider: provider, Direction: filter.DirectionInput}
family := filter.Family(strings.ToLower(strings.TrimSpace(port.Family)))
switch provider {
case filter.ProviderIptables, filter.ProviderNftables:
if family != filter.FamilyIPv6 {
family = filter.FamilyIPv4
}
scope.Family, scope.Table = family, "filter"
case filter.ProviderFirewalld:
if family != filter.FamilyIPv4 && family != filter.FamilyIPv6 {
family = filter.FamilyInet
}
scope.Family, scope.Zone = family, filter.FirewalldInputZone
case filter.ProviderUFW:
if family != filter.FamilyIPv6 {
family = filter.FamilyIPv4
}
scope.Family = family
}
return filter.FirewallRule{
Scope: scope, Protocol: port.Protocol, DestinationPort: port.Port,
SourceAddress: port.SourceAddress, Action: filter.ActionAccept,
}
}
func NormalizeSystemPorts(ports []SystemPort) (map[string]SystemPort, error) {
result := make(map[string]SystemPort, len(ports))
for _, port := range ports {
normalized, err := filter.NormalizeRule(RuleForSystemPort(filter.ProviderIptables, port))
if err != nil {
return nil, err
}
family := strings.ToLower(strings.TrimSpace(port.Family))
if family != "" {
family = string(normalized.Scope.Family)
}
item := SystemPort{
Family: family, Port: normalized.DestinationPort,
Protocol: normalized.Protocol, SourceAddress: normalized.SourceAddress,
}
result[SystemPortKey(item)] = item
}
return result, nil
}
func SystemPortKey(port SystemPort) string {
key := strings.ToLower(strings.TrimSpace(port.Protocol)) + "/" + strings.TrimSpace(port.Port)
if family := strings.ToLower(strings.TrimSpace(port.Family)); family != "" {
key = family + "/" + key
}
if source := strings.TrimSpace(port.SourceAddress); source != "" {
key += "/" + source
}
return key
}
func SortedSystemPortKeys(ports map[string]SystemPort) []string {
keys := make([]string, 0, len(ports))
for key := range ports {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}
func NormalizeRequiredPorts(ports []PortWhitelist) ([]PortWhitelist, error) {
result := make([]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)
if len(port.Sources) > 0 {
port.Sources, err = NormalizeWhitelistSources(port.Family, port.Sources)
if err != nil {
return nil, err
}
}
result = append(result, port)
}
return NormalizePortWhitelist(result), nil
}
func RequiredPortWhitelist(entries []PortWhitelist) ([]PortWhitelist, error) {
result := make([]PortWhitelist, 0, len(entries))
for _, entry := range entries {
if entry.Type == "" {
continue
}
if entry.Port == "" {
return nil, fmt.Errorf("firewall whitelist %s has no stored port", entry.Type)
}
protocol := strings.ToLower(strings.TrimSpace(entry.Protocol))
if protocol == "" {
protocol = "tcp"
}
result = append(result, PortWhitelist{Family: entry.Family, Port: entry.Port, Protocol: protocol, Sources: entry.Sources})
}
return NormalizeRequiredPorts(result)
}