Files

132 lines
3.8 KiB
Go

package docker_guard
import (
"encoding/json"
"errors"
"fmt"
"net/netip"
"sort"
"strconv"
"strings"
)
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 == ModeSources && len(normalizedSources) == 0 {
return Policy{}, fmt.Errorf("%w: deny_sources requires 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 := append([]string(nil), policy.Sources...)
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 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
}