Files
1Panel/agent/utils/firewall/port_whitelist.go

329 lines
9.4 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 := LegacySystemPortKey(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 LegacySystemPortKey(port SystemPort) string {
return strings.ToLower(strings.TrimSpace(port.Protocol)) + "/" + strings.TrimSpace(port.Port)
}
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)
}