Files

165 lines
4.6 KiB
Go

package docker_guard
import (
"fmt"
"net/netip"
"sort"
"strconv"
"strings"
"github.com/mattn/go-shellwords"
)
type observedPolicy struct {
policy Policy
dropAll bool
droppedSource []string
allowedSource []string
}
func parseDockerGuardPolicies(output, family string) ([]Policy, error) {
groups := make(map[string]*observedPolicy)
order := make([]string, 0)
for _, line := range strings.Split(output, "\n") {
if !strings.Contains(line, "1panel-docker:") {
continue
}
tokens, err := shellwords.Parse(strings.TrimSpace(line))
if err != nil {
return nil, fmt.Errorf("parse Docker guard rule: %w", err)
}
fragment, source, action, err := parseDockerGuardRuleTokens(tokens, family)
if err != nil {
return nil, err
}
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 == "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 nil, fmt.Errorf("unsupported Docker guard rule action %q", action)
}
}
policies := make([]Policy, 0, len(order))
for _, key := range order {
group := groups[key]
switch {
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 nil, fmt.Errorf("Docker guard policy %s has no effective rules", group.policy.UUID)
}
policies = append(policies, group.policy)
}
return policies, nil
}
func parseDockerGuardRuleTokens(tokens []string, family string) (Policy, string, string, error) {
policy := Policy{Family: family, HostIP: wildcardHost(family)}
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 "--ctorigdstport":
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":
if nextPolicyToken(tokens, index) == "saddr" {
source = nextPolicyToken(tokens, index+1)
}
case "drop", "return":
action = tokens[index]
}
}
if policy.UUID == "" || policy.Protocol == "" || policy.HostPort == 0 || action == "" {
return Policy{}, "", "", fmt.Errorf("incomplete 1Panel Docker guard rule")
}
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 wildcardHost(family string) string {
if family == FamilyIPv6 {
return "::"
}
return "0.0.0.0"
}
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
}