mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-27 00:00:14 +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.
325 lines
9.3 KiB
Go
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)
|
|
}
|