mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 16:00:51 +00:00
1068 lines
38 KiB
Go
1068 lines
38 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/netip"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
"github.com/1Panel-dev/1Panel/agent/global"
|
|
agenti18n "github.com/1Panel-dev/1Panel/agent/i18n"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/lifecycle"
|
|
containertypes "github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/system"
|
|
"github.com/docker/docker/client"
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
dockerGuardComposeProjectLabel = "com.docker.compose.project"
|
|
dockerGuardComposeCreatedBy = "createdBy"
|
|
dockerTrafficPathForward = "forward"
|
|
dockerTrafficPathInput = "input"
|
|
dockerTrafficPathUnknown = "unknown"
|
|
dockerManagementContainerGuard = "container_guard"
|
|
dockerManagementHostFirewall = "host_firewall"
|
|
dockerManagementNeedsDiagnosis = "needs_diagnosis"
|
|
dockerReasonNATInspectFailed = "nat_inspect_failed"
|
|
dockerReasonNATChainUnreachable = "nat_chain_unreachable"
|
|
dockerReasonProxyInspectFailed = "proxy_inspect_failed"
|
|
dockerReasonNoMatchingPath = "no_matching_path"
|
|
)
|
|
|
|
type dockerProxyEndpoint struct {
|
|
protocol string
|
|
hostIP string
|
|
hostPort uint16
|
|
}
|
|
|
|
type dockerForwardRules struct {
|
|
output string
|
|
inspected bool
|
|
}
|
|
|
|
type dockerProxyEndpoints struct {
|
|
items []dockerProxyEndpoint
|
|
inspected bool
|
|
}
|
|
|
|
type dockerGuardRuntime = docker_guard.Runtime
|
|
|
|
type DockerPortGuardService struct {
|
|
policies repo.IDockerPortGuardRepo
|
|
runtime dockerGuardRuntime
|
|
runtimeForBackend func(string) dockerGuardRuntime
|
|
client func() (*client.Client, error)
|
|
version func(string) string
|
|
}
|
|
|
|
var (
|
|
dockerPortGuardServiceMu sync.Mutex
|
|
dockerPortGuardSyncMu sync.RWMutex
|
|
dockerPortGuardSyncErr error
|
|
ErrDockerGuardInvalid = docker_guard.ErrInvalidPolicy
|
|
ErrDockerUnavailable = docker.ErrUnavailable
|
|
ErrDockerIptablesChainUnavailable = docker_guard.ErrDockerIptablesChainUnavailable
|
|
ErrDockerNftablesChainUnavailable = docker_guard.ErrDockerNftablesChainUnavailable
|
|
)
|
|
|
|
type IDockerPortGuardService interface {
|
|
LoadOverview(context.Context) (dto.DockerPortGuardList, error)
|
|
LoadPublishedPorts(context.Context) ([]dto.DockerPortGuardContainer, error)
|
|
Operate(context.Context, dto.DockerPortGuardOperation) error
|
|
QueueInitialization(dto.DockerPortGuardOperation) (dto.FilterChainOperationResponse, error)
|
|
DeletePolicies(dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error)
|
|
UpsertPolicies(dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error)
|
|
Reconcile(context.Context) error
|
|
}
|
|
|
|
func NewIDockerPortGuardService() IDockerPortGuardService {
|
|
return newDockerPortGuardService()
|
|
}
|
|
|
|
func newDockerPortGuardService() *DockerPortGuardService {
|
|
return &DockerPortGuardService{
|
|
policies: repo.NewIDockerPortGuardRepo(),
|
|
client: docker.NewDockerClient,
|
|
version: dockerFirewallVersion,
|
|
}
|
|
}
|
|
|
|
func ReconcileDockerPortGuard(ctx context.Context) error {
|
|
if global.DB == nil {
|
|
return nil
|
|
}
|
|
return NewIDockerPortGuardService().Reconcile(ctx)
|
|
}
|
|
|
|
func ReconcileDockerPortGuardBestEffort(ctx context.Context) {
|
|
if err := ReconcileDockerPortGuard(ctx); err != nil {
|
|
global.LOG.Warnf("reconcile Docker port guard failed, err: %v", err)
|
|
}
|
|
}
|
|
|
|
func (s *DockerPortGuardService) LoadPublishedPorts(ctx context.Context) ([]dto.DockerPortGuardContainer, error) {
|
|
cli, err := s.client()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrDockerUnavailable, err)
|
|
}
|
|
defer cli.Close()
|
|
|
|
if socketPath, local := strings.CutPrefix(cli.DaemonHost(), "unix://"); local {
|
|
if _, statErr := os.Stat(socketPath); errors.Is(statErr, os.ErrNotExist) {
|
|
return []dto.DockerPortGuardContainer{}, nil
|
|
}
|
|
}
|
|
|
|
endpoints, err := discoverDockerEndpoints(ctx, cli, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
backend := selectedDockerFirewallBackend("")
|
|
if info, infoErr := cli.Info(ctx); infoErr == nil {
|
|
backend = dockerFirewallBackend(info)
|
|
}
|
|
annotateDockerEndpointManagement(endpoints, backend)
|
|
return groupDockerGuardContainers(endpoints), nil
|
|
}
|
|
|
|
func (s *DockerPortGuardService) LoadOverview(ctx context.Context) (dto.DockerPortGuardList, error) {
|
|
policies, err := s.policies.ListManaged(ctx)
|
|
if err != nil {
|
|
return dto.DockerPortGuardList{}, err
|
|
}
|
|
unavailable := func() dto.DockerPortGuardList {
|
|
backend := selectedDockerFirewallBackend("")
|
|
base := s.runtimeStatus(s.guardRuntime(backend), backend)
|
|
base.Version = s.loadFirewallVersion(backend)
|
|
base.Message = agenti18n.Get("ErrDockerFailed")
|
|
return dto.DockerPortGuardList{Base: base, Containers: []dto.DockerPortGuardContainer{}, OrphanPolicies: dockerGuardPolicyEndpoints(policies)}
|
|
}
|
|
cli, err := s.client()
|
|
if err != nil {
|
|
return unavailable(), nil
|
|
}
|
|
defer cli.Close()
|
|
info, err := cli.Info(ctx)
|
|
if err != nil {
|
|
return unavailable(), nil
|
|
}
|
|
detectedBackend := dockerFirewallBackend(info)
|
|
backend := selectedDockerFirewallBackend(detectedBackend)
|
|
base := s.runtimeStatus(s.guardRuntime(backend), backend)
|
|
base.Version = s.loadFirewallVersion(backend)
|
|
if reconcileErr := lastDockerPortGuardReconcileError(); reconcileErr != nil {
|
|
markDockerGuardReconcileFailure(&base, reconcileErr)
|
|
}
|
|
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
|
|
if err != nil {
|
|
return dto.DockerPortGuardList{}, err
|
|
}
|
|
annotateDockerEndpointManagement(endpoints, detectedBackend)
|
|
endpoints, orphanPolicies := matchDockerGuardPolicies(base, policies, endpoints)
|
|
sort.Slice(endpoints, func(i, j int) bool {
|
|
return guardEndpointKey(endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol) < guardEndpointKey(endpoints[j].Family, endpoints[j].HostIP, endpoints[j].HostPort, endpoints[j].Protocol)
|
|
})
|
|
sort.Slice(orphanPolicies, func(i, j int) bool {
|
|
return guardEndpointKey(orphanPolicies[i].Family, orphanPolicies[i].HostIP, orphanPolicies[i].HostPort, orphanPolicies[i].Protocol) < guardEndpointKey(orphanPolicies[j].Family, orphanPolicies[j].HostIP, orphanPolicies[j].HostPort, orphanPolicies[j].Protocol)
|
|
})
|
|
return dto.DockerPortGuardList{Base: base, Containers: groupDockerGuardContainers(endpoints), OrphanPolicies: orphanPolicies}, nil
|
|
}
|
|
|
|
func matchDockerGuardPolicies(
|
|
base dto.DockerPortGuardBase,
|
|
policies []model.DockerPortGuardPolicy,
|
|
endpoints []dto.DockerPortGuardEndpoint,
|
|
) ([]dto.DockerPortGuardEndpoint, []dto.DockerPortGuardEndpoint) {
|
|
byEndpoint := make(map[string]model.DockerPortGuardPolicy, len(policies))
|
|
for _, policy := range policies {
|
|
byEndpoint[guardEndpointKey(policy.Family, policy.HostIP, policy.HostPort, policy.Protocol)] = policy
|
|
}
|
|
for i := range endpoints {
|
|
key := guardEndpointKey(endpoints[i].Family, endpoints[i].HostIP, endpoints[i].HostPort, endpoints[i].Protocol)
|
|
policy, ok := byEndpoint[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
endpoints[i].PolicyUUID, endpoints[i].Mode, endpoints[i].Sources = policy.UUID, policy.Mode, docker_guard.DecodeSources(policy.Sources)
|
|
endpoints[i].Description = policy.Description
|
|
endpoints[i].Effective = endpoints[i].ManagementTarget == dockerManagementContainerGuard &&
|
|
((policy.Family == docker_guard.FamilyIPv4 && base.IPv4.Effective) || (policy.Family == docker_guard.FamilyIPv6 && base.IPv6.Effective))
|
|
delete(byEndpoint, key)
|
|
}
|
|
orphanPolicies := make([]dto.DockerPortGuardEndpoint, 0, len(byEndpoint))
|
|
for _, policy := range byEndpoint {
|
|
orphanPolicies = append(orphanPolicies, dto.DockerPortGuardEndpoint{
|
|
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
|
|
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources), Description: policy.Description,
|
|
TrafficPath: dockerTrafficPathUnknown, ManagementTarget: dockerManagementNeedsDiagnosis,
|
|
ManagementReason: dockerReasonNoMatchingPath,
|
|
})
|
|
}
|
|
return endpoints, orphanPolicies
|
|
}
|
|
|
|
func initializeDockerGuardRuntime(runtime dockerGuardRuntime, policies []docker_guard.Policy) error {
|
|
err := runtime.Initialize(policies)
|
|
if errors.Is(err, docker_guard.ErrDockerForwardPolicyDrop) {
|
|
family := "IPv4"
|
|
var familyErr *docker_guard.FamilyError
|
|
if errors.As(err, &familyErr) && familyErr.Family == docker_guard.FamilyIPv6 {
|
|
family = "IPv6"
|
|
}
|
|
return buserr.WithMap("ErrDockerForwardPolicyDrop", map[string]interface{}{"family": family}, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (s *DockerPortGuardService) Operate(ctx context.Context, request dto.DockerPortGuardOperation) error {
|
|
dockerPortGuardServiceMu.Lock()
|
|
defer dockerPortGuardServiceMu.Unlock()
|
|
switch request.Operation {
|
|
case "initialize":
|
|
runtime, backend, err := s.runtimeForDocker(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
policies, err := s.runtimePolicies(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := initializeDockerGuardRuntime(runtime, policies); err != nil {
|
|
recordDockerPortGuardReconcileError(err)
|
|
return err
|
|
}
|
|
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, backend); err != nil {
|
|
return err
|
|
}
|
|
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable); err != nil {
|
|
return err
|
|
}
|
|
recordDockerPortGuardReconcileError(nil)
|
|
return nil
|
|
case "bind":
|
|
runtime, _, err := s.runtimeForDocker(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := runtime.Bind(); err != nil {
|
|
return err
|
|
}
|
|
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable)
|
|
case "unbind":
|
|
var err error
|
|
if s.runtime != nil {
|
|
err = s.runtime.Unbind()
|
|
} else {
|
|
err = errors.Join(docker_guard.NewManager().Unbind(), docker_guard.NewNftablesManager().Unbind())
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusDisable)
|
|
default:
|
|
return fmt.Errorf("unsupported Docker port guard operation: %s", request.Operation)
|
|
}
|
|
}
|
|
|
|
func (s *DockerPortGuardService) QueueInitialization(
|
|
request dto.DockerPortGuardOperation,
|
|
) (dto.FilterChainOperationResponse, error) {
|
|
if request.Operation != "initialize" {
|
|
return dto.FilterChainOperationResponse{}, fmt.Errorf("only Docker port guard initialization can be queued")
|
|
}
|
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeFirewall, 0); err != nil {
|
|
return dto.FilterChainOperationResponse{}, err
|
|
}
|
|
taskItem, err := task.NewTask(firewallTaskName(task.TaskExec, firewallTaskDocker, ""), task.TaskExec, task.TaskScopeFirewall, request.TaskID, 0)
|
|
if err != nil {
|
|
return dto.FilterChainOperationResponse{}, fmt.Errorf("create Docker port guard initialization task: %w", err)
|
|
}
|
|
var runtime dockerGuardRuntime
|
|
var backend string
|
|
var policies []docker_guard.Policy
|
|
taskItem.AddSubTask(agenti18n.GetMsgByKey("FirewallInspectDockerGuardStep"), func(t *task.Task) error {
|
|
var err error
|
|
runtime, backend, err = s.runtimeForDocker(t.TaskCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
policies, err = s.runtimePolicies(t.TaskCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
t.Logf("backend=%s", backend)
|
|
return nil
|
|
}, nil)
|
|
taskItem.AddSubTask(agenti18n.GetWithName("FirewallInitializeDockerGuardStep", "Docker"), func(t *task.Task) error {
|
|
dockerPortGuardServiceMu.Lock()
|
|
defer dockerPortGuardServiceMu.Unlock()
|
|
t.Logf("backend=%s", backend)
|
|
err := initializeDockerGuardRuntime(runtime, policies)
|
|
recordDockerPortGuardReconcileError(err)
|
|
return err
|
|
}, nil)
|
|
taskItem.AddSubTask(agenti18n.GetMsgByKey("FirewallPersistDockerGuardStep"), func(t *task.Task) error {
|
|
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerBackendKey, backend); err != nil {
|
|
return err
|
|
}
|
|
if err := settingRepo.UpdateOrCreate(constant.FirewallDockerPortGuardStatusKey, constant.StatusEnable); err != nil {
|
|
return err
|
|
}
|
|
recordDockerPortGuardReconcileError(nil)
|
|
return nil
|
|
}, nil)
|
|
if err := repo.NewITaskRepo().Save(context.Background(), taskItem.Task); err != nil {
|
|
return dto.FilterChainOperationResponse{}, fmt.Errorf("save Docker port guard initialization task: %w", err)
|
|
}
|
|
go func() { _ = taskItem.Execute() }()
|
|
return dto.FilterChainOperationResponse{TaskID: taskItem.TaskID, Queued: true}, nil
|
|
}
|
|
|
|
func (s *DockerPortGuardService) DeletePolicies(request dto.DockerPortGuardPolicyBatchDelete) (dto.FilterChainOperationResponse, error) {
|
|
uuids, err := docker_guard.NormalizePolicyUUIDs(request.UUIDs)
|
|
if err != nil {
|
|
return dto.FilterChainOperationResponse{}, err
|
|
}
|
|
labels := make([]string, len(uuids))
|
|
for i, id := range uuids {
|
|
labels[i] = fmt.Sprintf("[%d/%d] %s", i+1, len(uuids), id)
|
|
}
|
|
return queueFirewallRuleTask(firewallTaskDocker, task.TaskDelete, labels, func(ctx context.Context) error {
|
|
dockerPortGuardServiceMu.Lock()
|
|
defer dockerPortGuardServiceMu.Unlock()
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if err := s.policies.DeleteBatch(ctx, uuids); err != nil {
|
|
return err
|
|
}
|
|
return s.reconcileLocked(ctx)
|
|
})
|
|
}
|
|
|
|
func (s *DockerPortGuardService) UpsertPolicies(request dto.DockerPortGuardPolicyBatch) (dto.FilterChainOperationResponse, error) {
|
|
labels := make([]string, len(request.Policies))
|
|
for i, policy := range request.Policies {
|
|
labels[i] = fmt.Sprintf("[%d/%d] %s %s %s:%d %s", i+1, len(request.Policies), policy.Family, policy.Protocol, policy.HostIP, policy.HostPort, policy.Mode)
|
|
}
|
|
return queueFirewallRuleTask(firewallTaskDocker, task.TaskUpdate, labels, func(ctx context.Context) error {
|
|
dockerPortGuardServiceMu.Lock()
|
|
defer dockerPortGuardServiceMu.Unlock()
|
|
policies := make([]model.DockerPortGuardPolicy, 0, len(request.Policies))
|
|
endpoints := make([]dto.DockerPortGuardEndpointIdentity, 0, len(request.Policies))
|
|
for i, policy := range request.Policies {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
normalized, err := docker_guard.NormalizePolicy(docker_guard.Policy{
|
|
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort,
|
|
Protocol: policy.Protocol, Mode: policy.Mode, Sources: policy.Sources,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", labels[i], err)
|
|
}
|
|
encoded, err := json.Marshal(normalized.Sources)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", labels[i], err)
|
|
}
|
|
policies = append(policies, model.DockerPortGuardPolicy{
|
|
UUID: uuid.NewString(), Family: normalized.Family, HostIP: normalized.HostIP,
|
|
HostPort: normalized.HostPort, Protocol: normalized.Protocol, Mode: normalized.Mode,
|
|
Sources: string(encoded), Description: strings.TrimSpace(policy.Description),
|
|
})
|
|
endpoints = append(endpoints, policy.DockerPortGuardEndpointIdentity)
|
|
}
|
|
if err := s.rejectHostInputDockerGuardEndpoints(ctx, endpoints); err != nil {
|
|
return err
|
|
}
|
|
if err := s.policies.UpsertBatch(ctx, policies); err != nil {
|
|
return err
|
|
}
|
|
return s.reconcileLocked(ctx)
|
|
})
|
|
}
|
|
|
|
func (s *DockerPortGuardService) rejectHostInputDockerGuardEndpoints(
|
|
ctx context.Context,
|
|
requested []dto.DockerPortGuardEndpointIdentity,
|
|
) error {
|
|
if s.client == nil || len(requested) == 0 {
|
|
return nil
|
|
}
|
|
cli, err := s.client()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer cli.Close()
|
|
info, err := cli.Info(ctx)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
endpoints, err := discoverDockerEndpoints(ctx, cli, true)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
annotateDockerEndpointManagement(endpoints, dockerFirewallBackend(info))
|
|
targets := make(map[string]string, len(endpoints))
|
|
for _, endpoint := range endpoints {
|
|
targets[guardEndpointKey(endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)] = endpoint.ManagementTarget
|
|
}
|
|
for _, endpoint := range requested {
|
|
target := targets[guardEndpointKey(endpoint.Family, endpoint.HostIP, endpoint.HostPort, endpoint.Protocol)]
|
|
if target == dockerManagementHostFirewall {
|
|
return fmt.Errorf("%w: endpoint traffic is handled by the host input firewall", ErrDockerGuardInvalid)
|
|
}
|
|
if target == dockerManagementNeedsDiagnosis {
|
|
return fmt.Errorf("%w: endpoint traffic management target requires diagnosis", ErrDockerGuardInvalid)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *DockerPortGuardService) Reconcile(ctx context.Context) error {
|
|
dockerPortGuardServiceMu.Lock()
|
|
defer dockerPortGuardServiceMu.Unlock()
|
|
return s.reconcileLocked(ctx)
|
|
}
|
|
|
|
func dockerGuardPolicyFromModel(policy model.DockerPortGuardPolicy) docker_guard.Policy {
|
|
return docker_guard.Policy{
|
|
UUID: policy.UUID, Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort,
|
|
Protocol: policy.Protocol, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources),
|
|
}
|
|
}
|
|
|
|
func dockerGuardReadOnlyPolicyUUID(policy docker_guard.ReadOnlyPolicy) string {
|
|
nativeRules, _ := json.Marshal(policy.NativeRules)
|
|
fingerprint := strings.Join([]string{
|
|
policy.Policy.Family, policy.Policy.HostIP, strconv.Itoa(int(policy.Policy.HostPort)),
|
|
policy.Policy.Protocol, policy.Action, string(nativeRules),
|
|
}, "\x00")
|
|
return uuid.NewSHA1(uuid.NameSpaceOID, []byte(fingerprint)).String()
|
|
}
|
|
|
|
func dockerGuardRuntimeReadOnlyModels(policies []docker_guard.ReadOnlyPolicy) ([]model.DockerPortGuardPolicy, error) {
|
|
result := make([]model.DockerPortGuardPolicy, 0, len(policies))
|
|
for _, policy := range policies {
|
|
sources, err := json.Marshal(policy.Policy.Sources)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nativeRules, err := json.Marshal(policy.NativeRules)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, model.DockerPortGuardPolicy{
|
|
UUID: dockerGuardReadOnlyPolicyUUID(policy),
|
|
ReadOnly: true,
|
|
Family: policy.Policy.Family, HostIP: policy.Policy.HostIP, HostPort: policy.Policy.HostPort,
|
|
Protocol: policy.Policy.Protocol, Sources: string(sources), NativeAction: policy.Action,
|
|
NativeRules: string(nativeRules), Sequence: policy.Sequence,
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *DockerPortGuardService) replaceRuntimeReadOnlyPolicies(
|
|
ctx context.Context,
|
|
policies []docker_guard.ReadOnlyPolicy,
|
|
) error {
|
|
stored, err := dockerGuardRuntimeReadOnlyModels(policies)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.policies.ReplaceRuntimeReadOnly(ctx, stored)
|
|
}
|
|
|
|
func (s *DockerPortGuardService) reconcileLocked(ctx context.Context) (err error) {
|
|
defer func() { recordDockerPortGuardReconcileError(err) }()
|
|
persistedEnabled, err := dockerPortGuardPersistedEnabled()
|
|
if err != nil {
|
|
return fmt.Errorf("load Docker port guard persisted status: %w", err)
|
|
}
|
|
initialized, err := s.anyRuntimeInitialized()
|
|
if err != nil {
|
|
return &docker_guard.FamilyError{Family: docker_guard.FamilyIPv4, Err: fmt.Errorf("inspect initialization: %w", err)}
|
|
}
|
|
if !initialized && !persistedEnabled {
|
|
return nil
|
|
}
|
|
runtime, _, err := s.runtimeForDocker(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
initialized, err = runtime.Initialized(docker_guard.FamilyIPv4)
|
|
if err != nil {
|
|
return &docker_guard.FamilyError{Family: docker_guard.FamilyIPv4, Err: fmt.Errorf("inspect initialization: %w", err)}
|
|
}
|
|
if !initialized && !persistedEnabled {
|
|
return nil
|
|
}
|
|
policies, err := s.runtimePolicies(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
inventory, err := runtime.ListPolicies()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.replaceRuntimeReadOnlyPolicies(ctx, inventory.ReadOnly); err != nil {
|
|
return err
|
|
}
|
|
if !initialized {
|
|
err = initializeDockerGuardRuntime(runtime, policies)
|
|
} else {
|
|
err = runtime.Reconcile(policies)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return docker_guard.Verify(runtime, policies, inventory.ReadOnly)
|
|
}
|
|
|
|
func dockerPortGuardPersistedEnabled() (bool, error) {
|
|
status, err := settingRepo.GetValueByKey(constant.FirewallDockerPortGuardStatusKey)
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return false, nil
|
|
}
|
|
return status == constant.StatusEnable, err
|
|
}
|
|
|
|
func (s *DockerPortGuardService) anyRuntimeInitialized() (bool, error) {
|
|
if s.runtime != nil {
|
|
return s.runtime.Initialized(docker_guard.FamilyIPv4)
|
|
}
|
|
for _, runtime := range []dockerGuardRuntime{docker_guard.NewManager(), docker_guard.NewNftablesManager()} {
|
|
initialized, err := runtime.Initialized(docker_guard.FamilyIPv4)
|
|
if err != nil || initialized {
|
|
return initialized, err
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func recordDockerPortGuardReconcileError(err error) {
|
|
dockerPortGuardSyncMu.Lock()
|
|
dockerPortGuardSyncErr = err
|
|
dockerPortGuardSyncMu.Unlock()
|
|
}
|
|
|
|
func lastDockerPortGuardReconcileError() error {
|
|
dockerPortGuardSyncMu.RLock()
|
|
defer dockerPortGuardSyncMu.RUnlock()
|
|
return dockerPortGuardSyncErr
|
|
}
|
|
|
|
func markDockerGuardReconcileFailure(base *dto.DockerPortGuardBase, err error) {
|
|
var familyErr *docker_guard.FamilyError
|
|
if errors.As(err, &familyErr) {
|
|
markDockerGuardFamilyNotEffective(base, familyErr.Family)
|
|
if familyErr.Family == docker_guard.FamilyIPv4 {
|
|
markDockerGuardFamilyNotEffective(base, docker_guard.FamilyIPv6)
|
|
}
|
|
return
|
|
}
|
|
markDockerGuardFamilyNotEffective(base, docker_guard.FamilyIPv4)
|
|
markDockerGuardFamilyNotEffective(base, docker_guard.FamilyIPv6)
|
|
}
|
|
|
|
func markDockerGuardFamilyNotEffective(base *dto.DockerPortGuardBase, family string) {
|
|
var status *dto.DockerPortGuardFamilyStatus
|
|
switch family {
|
|
case docker_guard.FamilyIPv4:
|
|
status = &base.IPv4
|
|
case docker_guard.FamilyIPv6:
|
|
status = &base.IPv6
|
|
default:
|
|
return
|
|
}
|
|
if !status.Initialized {
|
|
return
|
|
}
|
|
status.State = docker_guard.StatusNotEffective
|
|
status.Reason = docker_guard.ReasonInspectFailed
|
|
status.Effective = false
|
|
}
|
|
|
|
func (s *DockerPortGuardService) runtimePolicies(ctx context.Context) ([]docker_guard.Policy, error) {
|
|
stored, err := s.policies.ListManaged(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
policies := make([]docker_guard.Policy, 0, len(stored))
|
|
for _, policy := range stored {
|
|
policies = append(policies, docker_guard.Policy{UUID: policy.UUID, Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources)})
|
|
}
|
|
return policies, nil
|
|
}
|
|
|
|
func (s *DockerPortGuardService) runtimeStatus(runtime dockerGuardRuntime, backend string) dto.DockerPortGuardBase {
|
|
ipv4 := runtime.Status(docker_guard.FamilyIPv4)
|
|
ipv6 := runtime.Status(docker_guard.FamilyIPv6)
|
|
return dto.DockerPortGuardBase{
|
|
Name: dockerFirewallDisplayName(backend),
|
|
Backend: backend,
|
|
IsExist: ipv4.Reason != docker_guard.ReasonCommandMissing || ipv6.Reason != docker_guard.ReasonCommandMissing,
|
|
Initialized: ipv4.Initialized || ipv6.Initialized,
|
|
Bound: ipv4.Bound || ipv6.Bound,
|
|
IPv4: dto.DockerPortGuardFamilyStatus{State: ipv4.State, Reason: ipv4.Reason, Initialized: ipv4.Initialized, Bound: ipv4.Bound, Effective: ipv4.Effective},
|
|
IPv6: dto.DockerPortGuardFamilyStatus{State: ipv6.State, Reason: ipv6.Reason, Initialized: ipv6.Initialized, Bound: ipv6.Bound, Effective: ipv6.Effective},
|
|
}
|
|
}
|
|
|
|
func (s *DockerPortGuardService) guardRuntime(backend string) dockerGuardRuntime {
|
|
if s.runtimeForBackend != nil {
|
|
return s.runtimeForBackend(backend)
|
|
}
|
|
if s.runtime != nil {
|
|
return s.runtime
|
|
}
|
|
return docker_guard.NewRuntime(backend)
|
|
}
|
|
|
|
func (s *DockerPortGuardService) runtimeForDocker(ctx context.Context) (dockerGuardRuntime, string, error) {
|
|
if s.runtime != nil {
|
|
return s.runtime, selectedDockerFirewallBackend(constant.FirewallProviderIptables), nil
|
|
}
|
|
cli, err := s.client()
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("%w: %v", ErrDockerUnavailable, err)
|
|
}
|
|
defer cli.Close()
|
|
info, err := cli.Info(ctx)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("%w: %v", ErrDockerUnavailable, err)
|
|
}
|
|
backend := selectedDockerFirewallBackend(dockerFirewallBackend(info))
|
|
if backend != constant.FirewallProviderIptables && backend != constant.FirewallProviderNftables {
|
|
return nil, backend, fmt.Errorf("Docker firewall backend %q is not supported", backend)
|
|
}
|
|
return s.guardRuntime(backend), backend, nil
|
|
}
|
|
|
|
func dockerFirewallBackend(info system.Info) string {
|
|
if info.FirewallBackend == nil || info.FirewallBackend.Driver == "" {
|
|
return constant.FirewallProviderIptables
|
|
}
|
|
return strings.ToLower(info.FirewallBackend.Driver)
|
|
}
|
|
|
|
func dockerFirewallDisplayName(backend string) string {
|
|
switch strings.ToLower(strings.TrimSpace(backend)) {
|
|
case constant.FirewallProviderNftables:
|
|
return "nftables-docker"
|
|
default:
|
|
return "iptables-docker"
|
|
}
|
|
}
|
|
|
|
func (s *DockerPortGuardService) loadFirewallVersion(backend string) string {
|
|
if s.version == nil {
|
|
return "-"
|
|
}
|
|
return s.version(backend)
|
|
}
|
|
|
|
func dockerFirewallVersion(backend string) string {
|
|
client, err := lifecycle.NewClientFor(backend)
|
|
if err != nil {
|
|
return "-"
|
|
}
|
|
version, err := client.Version()
|
|
if err != nil || strings.TrimSpace(version) == "" {
|
|
return "-"
|
|
}
|
|
return version
|
|
}
|
|
|
|
func discoverDockerEndpoints(ctx context.Context, cli *client.Client, all bool) ([]dto.DockerPortGuardEndpoint, error) {
|
|
containers, err := cli.ContainerList(ctx, containertypes.ListOptions{All: all})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
endpoints := make([]dto.DockerPortGuardEndpoint, 0)
|
|
for _, item := range containers {
|
|
name := strings.TrimPrefix(firstGuardString(item.Names), "/")
|
|
compose := item.Labels[dockerGuardComposeProjectLabel]
|
|
application := ""
|
|
if created, ok := item.Labels[dockerGuardComposeCreatedBy]; ok && created == "Apps" {
|
|
application = compose
|
|
}
|
|
for _, port := range item.Ports {
|
|
if port.PublicPort == 0 || (port.Type != "tcp" && port.Type != "udp") {
|
|
continue
|
|
}
|
|
family := docker_guard.FamilyIPv4
|
|
hostIP := port.IP
|
|
if addr, err := netip.ParseAddr(hostIP); err == nil && addr.Is6() {
|
|
family = docker_guard.FamilyIPv6
|
|
} else if hostIP == "" {
|
|
hostIP = "0.0.0.0"
|
|
}
|
|
endpoints = append(endpoints, dto.DockerPortGuardEndpoint{Family: family, HostIP: hostIP, HostPort: port.PublicPort, Protocol: port.Type, ContainerID: item.ID, ContainerName: name, ContainerState: item.State, ContainerPort: port.PrivatePort, Compose: compose, Application: application, Sources: []string{}})
|
|
}
|
|
}
|
|
return endpoints, nil
|
|
}
|
|
|
|
func dockerGuardPolicyEndpoints(policies []model.DockerPortGuardPolicy) []dto.DockerPortGuardEndpoint {
|
|
endpoints := make([]dto.DockerPortGuardEndpoint, 0, len(policies))
|
|
for _, policy := range policies {
|
|
endpoints = append(endpoints, dto.DockerPortGuardEndpoint{
|
|
Family: policy.Family, HostIP: policy.HostIP, HostPort: policy.HostPort, Protocol: policy.Protocol,
|
|
PolicyUUID: policy.UUID, Mode: policy.Mode, Sources: docker_guard.DecodeSources(policy.Sources),
|
|
Description: policy.Description, TrafficPath: dockerTrafficPathUnknown,
|
|
ManagementTarget: dockerManagementNeedsDiagnosis, ManagementReason: dockerReasonNoMatchingPath,
|
|
})
|
|
}
|
|
return endpoints
|
|
}
|
|
|
|
func groupDockerGuardContainers(endpoints []dto.DockerPortGuardEndpoint) []dto.DockerPortGuardContainer {
|
|
containers := make(map[string]*dto.DockerPortGuardContainer)
|
|
order := make([]string, 0)
|
|
for _, endpoint := range endpoints {
|
|
key := endpoint.ContainerID
|
|
if key == "" {
|
|
key = "__orphan__"
|
|
}
|
|
container, ok := containers[key]
|
|
if !ok {
|
|
container = &dto.DockerPortGuardContainer{
|
|
Key: key, Name: endpoint.ContainerName, Compose: endpoint.Compose,
|
|
Application: endpoint.Application, Endpoints: []dto.DockerPortGuardEndpoint{},
|
|
}
|
|
containers[key] = container
|
|
order = append(order, key)
|
|
}
|
|
container.Endpoints = append(container.Endpoints, endpoint)
|
|
}
|
|
sort.Slice(order, func(i, j int) bool {
|
|
return containers[order[i]].Name < containers[order[j]].Name
|
|
})
|
|
|
|
result := make([]dto.DockerPortGuardContainer, 0, len(order))
|
|
for _, key := range order {
|
|
container := containers[key]
|
|
items := make([]docker.PortRangeItem, 0, len(container.Endpoints))
|
|
for i, endpoint := range container.Endpoints {
|
|
sources := append([]string(nil), endpoint.Sources...)
|
|
sort.Strings(sources)
|
|
policyKey := fmt.Sprintf("%t|%s|%s|%t|%s|%s|%s", endpoint.PolicyUUID != "", endpoint.Mode, strings.Join(sources, ","), endpoint.Effective, endpoint.Description, endpoint.ManagementTarget, endpoint.ManagementReason)
|
|
items = append(items, docker.PortRangeItem{
|
|
Key: endpoint.Family + "|" + endpoint.HostIP + "|" + endpoint.Protocol + "|" + policyKey,
|
|
PublicPort: endpoint.HostPort, PrivatePort: endpoint.ContainerPort,
|
|
HasPrivatePort: endpoint.ContainerPort != 0, Position: i,
|
|
})
|
|
}
|
|
container.PortGroups = make([]dto.DockerPortGuardPortGroup, 0, len(items))
|
|
for _, portRange := range docker.MergePortRanges(items) {
|
|
start := container.Endpoints[portRange.Start.Position]
|
|
address := start.HostIP
|
|
if strings.Contains(address, ":") {
|
|
address = "[" + address + "]"
|
|
}
|
|
ports := fmt.Sprintf("%d", portRange.Start.PublicPort)
|
|
if portRange.Start.PublicPort != portRange.End.PublicPort {
|
|
ports = fmt.Sprintf("%d-%d", portRange.Start.PublicPort, portRange.End.PublicPort)
|
|
}
|
|
container.PortGroups = append(container.PortGroups, dto.DockerPortGuardPortGroup{
|
|
Key: fmt.Sprintf("%s|%d-%d", portRange.Start.Key, portRange.Start.PublicPort, portRange.End.PublicPort),
|
|
Label: fmt.Sprintf("%s:%s/%s", address, ports, start.Protocol), Endpoint: start,
|
|
Endpoints: func() []dto.DockerPortGuardEndpoint {
|
|
members := make([]dto.DockerPortGuardEndpoint, 0, len(portRange.Items))
|
|
for _, item := range portRange.Items {
|
|
members = append(members, container.Endpoints[item.Position])
|
|
}
|
|
return members
|
|
}(),
|
|
})
|
|
}
|
|
result = append(result, *container)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func guardEndpointKey(family, hostIP string, hostPort uint16, protocol string) string {
|
|
return fmt.Sprintf("%s|%s|%d|%s", family, hostIP, hostPort, protocol)
|
|
}
|
|
|
|
func firstGuardString(values []string) string {
|
|
if len(values) == 0 {
|
|
return ""
|
|
}
|
|
return values[0]
|
|
}
|
|
|
|
func annotateDockerEndpointManagement(endpoints []dto.DockerPortGuardEndpoint, backend string) {
|
|
rules := map[string]dockerForwardRules{
|
|
constant.FirewallFamilyIPv4: loadDockerDNATRules(backend, constant.FirewallFamilyIPv4),
|
|
constant.FirewallFamilyIPv6: loadDockerDNATRules(backend, constant.FirewallFamilyIPv6),
|
|
}
|
|
proxies := loadDockerProxyEndpoints()
|
|
for i := range endpoints {
|
|
familyRules := rules[endpoints[i].Family]
|
|
endpoints[i].TrafficPath, endpoints[i].ManagementTarget, endpoints[i].ManagementReason =
|
|
dockerEndpointManagement(backend, familyRules, proxies, endpoints[i])
|
|
}
|
|
}
|
|
|
|
func dockerEndpointManagement(
|
|
backend string,
|
|
rules dockerForwardRules,
|
|
proxies dockerProxyEndpoints,
|
|
endpoint dto.DockerPortGuardEndpoint,
|
|
) (string, string, string) {
|
|
if !rules.inspected {
|
|
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonNATInspectFailed
|
|
}
|
|
dnatMatched := dockerDNATRuleMatches(backend, rules.output, endpoint)
|
|
if dnatMatched && dockerDNATIngressReachable(backend, rules.output) {
|
|
return dockerTrafficPathForward, dockerManagementContainerGuard, ""
|
|
}
|
|
if !proxies.inspected {
|
|
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonProxyInspectFailed
|
|
}
|
|
if dockerProxyEndpointMatches(proxies.items, endpoint) {
|
|
return dockerTrafficPathInput, dockerManagementHostFirewall, ""
|
|
}
|
|
if dnatMatched {
|
|
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonNATChainUnreachable
|
|
}
|
|
return dockerTrafficPathUnknown, dockerManagementNeedsDiagnosis, dockerReasonNoMatchingPath
|
|
}
|
|
|
|
func loadDockerDNATRules(backend, family string) dockerForwardRules {
|
|
manager := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C"))
|
|
if backend == constant.FirewallProviderNftables {
|
|
tableFamily := "ip"
|
|
if family == constant.FirewallFamilyIPv6 {
|
|
tableFamily = "ip6"
|
|
}
|
|
tables, err := manager.RunWithOptionalSudoAndStdout("nft", "list", "tables")
|
|
if err != nil {
|
|
return dockerForwardRules{}
|
|
}
|
|
if !strings.Contains(tables, "table "+tableFamily+" docker-bridges") {
|
|
return dockerForwardRules{inspected: true}
|
|
}
|
|
output, err := manager.RunWithOptionalSudoAndStdout("nft", "list", "table", tableFamily, "docker-bridges")
|
|
return dockerForwardRules{output: output, inspected: err == nil}
|
|
}
|
|
commands, err := lifecycle.ResolveIptablesCommands()
|
|
if err != nil {
|
|
return dockerForwardRules{}
|
|
}
|
|
executable := commands.IPv4
|
|
if family == constant.FirewallFamilyIPv6 {
|
|
executable = commands.IPv6
|
|
}
|
|
if executable == "" {
|
|
return dockerForwardRules{}
|
|
}
|
|
output, err := manager.RunWithOptionalSudoAndStdout(executable, "-w", "-t", "nat", "-S")
|
|
return dockerForwardRules{output: output, inspected: err == nil}
|
|
}
|
|
|
|
func loadDockerProxyEndpoints() dockerProxyEndpoints {
|
|
manager := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Second), cmd.WithEnv("LC_ALL=C"))
|
|
output, err := manager.RunWithStdout("ps", "-ww", "-eo", "args=")
|
|
if err != nil {
|
|
return dockerProxyEndpoints{}
|
|
}
|
|
return dockerProxyEndpoints{items: parseDockerProxyEndpoints(output), inspected: true}
|
|
}
|
|
|
|
func parseDockerProxyEndpoints(output string) []dockerProxyEndpoint {
|
|
result := make([]dockerProxyEndpoint, 0)
|
|
for _, line := range strings.Split(output, "\n") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) == 0 || !dockerProxyCommand(fields) {
|
|
continue
|
|
}
|
|
protocol := commandFlagValue(fields, "-proto")
|
|
hostIP := commandFlagValue(fields, "-host-ip")
|
|
hostPortValue := commandFlagValue(fields, "-host-port")
|
|
hostPort, err := strconv.ParseUint(hostPortValue, 10, 16)
|
|
if err != nil || (protocol != "tcp" && protocol != "udp") || hostIP == "" {
|
|
continue
|
|
}
|
|
result = append(result, dockerProxyEndpoint{protocol: protocol, hostIP: canonicalAddress(hostIP), hostPort: uint16(hostPort)})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func dockerProxyCommand(fields []string) bool {
|
|
for _, field := range fields {
|
|
if filepath.Base(field) == "docker-proxy" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func commandFlagValue(fields []string, name string) string {
|
|
for i := 0; i < len(fields); i++ {
|
|
if fields[i] == name && i+1 < len(fields) {
|
|
return fields[i+1]
|
|
}
|
|
if strings.HasPrefix(fields[i], name+"=") {
|
|
return strings.TrimPrefix(fields[i], name+"=")
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func dockerProxyEndpointMatches(proxies []dockerProxyEndpoint, endpoint dto.DockerPortGuardEndpoint) bool {
|
|
for _, proxy := range proxies {
|
|
if proxy.protocol == endpoint.Protocol && proxy.hostPort == endpoint.HostPort && hostAddressMatches(proxy.hostIP, endpoint.HostIP, endpoint.Family) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func dockerDNATRuleMatches(backend, output string, endpoint dto.DockerPortGuardEndpoint) bool {
|
|
if strings.TrimSpace(output) == "" {
|
|
return false
|
|
}
|
|
if backend == constant.FirewallProviderNftables {
|
|
return nftDNATRuleMatches(output, endpoint)
|
|
}
|
|
return iptablesDNATRuleMatches(output, endpoint)
|
|
}
|
|
|
|
func dockerDNATIngressReachable(backend, output string) bool {
|
|
if backend == constant.FirewallProviderNftables {
|
|
return strings.Contains(output, "hook prerouting")
|
|
}
|
|
for _, line := range strings.Split(output, "\n") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) >= 4 && fields[0] == "-A" && fields[1] == "PREROUTING" && commandFlagValue(fields, "-j") == "DOCKER" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func iptablesDNATRuleMatches(output string, endpoint dto.DockerPortGuardEndpoint) bool {
|
|
port := strconv.Itoa(int(endpoint.HostPort))
|
|
for _, line := range strings.Split(output, "\n") {
|
|
fields := strings.Fields(line)
|
|
if commandFlagValue(fields, "-p") != endpoint.Protocol || commandFlagValue(fields, "--dport") != port || commandFlagValue(fields, "-j") != "DNAT" {
|
|
continue
|
|
}
|
|
if destinationAddressMatches(commandFlagValue(fields, "-d"), endpoint) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func nftDNATRuleMatches(output string, endpoint dto.DockerPortGuardEndpoint) bool {
|
|
port := strconv.Itoa(int(endpoint.HostPort))
|
|
for _, line := range strings.Split(output, "\n") {
|
|
fields := strings.Fields(strings.NewReplacer("{", " ", "}", " ", ",", " ", ";", " ").Replace(line))
|
|
if !containsToken(fields, "dnat") || !nftProtocolPortMatches(fields, endpoint.Protocol, port) {
|
|
continue
|
|
}
|
|
destination := nftDestinationAddress(fields, endpoint.Family)
|
|
if destinationAddressMatches(destination, endpoint) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func nftProtocolPortMatches(fields []string, protocol, port string) bool {
|
|
for i := 0; i+2 < len(fields); i++ {
|
|
if fields[i] == protocol && fields[i+1] == "dport" && fields[i+2] == port {
|
|
return true
|
|
}
|
|
if fields[i] == "th" && fields[i+1] == "dport" && fields[i+2] == port && nftMetaProtocolMatches(fields, protocol) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func nftMetaProtocolMatches(fields []string, protocol string) bool {
|
|
for i := 0; i+2 < len(fields); i++ {
|
|
if fields[i] == "meta" && fields[i+1] == "l4proto" && fields[i+2] == protocol {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func nftDestinationAddress(fields []string, family string) string {
|
|
token := "ip"
|
|
if family == constant.FirewallFamilyIPv6 {
|
|
token = "ip6"
|
|
}
|
|
for i := 0; i+2 < len(fields); i++ {
|
|
if fields[i] == token && fields[i+1] == "daddr" {
|
|
return fields[i+2]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func destinationAddressMatches(ruleAddress string, endpoint dto.DockerPortGuardEndpoint) bool {
|
|
ruleAddress = strings.TrimSpace(strings.Split(ruleAddress, "/")[0])
|
|
if isWildcardHostAddress(endpoint.HostIP, endpoint.Family) {
|
|
return ruleAddress == ""
|
|
}
|
|
return ruleAddress == "" || canonicalAddress(ruleAddress) == canonicalAddress(endpoint.HostIP)
|
|
}
|
|
|
|
func hostAddressMatches(left, right, family string) bool {
|
|
if isWildcardHostAddress(left, family) && isWildcardHostAddress(right, family) {
|
|
return true
|
|
}
|
|
return canonicalAddress(left) == canonicalAddress(right)
|
|
}
|
|
|
|
func isWildcardHostAddress(value, family string) bool {
|
|
value = strings.TrimSpace(value)
|
|
if family == constant.FirewallFamilyIPv6 {
|
|
return value == "" || value == "::"
|
|
}
|
|
return value == "" || value == "0.0.0.0"
|
|
}
|
|
|
|
func canonicalAddress(value string) string {
|
|
if address, err := netip.ParseAddr(strings.TrimSpace(value)); err == nil {
|
|
return address.String()
|
|
}
|
|
return strings.TrimSpace(value)
|
|
}
|
|
|
|
func containsToken(fields []string, value string) bool {
|
|
for _, field := range fields {
|
|
if field == value {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|