mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-24 00:00:52 +00:00
396 lines
12 KiB
Go
396 lines
12 KiB
Go
package docker_guard
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/netip"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/mattn/go-shellwords"
|
|
)
|
|
|
|
var ErrInvalidPolicy = errors.New("invalid Docker port guard request")
|
|
|
|
func NormalizePolicy(policy Policy) (Policy, error) {
|
|
policy.Family = strings.ToLower(strings.TrimSpace(policy.Family))
|
|
policy.HostIP = strings.TrimSpace(policy.HostIP)
|
|
policy.Protocol = strings.ToLower(strings.TrimSpace(policy.Protocol))
|
|
policy.Mode = strings.ToLower(strings.TrimSpace(policy.Mode))
|
|
if policy.HostPort == 0 ||
|
|
(policy.Protocol != "tcp" && policy.Protocol != "udp") ||
|
|
(policy.Family != FamilyIPv4 && policy.Family != FamilyIPv6) ||
|
|
(policy.Mode != ModeAll && policy.Mode != ModeSources && policy.Mode != ModeAllow) {
|
|
return Policy{}, fmt.Errorf("%w: invalid policy fields", ErrInvalidPolicy)
|
|
}
|
|
address, err := netip.ParseAddr(policy.HostIP)
|
|
if err != nil || (policy.Family == FamilyIPv4) != address.Is4() {
|
|
return Policy{}, fmt.Errorf("%w: host IP does not match address family", ErrInvalidPolicy)
|
|
}
|
|
normalizedSources := make([]string, 0, len(policy.Sources))
|
|
seen := make(map[string]struct{}, len(policy.Sources))
|
|
for _, source := range policy.Sources {
|
|
source = strings.TrimSpace(source)
|
|
if source == "" {
|
|
continue
|
|
}
|
|
prefix, err := netip.ParsePrefix(source)
|
|
if err != nil {
|
|
if sourceAddress, addressErr := netip.ParseAddr(source); addressErr == nil {
|
|
bits := 128
|
|
if sourceAddress.Is4() {
|
|
bits = 32
|
|
}
|
|
prefix = netip.PrefixFrom(sourceAddress, bits)
|
|
} else {
|
|
return Policy{}, fmt.Errorf("%w: invalid source address %q", ErrInvalidPolicy, source)
|
|
}
|
|
}
|
|
if (policy.Family == FamilyIPv4) != prefix.Addr().Is4() {
|
|
return Policy{}, fmt.Errorf("%w: source %q does not match address family", ErrInvalidPolicy, source)
|
|
}
|
|
canonical := prefix.Masked().String()
|
|
if _, exists := seen[canonical]; !exists {
|
|
seen[canonical] = struct{}{}
|
|
normalizedSources = append(normalizedSources, canonical)
|
|
}
|
|
}
|
|
if policy.Mode != ModeAll && len(normalizedSources) == 0 {
|
|
return Policy{}, fmt.Errorf("%w: source-based modes require at least one source", ErrInvalidPolicy)
|
|
}
|
|
if policy.Mode == ModeAll {
|
|
normalizedSources = []string{}
|
|
}
|
|
sort.Strings(normalizedSources)
|
|
policy.Sources = normalizedSources
|
|
return policy, nil
|
|
}
|
|
|
|
func NormalizePolicyUUIDs(values []string) ([]string, error) {
|
|
uuids := make([]string, 0, len(values))
|
|
seen := make(map[string]struct{}, len(values))
|
|
for _, policyUUID := range values {
|
|
policyUUID = strings.TrimSpace(policyUUID)
|
|
if policyUUID == "" {
|
|
return nil, fmt.Errorf("%w: policy UUID cannot be empty", ErrInvalidPolicy)
|
|
}
|
|
if _, exists := seen[policyUUID]; exists {
|
|
continue
|
|
}
|
|
seen[policyUUID] = struct{}{}
|
|
uuids = append(uuids, policyUUID)
|
|
}
|
|
if len(uuids) == 0 {
|
|
return nil, fmt.Errorf("%w: policy UUIDs cannot be empty", ErrInvalidPolicy)
|
|
}
|
|
return uuids, nil
|
|
}
|
|
|
|
func PolicySyncKey(policy Policy) string {
|
|
mode := policy.Mode
|
|
if mode == ModeAllow && len(policy.Sources) == 0 {
|
|
mode = ModeAll
|
|
}
|
|
sources := make([]string, 0, len(policy.Sources))
|
|
for _, source := range policy.Sources {
|
|
sources = append(sources, canonicalPolicySource(source))
|
|
}
|
|
sort.Strings(sources)
|
|
return strings.Join([]string{
|
|
policy.UUID, policy.Family, CanonicalHost(policy.HostIP), strconv.Itoa(int(policy.HostPort)),
|
|
policy.Protocol, mode, strings.Join(sources, ","),
|
|
}, "\x00")
|
|
}
|
|
|
|
func canonicalPolicySource(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
if prefix, err := netip.ParsePrefix(value); err == nil {
|
|
return prefix.Masked().String()
|
|
}
|
|
if address, err := netip.ParseAddr(value); err == nil {
|
|
address = address.Unmap()
|
|
return netip.PrefixFrom(address, address.BitLen()).String()
|
|
}
|
|
return value
|
|
}
|
|
|
|
func PolicyStatesEqual(left, right []Policy) bool {
|
|
if len(left) != len(right) {
|
|
return false
|
|
}
|
|
counts := make(map[string]int, len(left))
|
|
for _, policy := range left {
|
|
counts[PolicySyncKey(policy)]++
|
|
}
|
|
for _, policy := range right {
|
|
key := PolicySyncKey(policy)
|
|
if counts[key] == 0 {
|
|
return false
|
|
}
|
|
counts[key]--
|
|
}
|
|
return true
|
|
}
|
|
|
|
func CanonicalHost(value string) string {
|
|
if address, err := netip.ParseAddr(value); err == nil {
|
|
return address.String()
|
|
}
|
|
return value
|
|
}
|
|
|
|
func DecodeSources(value string) []string {
|
|
result := []string{}
|
|
_ = json.Unmarshal([]byte(value), &result)
|
|
return result
|
|
}
|
|
|
|
type observedPolicy struct {
|
|
policy Policy
|
|
sequence int64
|
|
nativeRules []NativeRule
|
|
managedOrders []int64
|
|
dropAll bool
|
|
droppedSource []string
|
|
allowedSource []string
|
|
acceptedSource []string
|
|
acceptAll bool
|
|
}
|
|
|
|
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)
|
|
}
|
|
managed := strings.Contains(line, "1panel-docker:")
|
|
if !managed && !hasAcceptAction(tokens) {
|
|
continue
|
|
}
|
|
sequence++
|
|
fragment, source, action, err := parseDockerGuardRuleTokens(tokens, family)
|
|
if err != nil {
|
|
return PolicyInventory{}, err
|
|
}
|
|
identity := fragment.UUID
|
|
if action == "accept" {
|
|
identity = action
|
|
}
|
|
key := strings.Join([]string{identity, fragment.Family, fragment.HostIP, strconv.Itoa(int(fragment.HostPort)), fragment.Protocol}, "|")
|
|
group, exists := groups[key]
|
|
if !exists {
|
|
group = &observedPolicy{policy: fragment, sequence: sequence}
|
|
groups[key] = group
|
|
order = append(order, key)
|
|
}
|
|
switch {
|
|
case action == "accept" && source != "":
|
|
group.acceptedSource = append(group.acceptedSource, source)
|
|
group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
|
|
case action == "accept":
|
|
group.acceptAll = true
|
|
group.nativeRules = append(group.nativeRules, NativeRule{Family: family, Order: sequence, Tokens: nativeRuleTokens(tokens)})
|
|
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)
|
|
}
|
|
if action != "accept" {
|
|
group.managedOrders = append(group.managedOrders, sequence)
|
|
}
|
|
}
|
|
inventory := PolicyInventory{Policies: make([]Policy, 0, len(order)), ManagedRuleOrders: make(map[string][]int64)}
|
|
for _, key := range order {
|
|
group := groups[key]
|
|
if group.acceptAll || len(group.acceptedSource) > 0 {
|
|
group.policy.Sources = uniqueSortedStrings(group.acceptedSource)
|
|
inventory.ReadOnly = append(inventory.ReadOnly, ReadOnlyPolicy{
|
|
Policy: group.policy, Action: "accept", Sequence: group.sequence, NativeRules: group.nativeRules,
|
|
})
|
|
continue
|
|
}
|
|
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 PolicyInventory{}, fmt.Errorf("Docker guard policy %s has no effective rules", group.policy.UUID)
|
|
}
|
|
inventory.Policies = append(inventory.Policies, group.policy)
|
|
inventory.ManagedRuleOrders[managedOrderKey(group.policy.Family, group.policy.UUID)] = append([]int64(nil), group.managedOrders...)
|
|
}
|
|
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 managedOrderKey(family, policyUUID string) string {
|
|
return family + "\x00" + policyUUID
|
|
}
|
|
|
|
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 "-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 isCommentValue(tokens, index) {
|
|
continue
|
|
}
|
|
action = tokens[index]
|
|
}
|
|
}
|
|
if action == "" || (action != "accept" && (policy.UUID == "" || 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 hasAcceptAction(tokens []string) bool {
|
|
for index, token := range tokens {
|
|
if token == "-j" && strings.EqualFold(nextPolicyToken(tokens, index), "accept") {
|
|
return true
|
|
}
|
|
if strings.EqualFold(token, "accept") && !isCommentValue(tokens, index) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isCommentValue(tokens []string, index int) bool {
|
|
if index == 0 {
|
|
return false
|
|
}
|
|
return tokens[index-1] == "comment" || tokens[index-1] == "--comment"
|
|
}
|
|
|
|
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
|
|
}
|