Skip to content
108 changes: 88 additions & 20 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ endif
# Interrogate the git repo and set some variables
REPO_ROOT ?= $(shell git rev-parse --show-toplevel)
REVISION ?= $(shell git rev-parse --short HEAD)
ACN_VERSION ?= $(shell git describe --exclude "azure-ip-masq-merger*" --exclude "azure-ipam*" --exclude "dropgz*" --exclude "zapai*" --exclude "ipv6-hp-bpf*" --tags --always)
ACN_VERSION ?= $(shell git describe --exclude "azure-iptables-monitor*" --exclude "azure-ip-masq-merger*" --exclude "azure-ipam*" --exclude "dropgz*" --exclude "zapai*" --exclude "ipv6-hp-bpf*" --tags --always)
IPV6_HP_BPF_VERSION ?= $(notdir $(shell git describe --match "ipv6-hp-bpf*" --tags --always))
AZURE_IPAM_VERSION ?= $(notdir $(shell git describe --match "azure-ipam*" --tags --always))
AZURE_IP_MASQ_MERGER_VERSION ?= $(notdir $(shell git describe --match "azure-ip-masq-merger*" --tags --always))
AZURE_IPTABLES_MONITOR_VERSION ?= $(notdir $(shell git describe --match "azure-iptables-monitor*" --tags --always))
CNI_VERSION ?= $(ACN_VERSION)
CNS_VERSION ?= $(ACN_VERSION)
NPM_VERSION ?= $(ACN_VERSION)
Expand All @@ -44,6 +45,7 @@ ZAPAI_VERSION ?= $(notdir $(shell git describe --match "zapai*" --tags --al
# Build directories.
AZURE_IPAM_DIR = $(REPO_ROOT)/azure-ipam
AZURE_IP_MASQ_MERGER_DIR = $(REPO_ROOT)/azure-ip-masq-merger
AZURE_IPTABLES_MONITOR_DIR = $(REPO_ROOT)/azure-iptables-monitor
IPV6_HP_BPF_DIR = $(REPO_ROOT)/bpf-prog/ipv6-hp-bpf

CNI_NET_DIR = $(REPO_ROOT)/cni/network/plugin
Expand All @@ -58,6 +60,7 @@ OUTPUT_DIR = $(REPO_ROOT)/output
BUILD_DIR = $(OUTPUT_DIR)/$(GOOS)_$(GOARCH)
AZURE_IPAM_BUILD_DIR = $(BUILD_DIR)/azure-ipam
AZURE_IP_MASQ_MERGER_BUILD_DIR = $(BUILD_DIR)/azure-ip-masq-merger
AZURE_IPTABLES_MONITOR_BUILD_DIR = $(BUILD_DIR)/azure-iptables-monitor
IPV6_HP_BPF_BUILD_DIR = $(BUILD_DIR)/bpf-prog/ipv6-hp-bpf
IMAGE_DIR = $(OUTPUT_DIR)/images

Expand Down Expand Up @@ -106,6 +109,7 @@ CNS_ARCHIVE_NAME = azure-cns-$(GOOS)-$(GOARCH)-$(CNS_VERSION).$(ARCHIVE_EXT)
NPM_ARCHIVE_NAME = azure-npm-$(GOOS)-$(GOARCH)-$(NPM_VERSION).$(ARCHIVE_EXT)
AZURE_IPAM_ARCHIVE_NAME = azure-ipam-$(GOOS)-$(GOARCH)-$(AZURE_IPAM_VERSION).$(ARCHIVE_EXT)
AZURE_IP_MASQ_MERGER_ARCHIVE_NAME = azure-ip-masq-merger-$(GOOS)-$(GOARCH)-$(AZURE_IP_MASQ_MERGER_VERSION).$(ARCHIVE_EXT)
AZURE_IPTABLES_MONITOR_ARCHIVE_NAME = azure-iptables-monitor-$(GOOS)-$(GOARCH)-$(AZURE_IPTABLES_MONITOR_VERSION).$(ARCHIVE_EXT)
IPV6_HP_BPF_ARCHIVE_NAME = ipv6-hp-bpf-$(GOOS)-$(GOARCH)-$(IPV6_HP_BPF_VERSION).$(ARCHIVE_EXT)

# Image info file names.
Expand All @@ -123,8 +127,8 @@ all-binaries-platforms: ## Make all platform binaries

# OS specific binaries/images
ifeq ($(GOOS),linux)
all-binaries: acncli azure-cni-plugin azure-cns azure-npm azure-ipam azure-ip-masq-merger ipv6-hp-bpf
all-images: npm-image cns-image cni-manager-image azure-ip-masq-merger-image ipv6-hp-bpf-image
all-binaries: acncli azure-cni-plugin azure-cns azure-npm azure-ipam azure-ip-masq-merger azure-iptables-monitor ipv6-hp-bpf
all-images: npm-image cns-image cni-manager-image azure-ip-masq-merger-image azure-iptables-monitor-image ipv6-hp-bpf-image
else
all-binaries: azure-cni-plugin azure-cns azure-npm
all-images:
Expand All @@ -139,6 +143,7 @@ azure-npm: azure-npm-binary npm-archive
azure-ipam: azure-ipam-binary azure-ipam-archive
ipv6-hp-bpf: ipv6-hp-bpf-binary ipv6-hp-bpf-archive
azure-ip-masq-merger: azure-ip-masq-merger-binary azure-ip-masq-merger-archive
azure-iptables-monitor: azure-iptables-monitor-binary azure-iptables-monitor-archive


##@ Versioning
Expand All @@ -157,6 +162,9 @@ azure-ipam-version: ## prints the azure-ipam version
azure-ip-masq-merger-version: ## prints the azure-ip-masq-merger version
@echo $(AZURE_IP_MASQ_MERGER_VERSION)

azure-iptables-monitor-version: ## prints the azure-iptables-monitor version
@echo $(AZURE_IPTABLES_MONITOR_VERSION)

ipv6-hp-bpf-version: ## prints the ipv6-hp-bpf version
@echo $(IPV6_HP_BPF_VERSION)

Expand Down Expand Up @@ -230,6 +238,10 @@ azure-npm-binary:
azure-ip-masq-merger-binary:
cd $(AZURE_IP_MASQ_MERGER_DIR) && CGO_ENABLED=0 go build -v -o $(AZURE_IP_MASQ_MERGER_BUILD_DIR)/azure-ip-masq-merger$(EXE_EXT) -ldflags "-X main.version=$(AZURE_IP_MASQ_MERGER_VERSION)" -gcflags="-dwarflocationlists=true"

# Build the azure-iptables-monitor binary.
azure-iptables-monitor-binary:
cd $(AZURE_IPTABLES_MONITOR_DIR) && CGO_ENABLED=0 go build -v -o $(AZURE_IPTABLES_MONITOR_BUILD_DIR)/azure-iptables-monitor$(EXE_EXT) -ldflags "-X main.version=$(AZURE_IPTABLES_MONITOR_VERSION)" -gcflags="-dwarflocationlists=true"

##@ Containers

## Common variables for all containers.
Expand Down Expand Up @@ -268,25 +280,27 @@ CONTAINER_TRANSPORT = docker
endif

## Image name definitions.
ACNCLI_IMAGE = acncli
AZURE_IPAM_IMAGE = azure-ipam
IPV6_HP_BPF_IMAGE = ipv6-hp-bpf
CNI_IMAGE = azure-cni
CNS_IMAGE = azure-cns
NPM_IMAGE = azure-npm
AZURE_IP_MASQ_MERGER_IMAGE = azure-ip-masq-merger
ACNCLI_IMAGE = acncli
AZURE_IPAM_IMAGE = azure-ipam
IPV6_HP_BPF_IMAGE = ipv6-hp-bpf
CNI_IMAGE = azure-cni
CNS_IMAGE = azure-cns
NPM_IMAGE = azure-npm
AZURE_IP_MASQ_MERGER_IMAGE = azure-ip-masq-merger
AZURE_IPTABLES_MONITOR_IMAGE = azure-iptables-monitor

## Image platform tags.
ACNCLI_PLATFORM_TAG ?= $(subst /,-,$(PLATFORM))-$(ACN_VERSION)
AZURE_IPAM_PLATFORM_TAG ?= $(subst /,-,$(PLATFORM))-$(AZURE_IPAM_VERSION)
AZURE_IPAM_WINDOWS_PLATFORM_TAG ?= $(subst /,-,$(PLATFORM))-$(AZURE_IPAM_VERSION)-$(OS_SKU_WIN)
IPV6_HP_BPF_IMAGE_PLATFORM_TAG ?= $(subst /,-,$(PLATFORM))-$(IPV6_HP_BPF_VERSION)
CNI_PLATFORM_TAG ?= $(subst /,-,$(PLATFORM))-$(CNI_VERSION)
CNI_WINDOWS_PLATFORM_TAG ?= $(subst /,-,$(PLATFORM))-$(CNI_VERSION)-$(OS_SKU_WIN)
CNS_PLATFORM_TAG ?= $(subst /,-,$(PLATFORM))-$(CNS_VERSION)
CNS_WINDOWS_PLATFORM_TAG ?= $(subst /,-,$(PLATFORM))-$(CNS_VERSION)-$(OS_SKU_WIN)
NPM_PLATFORM_TAG ?= $(subst /,-,$(PLATFORM))-$(NPM_VERSION)
ACNCLI_PLATFORM_TAG?= $(subst /,-,$(PLATFORM))-$(ACN_VERSION)
AZURE_IPAM_PLATFORM_TAG?= $(subst /,-,$(PLATFORM))-$(AZURE_IPAM_VERSION)
AZURE_IPAM_WINDOWS_PLATFORM_TAG?= $(subst /,-,$(PLATFORM))-$(AZURE_IPAM_VERSION)-$(OS_SKU_WIN)
IPV6_HP_BPF_IMAGE_PLATFORM_TAG?= $(subst /,-,$(PLATFORM))-$(IPV6_HP_BPF_VERSION)
CNI_PLATFORM_TAG?= $(subst /,-,$(PLATFORM))-$(CNI_VERSION)
CNI_WINDOWS_PLATFORM_TAG?= $(subst /,-,$(PLATFORM))-$(CNI_VERSION)-$(OS_SKU_WIN)
CNS_PLATFORM_TAG?= $(subst /,-,$(PLATFORM))-$(CNS_VERSION)
CNS_WINDOWS_PLATFORM_TAG?= $(subst /,-,$(PLATFORM))-$(CNS_VERSION)-$(OS_SKU_WIN)
NPM_PLATFORM_TAG?= $(subst /,-,$(PLATFORM))-$(NPM_VERSION)
AZURE_IP_MASQ_MERGER_PLATFORM_TAG ?= $(subst /,-,$(PLATFORM))-$(AZURE_IP_MASQ_MERGER_VERSION)
AZURE_IPTABLES_MONITOR_PLATFORM_TAG ?= $(subst /,-,$(PLATFORM))-$(AZURE_IPTABLES_MONITOR_VERSION)


qemu-user-static: ## Set up the host to run qemu multiplatform container builds.
Expand Down Expand Up @@ -424,6 +438,32 @@ azure-ip-masq-merger-image-pull: ## pull azure-ip-masq-merger container image.
IMAGE=$(AZURE_IP_MASQ_MERGER_IMAGE) \
TAG=$(AZURE_IP_MASQ_MERGER_PLATFORM_TAG)

# azure-iptables-monitor
azure-iptables-monitor-image-name: # util target to print the azure-iptables-monitor image name.
@echo $(AZURE_IPTABLES_MONITOR_IMAGE)

azure-iptables-monitor-image-name-and-tag: # util target to print the azure-iptables-monitor image name and tag.
@echo $(IMAGE_REGISTRY)/$(AZURE_IPTABLES_MONITOR_IMAGE):$(AZURE_IPTABLES_MONITOR_PLATFORM_TAG)

azure-iptables-monitor-image: ## build azure-iptables-monitor container image.
$(MAKE) container \
DOCKERFILE=azure-iptables-monitor/Dockerfile \
IMAGE=$(AZURE_IPTABLES_MONITOR_IMAGE) \
PLATFORM=$(PLATFORM) \
TAG=$(AZURE_IPTABLES_MONITOR_PLATFORM_TAG) \
TARGET=$(OS) \
OS=$(OS) \
ARCH=$(ARCH)

azure-iptables-monitor-image-push: ## push azure-iptables-monitor container image.
$(MAKE) container-push \
IMAGE=$(AZURE_IPTABLES_MONITOR_IMAGE) \
TAG=$(AZURE_IPTABLES_MONITOR_PLATFORM_TAG)

azure-iptables-monitor-image-pull: ## pull azure-iptables-monitor container image.
$(MAKE) container-pull \
IMAGE=$(AZURE_IPTABLES_MONITOR_IMAGE) \
TAG=$(AZURE_IPTABLES_MONITOR_PLATFORM_TAG)

# ipv6-hp-bpf

Expand Down Expand Up @@ -617,6 +657,22 @@ azure-ip-masq-merger-skopeo-archive: ## export tar archive of azure-ip-masq-merg
IMAGE=$(AZURE_IP_MASQ_MERGER_IMAGE) \
TAG=$(AZURE_IP_MASQ_MERGER_VERSION)

azure-iptables-monitor-manifest-build: ## build azure-iptables-monitor multiplat container manifest.
$(MAKE) manifest-build \
PLATFORMS="$(PLATFORMS)" \
IMAGE=$(AZURE_IPTABLES_MONITOR_IMAGE) \
TAG=$(AZURE_IPTABLES_MONITOR_VERSION)

azure-iptables-monitor-manifest-push: ## push azure-iptables-monitor multiplat container manifest
$(MAKE) manifest-push \
IMAGE=$(AZURE_IPTABLES_MONITOR_IMAGE) \
TAG=$(AZURE_IPTABLES_MONITOR_VERSION)

azure-iptables-monitor-skopeo-archive: ## export tar archive of azure-iptables-monitor multiplat container manifest.
$(MAKE) manifest-skopeo-archive \
IMAGE=$(AZURE_IPTABLES_MONITOR_IMAGE) \
TAG=$(AZURE_IPTABLES_MONITOR_VERSION)

ipv6-hp-bpf-manifest-build: ## build ipv6-hp-bpf multiplat container manifest.
$(MAKE) manifest-build \
PLATFORMS="$(PLATFORMS)" \
Expand Down Expand Up @@ -775,6 +831,14 @@ ifeq ($(GOOS),linux)
cd $(AZURE_IP_MASQ_MERGER_BUILD_DIR) && $(ARCHIVE_CMD) $(AZURE_IP_MASQ_MERGER_ARCHIVE_NAME) azure-ip-masq-merger$(EXE_EXT)
endif

# Create a azure-iptables-monitor archive for the target platform.
.PHONY: azure-iptables-monitor-archive
azure-iptables-monitor-archive: azure-iptables-monitor-binary
ifeq ($(GOOS),linux)
$(MKDIR) $(AZURE_IPTABLES_MONITOR_BUILD_DIR)
cd $(AZURE_IPTABLES_MONITOR_BUILD_DIR) && $(ARCHIVE_CMD) $(AZURE_IPTABLES_MONITOR_ARCHIVE_NAME) azure-iptables-monitor$(EXE_EXT)
endif

# Create a ipv6-hp-bpf archive for the target platform.
.PHONY: ipv6-hp-bpf-archive
ipv6-hp-bpf-archive: ipv6-hp-bpf-binary
Expand Down Expand Up @@ -811,6 +875,7 @@ workspace: ## Set up the Go workspace.
go work use .
go work use ./azure-ipam
go work use ./azure-ip-masq-merger
go work use ./azure-iptables-monitor
go work use ./build/tools
go work use ./dropgz
go work use ./zapai
Expand All @@ -823,7 +888,7 @@ RESTART_CASE ?= false
# CNI type is a key to direct the types of state validation done on a cluster.
CNI_TYPE ?= cilium

test-all: test-azure-ipam test-azure-ip-masq-merger test-main ## run all unit tests.
test-all: test-azure-ipam test-azure-ip-masq-merger test-azure-iptables-monitor test-main ## run all unit tests.

test-main:
go test -mod=readonly -buildvcs=false -tags "unit" --skip 'TestE2E*' -race -covermode atomic -coverprofile=coverage-main.out $(COVER_PKG)/...
Expand Down Expand Up @@ -863,6 +928,9 @@ test-azure-ipam: ## run the unit test for azure-ipam
test-azure-ip-masq-merger: ## run the unit test for azure-ip-masq-merger
cd $(AZURE_IP_MASQ_MERGER_DIR) && go test -race -covermode atomic -coverprofile=../coverage-azure-ip-masq-merger.out && go tool cover -func=../coverage-azure-ip-masq-merger.out

test-azure-iptables-monitor: ## run the unit test for azure-iptables-monitor
cd $(AZURE_IPTABLES_MONITOR_DIR) && go test -race -covermode atomic -coverprofile=../coverage-azure-iptables-monitor.out && go tool cover -func=../coverage-azure-iptables-monitor.out

kind:
kind create cluster --config ./test/kind/kind.yaml

Expand Down
28 changes: 28 additions & 0 deletions azure-iptables-monitor/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
ARG ARCH

# mcr.microsoft.com/azurelinux/base/core:3.0
FROM mcr.microsoft.com/azurelinux/base/core@sha256:9948138108a3d69f1dae62104599ac03132225c3b7a5ac57b85a214629c8567d AS mariner-core

# mcr.microsoft.com/azurelinux/distroless/minimal:3.0
FROM mcr.microsoft.com/azurelinux/distroless/minimal@sha256:0801b80a0927309572b9adc99bd1813bc680473175f6e8175cd4124d95dbd50c AS mariner-distroless

# skopeo inspect docker://mcr.microsoft.com/oss/go/microsoft/golang:1.23.2-azurelinux3.0 --format "{{.Name}}@{{.Digest}}"
FROM --platform=linux/${ARCH} mcr.microsoft.com/oss/go/microsoft/golang@sha256:f1f0cbd464ae4cd9d41176d47f1f9fe16a6965425871f817587314e3a04576ec AS go


FROM go AS azure-iptables-monitor
ARG OS
ARG VERSION
WORKDIR /azure-iptables-monitor
COPY ./azure-iptables-monitor .
RUN GOOS=$OS CGO_ENABLED=0 go build -a -o /go/bin/iptables-monitor -trimpath -ldflags "-X main.version="$VERSION"" -gcflags="-dwarflocationlists=true" .

FROM mariner-core AS iptables
RUN tdnf install -y iptables

FROM mariner-distroless AS linux
COPY --from=iptables /usr/sbin/*tables* /usr/sbin/
COPY --from=iptables /usr/lib /usr/lib
COPY --from=azure-iptables-monitor /go/bin/iptables-monitor azure-iptables-monitor

ENTRYPOINT ["/azure-iptables-monitor"]
64 changes: 64 additions & 0 deletions azure-iptables-monitor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# azure-iptables-monitor

`azure-iptables-monitor` is a utility for monitoring iptables rules on Kubernetes nodes and labeling a ciliumnode resource based on whether the corresponding node contains user-defined iptables rules.

## Description

The goal of this program is to periodically scan iptables rules across all tables (nat, mangle, filter, raw, security) and determine if any rules exist that don't match expected patterns. When unexpected rules are found, the ciliumnode resource is labeled to indicate the presence of user-defined iptables rules.

## Usage

Follow the steps below to build and run the program:

1. Build the binary using `make`:
```bash
make azure-iptables-monitor
```
or make an image:
```bash
make azure-iptables-monitor-image
```

2. Deploy or copy the binary to your node(s).

3. Prepare your allowed pattern files in the input directory. Each file should be named after an iptables table (`nat`, `mangle`, `filter`, `raw`, `security`) or `global` and contain regex patterns that match expected iptables rules. You may want to mount a configmap for this purpose.

4. Start the program with:
```bash
./azure-iptables-monitor --input=/etc/config/ --interval=300
```
- The `--input` flag specifies the directory containing allowed regex pattern files. Default: `/etc/config/`
- The `--interval` flag specifies how often to check iptables rules in seconds. Default: `300`
- The `--events` flag enables Kubernetes event creation for rule violations. Default: `false`
- The program must be in a k8s environment and `NODE_NAME` must be a set environment variable with the current node.

5. The program will set the `user-iptables-rules` label to `true` on the specified ciliumnode resource if unexpected rules are found, or `false` if all rules match expected patterns. Proper RBAC is required for patching (patch for ciliumnodes, create for events, get for nodes).


## Pattern File Format

Each pattern file should contain one regex pattern per line:
```
^-A INPUT -i lo -j ACCEPT$
^-A FORWARD -j DOCKER.*
^-A POSTROUTING -s 10\.0\.0\.0/8 -j MASQUERADE$
```

- `global`: Patterns that can match rules in any iptables table
- `nat`, `mangle`, `filter`, `raw`, `security`: Patterns specific to each iptables table
- Empty lines are ignored
- Each line should be a valid Go regex pattern

## Debugging

Logs are output to standard error. Increase verbosity with the `-v` flag:
```bash
./azure-iptables-monitor -v 3
```

## Development

To run tests at the repository level:
```bash
make test-azure-iptables-monitor
```
62 changes: 62 additions & 0 deletions azure-iptables-monitor/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module github.com/Azure/azure-container-networking/azure-iptables-monitor

go 1.23.0

require (
github.com/coreos/go-iptables v0.8.0
github.com/stretchr/testify v1.9.0
k8s.io/apimachinery v0.31.3
k8s.io/client-go v0.31.3
k8s.io/component-base v0.31.3
k8s.io/klog/v2 v2.130.1
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.31.3 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
Loading
Loading