Files
1Panel/agent/utils/firewall/docker_guard/policy.go
T

358 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 {
if prefix.Bits() == 0 {
continue
}
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":
if source == "" {
source = "0.0.0.0/0"
if family == FamilyIPv6 {
source = "::/0"
}
}
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
}