mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-12 16:00:33 +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.
349 lines
11 KiB
Go
349 lines
11 KiB
Go
package docker_guard
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/google/uuid"
|
|
"net/netip"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/mattn/go-shellwords"
|
|
)
|
|
|
|
type observedPolicy struct {
|
|
policy Policy
|
|
orders []int64
|
|
dropAll bool
|
|
droppedSource []string
|
|
allowedSource []string
|
|
acceptedSource []string
|
|
acceptAll bool
|
|
}
|
|
|
|
func ConvertPolicyBackend(policy Policy, sourceBackend, targetBackend string) (Policy, error) {
|
|
compiled := make(map[string][][]string, 2)
|
|
for _, backend := range []string{sourceBackend, targetBackend} {
|
|
switch backend {
|
|
case "iptables":
|
|
compiled[backend] = compilePolicy(policy)
|
|
case "nftables":
|
|
compiled[backend] = compileNftPolicy(policy)
|
|
for i := range compiled[backend] {
|
|
compiled[backend][i] = compiled[backend][i][5:]
|
|
}
|
|
default:
|
|
return Policy{}, fmt.Errorf("unsupported Docker firewall backend %q", backend)
|
|
}
|
|
}
|
|
if len(policy.NativeRules) == 0 {
|
|
return policy, nil
|
|
}
|
|
sourceRules, targetRules := compiled[sourceBackend], compiled[targetBackend]
|
|
if len(policy.NativeRules) != len(sourceRules) {
|
|
return Policy{}, fmt.Errorf("Docker policy %s contains native rules that cannot be converted from %s to %s", policy.UUID, sourceBackend, targetBackend)
|
|
}
|
|
byRule := make(map[string]int, len(sourceRules))
|
|
for index, rule := range sourceRules {
|
|
byRule[dockerPolicyRuleKey(rule, sourceBackend)] = index
|
|
}
|
|
converted := make([]NativeRule, 0, len(policy.NativeRules))
|
|
for _, native := range policy.NativeRules {
|
|
key := dockerPolicyRuleKey(native.Tokens, sourceBackend)
|
|
index, exists := byRule[key]
|
|
if !exists || native.Family != policy.Family {
|
|
return Policy{}, fmt.Errorf("Docker policy %s contains native conditions that cannot be converted from %s to %s", policy.UUID, sourceBackend, targetBackend)
|
|
}
|
|
delete(byRule, key)
|
|
tokens := append([]string(nil), targetRules[index]...)
|
|
for i, token := range tokens {
|
|
if unquoted, err := strconv.Unquote(token); err == nil {
|
|
tokens[i] = unquoted
|
|
}
|
|
}
|
|
converted = append(converted, NativeRule{Family: native.Family, Order: native.Order, Tokens: tokens})
|
|
}
|
|
policy.NativeRules = converted
|
|
return policy, nil
|
|
}
|
|
|
|
func dockerPolicyRuleKey(tokens []string, backend string) string {
|
|
tokens = nativeRuleTokens(tokens)
|
|
if backend == "iptables" {
|
|
if len(tokens)%2 != 0 {
|
|
return ""
|
|
}
|
|
protocol := ""
|
|
if index := slices.Index(tokens, "-p"); index >= 0 && index+1 < len(tokens) {
|
|
protocol = tokens[index+1]
|
|
}
|
|
parts := make([]string, 0, len(tokens)/2)
|
|
for i := 0; i < len(tokens); i += 2 {
|
|
option, value := tokens[i], tokens[i+1]
|
|
if option == "-m" && value == protocol && (value == "tcp" || value == "udp") {
|
|
continue
|
|
}
|
|
if option == "--ctorigdst" {
|
|
value = normalizeObservedHost(value)
|
|
}
|
|
if option == "-s" {
|
|
if address, err := netip.ParseAddr(value); err == nil {
|
|
value = netip.PrefixFrom(address, address.BitLen()).String()
|
|
} else if prefix, err := netip.ParsePrefix(value); err == nil {
|
|
value = prefix.Masked().String()
|
|
}
|
|
}
|
|
parts = append(parts, option+"\x00"+value)
|
|
}
|
|
sort.Strings(parts)
|
|
return strings.Join(parts, "\x01")
|
|
}
|
|
parts := make([]string, 0, len(tokens))
|
|
for i, token := range tokens {
|
|
if token == "counter" {
|
|
continue
|
|
}
|
|
if unquoted, err := strconv.Unquote(token); err == nil {
|
|
token = unquoted
|
|
}
|
|
if i > 0 && tokens[i-1] == "daddr" {
|
|
token = normalizeObservedHost(token)
|
|
}
|
|
if i > 0 && tokens[i-1] == "saddr" {
|
|
if address, err := netip.ParseAddr(token); err == nil {
|
|
token = netip.PrefixFrom(address, address.BitLen()).String()
|
|
} else if prefix, err := netip.ParsePrefix(token); err == nil {
|
|
token = prefix.Masked().String()
|
|
}
|
|
}
|
|
parts = append(parts, token)
|
|
}
|
|
return strings.Join(parts, "\x00")
|
|
}
|
|
|
|
func parseDockerGuardPolicies(output, family string) (PolicyInventory, error) {
|
|
groups := make(map[string]*observedPolicy)
|
|
order := make([]string, 0)
|
|
sequence := int64(0)
|
|
for _, line := range strings.Split(output, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
tokens, err := shellwords.Parse(line)
|
|
if err != nil {
|
|
return PolicyInventory{}, fmt.Errorf("parse Docker guard rule: %w", err)
|
|
}
|
|
sequence++
|
|
fragment, source, action, err := parseDockerGuardRuleTokens(tokens, family)
|
|
if err != nil {
|
|
return PolicyInventory{}, err
|
|
}
|
|
if action == "" || (fragment.HostPort == 0 && action == "return") {
|
|
continue
|
|
}
|
|
key := strings.Join([]string{fragment.UUID, fragment.Family, fragment.HostIP, strconv.Itoa(int(fragment.HostPort)), fragment.Protocol}, "|")
|
|
group, exists := groups[key]
|
|
if !exists {
|
|
group = &observedPolicy{policy: fragment}
|
|
groups[key] = group
|
|
order = append(order, key)
|
|
}
|
|
switch {
|
|
case action == "accept" && source != "":
|
|
group.acceptedSource = append(group.acceptedSource, source)
|
|
case action == "accept":
|
|
group.acceptAll = true
|
|
case action == "return" && source != "":
|
|
group.allowedSource = append(group.allowedSource, source)
|
|
case action == "drop" && source != "":
|
|
group.droppedSource = append(group.droppedSource, source)
|
|
case action == "drop":
|
|
group.dropAll = true
|
|
default:
|
|
return PolicyInventory{}, fmt.Errorf("unsupported Docker guard rule action %q", action)
|
|
}
|
|
group.policy.NativeRules = append(group.policy.NativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
|
|
group.orders = append(group.orders, sequence)
|
|
}
|
|
inventory := PolicyInventory{Policies: make([]Policy, 0, len(order)), RuleOrders: make(map[string][]int64)}
|
|
for _, key := range order {
|
|
group := groups[key]
|
|
switch {
|
|
case group.acceptAll:
|
|
group.policy.Mode = ModeAcceptAll
|
|
group.policy.Sources = []string{}
|
|
case len(group.acceptedSource) > 0:
|
|
group.policy.Mode = ModeAcceptSources
|
|
group.policy.Sources = uniqueSortedStrings(group.acceptedSource)
|
|
case len(group.allowedSource) > 0:
|
|
group.policy.Mode = ModeAllow
|
|
group.policy.Sources = uniqueSortedStrings(group.allowedSource)
|
|
case len(group.droppedSource) > 0:
|
|
group.policy.Mode = ModeSources
|
|
group.policy.Sources = uniqueSortedStrings(group.droppedSource)
|
|
case group.dropAll:
|
|
group.policy.Mode = ModeAll
|
|
default:
|
|
return PolicyInventory{}, fmt.Errorf("Docker guard policy %s has no effective rules", group.policy.UUID)
|
|
}
|
|
if _, err := uuid.Parse(group.policy.UUID); err != nil {
|
|
rules := make([][]string, 0, len(group.policy.NativeRules))
|
|
for _, rule := range group.policy.NativeRules {
|
|
rules = append(rules, rule.Tokens)
|
|
}
|
|
fingerprint, _ := json.Marshal(rules)
|
|
group.policy.UUID = uuid.NewSHA1(uuid.NameSpaceOID, append([]byte(family+"\x00"), fingerprint...)).String()
|
|
}
|
|
inventory.Policies = append(inventory.Policies, group.policy)
|
|
inventory.RuleOrders[group.policy.Family+"\x00"+group.policy.UUID] = append([]int64(nil), group.orders...)
|
|
}
|
|
return inventory, nil
|
|
}
|
|
|
|
func nativeRuleTokens(tokens []string) []string {
|
|
result := make([]string, 0, len(tokens))
|
|
for index, token := range tokens {
|
|
if token == "#" {
|
|
tokens = tokens[:index]
|
|
break
|
|
}
|
|
}
|
|
if len(tokens) >= 2 && tokens[len(tokens)-2] == "handle" {
|
|
tokens = tokens[:len(tokens)-2]
|
|
}
|
|
for index := 0; index < len(tokens); index++ {
|
|
result = append(result, tokens[index])
|
|
if tokens[index] == "counter" && index+4 < len(tokens) && tokens[index+1] == "packets" && tokens[index+3] == "bytes" {
|
|
index += 4
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string, string, error) {
|
|
policy := Policy{Family: family, HostIP: "0.0.0.0"}
|
|
if family == FamilyIPv6 {
|
|
policy.HostIP = "::"
|
|
}
|
|
source, action := "", ""
|
|
for index := 0; index < len(tokens); index++ {
|
|
switch tokens[index] {
|
|
case "-p":
|
|
policy.Protocol = nextPolicyToken(tokens, index)
|
|
case "--ctorigdst":
|
|
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index))
|
|
case "-d":
|
|
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index))
|
|
case "--ctorigdstport":
|
|
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index))
|
|
case "--dport":
|
|
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index))
|
|
case "-s":
|
|
source = nextPolicyToken(tokens, index)
|
|
case "--comment", "comment":
|
|
marker := nextPolicyToken(tokens, index)
|
|
if strings.HasPrefix(marker, "1panel-docker:") {
|
|
policy.UUID = strings.TrimPrefix(marker, "1panel-docker:")
|
|
}
|
|
case "-j":
|
|
action = strings.ToLower(nextPolicyToken(tokens, index))
|
|
case "meta":
|
|
if nextPolicyToken(tokens, index) == "l4proto" {
|
|
policy.Protocol = nextPolicyToken(tokens, index+1)
|
|
}
|
|
case "ct":
|
|
if nextPolicyToken(tokens, index) != "original" {
|
|
continue
|
|
}
|
|
switch nextPolicyToken(tokens, index+1) {
|
|
case "proto-dst":
|
|
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+2))
|
|
case "ip", "ip6":
|
|
if nextPolicyToken(tokens, index+2) == "daddr" {
|
|
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index+3))
|
|
}
|
|
}
|
|
case "ip", "ip6":
|
|
switch nextPolicyToken(tokens, index) {
|
|
case "saddr":
|
|
source = nextPolicyToken(tokens, index+1)
|
|
case "daddr":
|
|
policy.HostIP = normalizeObservedHost(nextPolicyToken(tokens, index+1))
|
|
}
|
|
case "tcp", "udp":
|
|
if nextPolicyToken(tokens, index) == "dport" {
|
|
policy.Protocol = tokens[index]
|
|
policy.HostPort = parsePolicyPort(nextPolicyToken(tokens, index+1))
|
|
}
|
|
case "accept", "drop", "return":
|
|
if index > 0 && (tokens[index-1] == "comment" || tokens[index-1] == "--comment") {
|
|
continue
|
|
}
|
|
action = tokens[index]
|
|
}
|
|
}
|
|
if action != "accept" && action != "drop" && action != "return" {
|
|
return policy, "", "", nil
|
|
}
|
|
if action == "drop" && (policy.Protocol == "" || policy.HostPort == 0) {
|
|
return Policy{}, "", "", fmt.Errorf("incomplete 1Panel Docker guard rule")
|
|
}
|
|
if action == "accept" && policy.Protocol == "" {
|
|
policy.Protocol = "all"
|
|
}
|
|
return policy, source, action, nil
|
|
}
|
|
|
|
func normalizeObservedHost(value string) string {
|
|
if prefix, err := netip.ParsePrefix(value); err == nil && prefix.Bits() == prefix.Addr().BitLen() {
|
|
return prefix.Addr().String()
|
|
}
|
|
return value
|
|
}
|
|
|
|
func nextPolicyToken(tokens []string, index int) string {
|
|
if index+1 >= len(tokens) {
|
|
return ""
|
|
}
|
|
return tokens[index+1]
|
|
}
|
|
|
|
func parsePolicyPort(value string) uint16 {
|
|
port, err := strconv.ParseUint(value, 10, 16)
|
|
if err != nil || port == 0 {
|
|
return 0
|
|
}
|
|
return uint16(port)
|
|
}
|
|
|
|
func uniqueSortedStrings(values []string) []string {
|
|
seen := make(map[string]struct{}, len(values))
|
|
result := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
if _, exists := seen[value]; exists {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
result = append(result, value)
|
|
}
|
|
sort.Strings(result)
|
|
return result
|
|
}
|
|
|
|
type orderedPolicyRule struct {
|
|
order int64
|
|
rule []string
|
|
}
|
|
|
|
func sortPolicyRules(segments []orderedPolicyRule) [][]string {
|
|
sort.SliceStable(segments, func(i, j int) bool { return segments[i].order < segments[j].order })
|
|
rules := make([][]string, 0, len(segments))
|
|
for _, segment := range segments {
|
|
rules = append(rules, segment.rule)
|
|
}
|
|
return rules
|
|
}
|