Merge pull request #1563 from kernelkit/misc

Firewall address-set support
This commit is contained in:
Joachim Wiberg
2026-07-06 09:37:21 +02:00
committed by GitHub
95 changed files with 2725 additions and 525 deletions
+1 -1
View File
@@ -78,7 +78,7 @@ jobs:
- name: Configure ${{ matrix.defconfig }}_defconfig - name: Configure ${{ matrix.defconfig }}_defconfig
run: | run: |
make ${{ matrix.defconfig }}_defconfig make ${{ matrix.defconfig }}_defconfig apply-mirror
- name: Build ${{ matrix.defconfig }}_defconfig - name: Build ${{ matrix.defconfig }}_defconfig
run: | run: |
+1 -1
View File
@@ -173,7 +173,7 @@ jobs:
export BUILD_DIR=$PWD/build export BUILD_DIR=$PWD/build
export BR2_EXTERNAL_INFIX_PATH=$PWD export BR2_EXTERNAL_INFIX_PATH=$PWD
export RELEASE="" export RELEASE=""
export INFIX_ID="infix" export IX_ID="infix"
for target in $TARGETS; do for target in $TARGETS; do
export BINARIES_DIR=$PWD/output_${target}/images export BINARIES_DIR=$PWD/output_${target}/images
+25 -1
View File
@@ -54,7 +54,7 @@ jobs:
run: | run: |
target=${{ matrix.target }}_defconfig target=${{ matrix.target }}_defconfig
echo "Building $target ..." echo "Building $target ..."
make $target make $target apply-mirror
make make
- name: Generate SBOM from Build - name: Generate SBOM from Build
@@ -70,6 +70,25 @@ jobs:
run: | run: |
make test-spec make test-spec
# rootfs.squashfs is still present here; it is stripped from the
# tarball below. mkimage pulls the rpi64 bootloader from the
# latest-boot release and uses the freshly built host genimage.
- name: Build Raspberry Pi image
if: matrix.target == 'aarch64'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
./utils/mkimage.sh -d -r output raspberrypi-rpi64
mkdir -p rpi-image
mv output/images/*-rpi64-sdcard.img rpi-image/
xz -T0 rpi-image/*-rpi64-sdcard.img
- uses: actions/upload-artifact@v7
if: matrix.target == 'aarch64'
with:
name: artifact-rpi64-image
path: rpi-image/*.img.xz
- name: Prepare Artifacts - name: Prepare Artifacts
run: | run: |
cd output/ cd output/
@@ -98,3 +117,8 @@ jobs:
with: with:
name: artifact-disk-image-${{ matrix.target }} name: artifact-disk-image-${{ matrix.target }}
path: output/images/*.qcow2 path: output/images/*.qcow2
- uses: actions/upload-artifact@v7
with:
name: artifact-pkg-${{ matrix.target }}
path: output/images/*.pkg
+1 -1
View File
@@ -129,7 +129,7 @@ jobs:
- name: Configure ${{ env.TARGET }} - name: Configure ${{ env.TARGET }}
run: | run: |
make ${{ env.TARGET }}_defconfig make ${{ env.TARGET }}_defconfig apply-mirror
- name: Cleanup stale containers and ports - name: Cleanup stale containers and ports
run: | run: |
+1 -1
View File
@@ -95,7 +95,7 @@ jobs:
makeLatest: ${{ steps.rel.outputs.latest }} makeLatest: ${{ steps.rel.outputs.latest }}
discussionCategory: ${{ steps.rel.outputs.cat }} discussionCategory: ${{ steps.rel.outputs.cat }}
bodyFile: release.md bodyFile: release.md
artifacts: "*.tar.gz,*.qcow2" artifacts: "*.tar.gz,*.qcow2,*.pkg,*.img.xz"
- name: Summary - name: Summary
run: | run: |
+19 -19
View File
@@ -2,7 +2,7 @@ source "$BR2_EXTERNAL_INFIX_PATH/board/Config.in"
menu "Branding" menu "Branding"
config INFIX_VENDOR config IX_VENDOR
string "Vendor name" string "Vendor name"
default "KernelKit" default "KernelKit"
help help
@@ -15,7 +15,7 @@ config INFIX_VENDOR
Used for VENDOR_NAME in /etc/os-release and GNS3 appliance files. Used for VENDOR_NAME in /etc/os-release and GNS3 appliance files.
config INFIX_VENDOR_HOME config IX_VENDOR_HOME
string "Vendor URL" string "Vendor URL"
help help
The homepage of the OS vendor. The value should be in RFC3986 The homepage of the OS vendor. The value should be in RFC3986
@@ -24,14 +24,14 @@ config INFIX_VENDOR_HOME
Optional, used for VENDOR_HOME in /etc/os-release Optional, used for VENDOR_HOME in /etc/os-release
config INFIX_NAME config IX_NAME
string "Operating system name" string "Operating system name"
default "Infix" default "Infix"
help help
Mandatory. Used for identifying the OS as NAME in /etc/os-release Mandatory. Used for identifying the OS as NAME in /etc/os-release
and product_name in GNS3 appliance files. and product_name in GNS3 appliance files.
config INFIX_ID config IX_ID
string "Operating system identifier" string "Operating system identifier"
default "infix" default "infix"
help help
@@ -43,9 +43,9 @@ config INFIX_ID
Mandatory. Used for identifying the OS as ID in /etc/os-release and Mandatory. Used for identifying the OS as ID in /etc/os-release and
in the generated image name: ID-ARCH-VERSION.img in the generated image name: ID-ARCH-VERSION.img
config INFIX_IMAGE_ID config IX_IMAGE_ID
string "Operating system image name" string "Operating system image name"
default "${INFIX_ID}-${BR2_ARCH}" default "${IX_ID}-${BR2_ARCH}"
help help
A lower-case string (no spaces or other characters outside of 09, A lower-case string (no spaces or other characters outside of 09,
az, ".", "_" and "-"), for naming critical image files, directories az, ".", "_" and "-"), for naming critical image files, directories
@@ -54,18 +54,18 @@ config INFIX_IMAGE_ID
Mandatory. When INFIX_RELEASE is set, this string is appended to Mandatory. When INFIX_RELEASE is set, this string is appended to
the IMAGE_ID with a '-' separator. the IMAGE_ID with a '-' separator.
config INFIX_COMPATIBLE config IX_COMPATIBLE
string "Operating system compatible string" string "Operating system compatible string"
default "${INFIX_IMAGE_ID}" default "${IX_IMAGE_ID}"
help help
A lower-case string (no spaces or other characters outside of 09, A lower-case string (no spaces or other characters outside of 09,
az, ".", "_" and "-"), used for image identification at upgrade. az, ".", "_" and "-"), used for image identification at upgrade.
E.g., the RAUC [system] compatible string. E.g., the RAUC [system] compatible string.
Mandatory. Defaults to $INFIX_IMAGE_ID, which in turn is composed Mandatory. Defaults to $IX_IMAGE_ID, which in turn is composed
of $INFIX_ID-$BR2_ARCH. of $IX_ID-$BR2_ARCH.
config INFIX_TAGLINE config IX_TAGLINE
string "Operating system tagline" string "Operating system tagline"
default "Infix OS — Immutable.Friendly.Secure" default "Infix OS — Immutable.Friendly.Secure"
help help
@@ -74,25 +74,25 @@ config INFIX_TAGLINE
This is also show at boot when the system init process starts. This is also show at boot when the system init process starts.
config INFIX_DESC config IX_DESC
string "Operating system description" string "Operating system description"
help help
Optional. Used for long description texts about the OS. E.g., Optional. Used for long description texts about the OS. E.g.,
the GNS3 appliance file description field. Saved in the file the GNS3 appliance file description field. Saved in the file
/etc/os-release as INFIX_DESC. /etc/os-release as IX_DESC.
config INFIX_HOME config IX_HOME
string "Operating system URL" string "Operating system URL"
help help
Used for identifying the OS, e.g. as HOME_URL in /etc/os-release Used for identifying the OS, e.g. as HOME_URL in /etc/os-release
config INFIX_DOC config IX_DOC
string "Operating system docs" string "Operating system docs"
help help
Optional. Main documentation URL, will be shown in /etc/os-release Optional. Main documentation URL, will be shown in /etc/os-release
as DOCUMENTATION_URL. as DOCUMENTATION_URL.
config INFIX_SUPPORT config IX_SUPPORT
string "Operating system support" string "Operating system support"
help help
Main support page for the operating system, if there is any. This Main support page for the operating system, if there is any. This
@@ -102,13 +102,13 @@ config INFIX_SUPPORT
Optional. Shown, e.g., as SUPPORT_URL in /etc/os-release or Optional. Shown, e.g., as SUPPORT_URL in /etc/os-release or
maintainer_email in .gns3a. maintainer_email in .gns3a.
config INFIX_OEM_PATH config IX_OEM_PATH
string "Path to OEM br2-external" string "Path to OEM br2-external"
help help
A br2-external using Infix will likely want to version the branded A br2-external using Infix will likely want to version the branded
OS using their own GIT tags. Set this variable to point to the base OS using their own GIT tags. Set this variable to point to the base
directory (absolute path) and the Infix post-build.sh will call `git directory (absolute path) and the Infix post-build.sh will call `git
describe -C $INFIX_OEM_PATH`. describe -C $IX_OEM_PATH`.
Note: the OS version (VERSION, VERSION_ID, BUILD_ID in Note: the OS version (VERSION, VERSION_ID, BUILD_ID in
/etc/os-release) is always derived from `git describe`. The global /etc/os-release) is always derived from `git describe`. The global
@@ -118,7 +118,7 @@ config INFIX_OEM_PATH
endmenu endmenu
# For /etc/os-release, uses CondtionArchitechture= from systemd.unit(5) # For /etc/os-release, uses CondtionArchitechture= from systemd.unit(5)
config INFIX_ARCH config IX_ARCH
string string
default "arm" if BR2_arm default "arm" if BR2_arm
default "arm64" if BR2_aarch64 default "arm64" if BR2_aarch64
@@ -22,7 +22,7 @@ image var.ext4 {
} }
} }
image #INFIX_ID##VERSION#-vero-w-#TARGET#.img { image #IX_ID##VERSION#-vero-w-#TARGET#.img {
hdimage { hdimage {
partition-table-type = "gpt" partition-table-type = "gpt"
gpt-no-backup = true gpt-no-backup = true
@@ -24,7 +24,7 @@ image var.ext4 {
} }
} }
image #INFIX_ID##VERSION#-bpi-r3-#TARGET#.img { image #IX_ID##VERSION#-bpi-r3-#TARGET#.img {
hdimage { hdimage {
partition-table-type = "gpt" partition-table-type = "gpt"
@@ -24,7 +24,7 @@ image var.ext4 {
} }
} }
image #INFIX_ID##VERSION#-bpi-r4-#TARGET#.img { image #IX_ID##VERSION#-bpi-r4-#TARGET#.img {
hdimage { hdimage {
partition-table-type = "gpt" partition-table-type = "gpt"
gpt-no-backup = true gpt-no-backup = true
@@ -24,7 +24,7 @@ image var.ext4 {
} }
} }
image #INFIX_ID##VERSION#-bpi-r64-#TARGET#.img { image #IX_ID##VERSION#-bpi-r64-#TARGET#.img {
hdimage { hdimage {
partition-table-type = "hybrid" partition-table-type = "hybrid"
# MT7622 TF-A partition driver detects GPT by checking MBR[0] # MT7622 TF-A partition driver detects GPT by checking MBR[0]
@@ -26,7 +26,7 @@ image var.ext4 {
} }
} }
image #INFIX_ID##VERSION#-nanopi-r2s-sdcard.img { image #IX_ID##VERSION#-nanopi-r2s-sdcard.img {
hdimage { hdimage {
partition-table-type = "gpt" partition-table-type = "gpt"
} }
@@ -24,7 +24,7 @@ image var.ext4 {
} }
} }
image #INFIX_ID##VERSION#-espressobin-#TARGET#.img { image #IX_ID##VERSION#-espressobin-#TARGET#.img {
hdimage { hdimage {
partition-table-type = "gpt" partition-table-type = "gpt"
} }
@@ -36,7 +36,7 @@ image var.ext4 {
} }
} }
image #INFIX_ID##VERSION#-rpi64-sdcard.img { image #IX_ID##VERSION#-rpi64-sdcard.img {
hdimage { hdimage {
partition-table-type = "hybrid" partition-table-type = "hybrid"
} }
@@ -39,7 +39,7 @@ image var.ext4 {
} }
} }
image #INFIX_ID##VERSION#-sama7g54-ek-#TARGET#.img { image #IX_ID##VERSION#-sama7g54-ek-#TARGET#.img {
hdimage { hdimage {
partition-table-type = "hybrid" partition-table-type = "hybrid"
} }
+1 -1
View File
@@ -36,7 +36,7 @@ image var.ext4 {
} }
} }
image #INFIX_ID##VERSION#-rpi2-sdcard.img { image #IX_ID##VERSION#-rpi2-sdcard.img {
hdimage { hdimage {
partition-table-type = "hybrid" partition-table-type = "hybrid"
} }
+8 -8
View File
@@ -11,7 +11,7 @@ source "$BR2_EXTERNAL_INFIX_PATH/board/common/image/image-readme/Config.in"
endmenu endmenu
config QEMU_SCRIPTS config IX_QEMU_SCRIPTS
bool "QEMU scripts" bool "QEMU scripts"
default y default y
help help
@@ -19,20 +19,20 @@ config QEMU_SCRIPTS
directory, which can be used to launch virtual Infix directory, which can be used to launch virtual Infix
instances. instances.
menuconfig TRUSTED_KEYS menuconfig IX_TRUSTED_KEYS
bool "Trusted keys for image" bool "Trusted keys for image"
help help
Keys that will be accepted for this image Keys that will be accepted for this image
config TRUSTED_KEYS_DEVELOPMENT config IX_TRUSTED_KEYS_DEVELOPMENT
bool "Development key" bool "Development key"
depends on TRUSTED_KEYS depends on IX_TRUSTED_KEYS
config TRUSTED_KEYS_DEVELOPMENT_PATH config IX_TRUSTED_KEYS_DEVELOPMENT_PATH
string string
depends on TRUSTED_KEYS_DEVELOPMENT depends on IX_TRUSTED_KEYS_DEVELOPMENT
default "${BR2_EXTERNAL_INFIX_PATH}/board/common/signing-keys/development/infix.crt" default "${BR2_EXTERNAL_INFIX_PATH}/board/common/signing-keys/development/infix.crt"
config TRUSTED_KEYS_EXTRA_PATH config IX_TRUSTED_KEYS_EXTRA_PATH
string "Path to extra keys to include in image" string "Path to extra keys to include in image"
depends on TRUSTED_KEYS depends on IX_TRUSTED_KEYS
+3 -3
View File
@@ -1,14 +1,14 @@
include $(BR2_EXTERNAL_INFIX_PATH)/board/common/image/image.mk include $(BR2_EXTERNAL_INFIX_PATH)/board/common/image/image.mk
include $(BR2_EXTERNAL_INFIX_PATH)/board/common/qemu/qemu.mk include $(BR2_EXTERNAL_INFIX_PATH)/board/common/qemu/qemu.mk
ifeq ($(TRUSTED_KEYS),y) ifeq ($(IX_TRUSTED_KEYS),y)
include $(BR2_EXTERNAL_INFIX_PATH)/board/common/uboot/uboot.mk include $(BR2_EXTERNAL_INFIX_PATH)/board/common/uboot/uboot.mk
TRUSTED_KEYS=$(TRUSTED_KEYS_DEVELOPMENT_PATH) $(TRUSTED_KEYS_EXTRA_PATH) IX_TRUSTED_KEYS=$(IX_TRUSTED_KEYS_DEVELOPMENT_PATH) $(IX_TRUSTED_KEYS_EXTRA_PATH)
define RAUC_POST_BUILD_INSTALL_CERT define RAUC_POST_BUILD_INSTALL_CERT
@$(call IXMSG,"Installing signing cert for RAUC") @$(call IXMSG,"Installing signing cert for RAUC")
mkdir -p $(TARGET_DIR)/etc/rauc/keys mkdir -p $(TARGET_DIR)/etc/rauc/keys
$(foreach crt,$(shell ls $(TRUSTED_KEYS)), \ $(foreach crt,$(shell ls $(IX_TRUSTED_KEYS)), \
cp $(crt) $(TARGET_DIR)/etc/rauc/keys/$(shell openssl x509 -hash -noout <$(crt)).0;) cp $(crt) $(TARGET_DIR)/etc/rauc/keys/$(shell openssl x509 -hash -noout <$(crt)).0;)
endef endef
+5 -5
View File
@@ -1,4 +1,4 @@
config IMAGE_EXT4_RAUC config IX_IMAGE_EXT4_RAUC
bool "RAUC upgrade bundle (ext4)" bool "RAUC upgrade bundle (ext4)"
depends on BR2_TARGET_ROOTFS_EXT2 depends on BR2_TARGET_ROOTFS_EXT2
select BR2_PACKAGE_HOST_RAUC select BR2_PACKAGE_HOST_RAUC
@@ -7,17 +7,17 @@ config IMAGE_EXT4_RAUC
image. Intended for development boards whose bootloader does image. Intended for development boards whose bootloader does
not support squashfs. not support squashfs.
config IMAGE_EXT4_RAUC_KEY config IX_IMAGE_EXT4_RAUC_KEY
string "signing key" string "signing key"
depends on IMAGE_EXT4_RAUC depends on IX_IMAGE_EXT4_RAUC
default "${BR2_EXTERNAL_INFIX_PATH}/board/common/signing-keys/development/infix.key" default "${BR2_EXTERNAL_INFIX_PATH}/board/common/signing-keys/development/infix.key"
help help
Path to the private key, in PKCS#8 format, used to sign Path to the private key, in PKCS#8 format, used to sign
the RAUC bundle; or a PKCS#11 URI. the RAUC bundle; or a PKCS#11 URI.
config IMAGE_EXT4_RAUC_CERT config IX_IMAGE_EXT4_RAUC_CERT
string "signing certificate" string "signing certificate"
depends on IMAGE_EXT4_RAUC depends on IX_IMAGE_EXT4_RAUC
default "${BR2_EXTERNAL_INFIX_PATH}/board/common/signing-keys/development/infix.crt" default "${BR2_EXTERNAL_INFIX_PATH}/board/common/signing-keys/development/infix.crt"
help help
Path to the X509 certificate which will be associated with Path to the X509 certificate which will be associated with
+2 -2
View File
@@ -1,6 +1,6 @@
config IMAGE_ITB_AUX config IX_IMAGE_ITB_AUX
bool "aux partition" bool "aux partition"
depends on IMAGE_ITB_ROOTFS depends on IX_IMAGE_ITB_ROOTFS
select BR2_PACKAGE_HOST_UBOOT_TOOLS select BR2_PACKAGE_HOST_UBOOT_TOOLS
select BR2_PACKAGE_HOST_GENIMAGE select BR2_PACKAGE_HOST_GENIMAGE
help help
@@ -1,4 +1,4 @@
menuconfig IMAGE_ITB_DL_RELEASE menuconfig IX_IMAGE_ITB_DL_RELEASE
bool "Download existing release" bool "Download existing release"
depends on !BR2_TARGET_ROOTFS_SQUASHFS depends on !BR2_TARGET_ROOTFS_SQUASHFS
help help
@@ -7,9 +7,9 @@ menuconfig IMAGE_ITB_DL_RELEASE
Infix image, to create a full disk image that can be Infix image, to create a full disk image that can be
provisioned to an SD-card or eMMC. provisioned to an SD-card or eMMC.
config IMAGE_ITB_DL_RELEASE_URL config IX_IMAGE_ITB_DL_RELEASE_URL
string "URL" string "URL"
depends on IMAGE_ITB_DL_RELEASE depends on IX_IMAGE_ITB_DL_RELEASE
default "https://github.com/kernelkit/infix/releases/download/latest/infix-${BR2_ARCH}.tar.gz" default "https://github.com/kernelkit/infix/releases/download/latest/infix-${BR2_ARCH}.tar.gz"
help help
URL to release tarball. URL to release tarball.
+6 -6
View File
@@ -1,23 +1,23 @@
menuconfig IMAGE_ITB_GNS3A menuconfig IX_IMAGE_ITB_GNS3A
bool "GNS3 Appliance (ITB)" bool "GNS3 Appliance (ITB)"
depends on BR2_x86_64 depends on BR2_x86_64
select IMAGE_ITB_QCOW select IX_IMAGE_ITB_QCOW
help help
Create a GNS3 appliance description that, together with the Create a GNS3 appliance description that, together with the
disk image, can be imported into GNS3. disk image, can be imported into GNS3.
config IMAGE_ITB_GNS3A_RAM config IX_IMAGE_ITB_GNS3A_RAM
int "Reserved RAM (MiB)" int "Reserved RAM (MiB)"
depends on IMAGE_ITB_GNS3A depends on IX_IMAGE_ITB_GNS3A
default "192" default "192"
help help
Amount of host RAM reserved for an appliance instance. Amount of host RAM reserved for an appliance instance.
Minimum supported size is 192M. Minimum supported size is 192M.
config IMAGE_ITB_GNS3A_IFNUM config IX_IMAGE_ITB_GNS3A_IFNUM
int "Number of interfaces" int "Number of interfaces"
depends on IMAGE_ITB_GNS3A depends on IX_IMAGE_ITB_GNS3A
default "1" default "1"
help help
Number of Ethernet interfaces to create for an appliance instance. Number of Ethernet interfaces to create for an appliance instance.
+16 -16
View File
@@ -1,6 +1,6 @@
menuconfig IMAGE_ITB_QCOW menuconfig IX_IMAGE_ITB_QCOW
bool "QEMU disk image (ITB)" bool "QEMU disk image (ITB)"
depends on (IMAGE_ITB_ROOTFS && IMAGE_ITB_AUX) || IMAGE_ITB_DL_RELEASE depends on (IX_IMAGE_ITB_ROOTFS && IX_IMAGE_ITB_AUX) || IX_IMAGE_ITB_DL_RELEASE
select BR2_PACKAGE_HOST_GENIMAGE select BR2_PACKAGE_HOST_GENIMAGE
help help
Compose a full disk image with redundant Linux OS partitions, Compose a full disk image with redundant Linux OS partitions,
@@ -11,9 +11,9 @@ menuconfig IMAGE_ITB_QCOW
- Creating a GNS3 appliance - Creating a GNS3 appliance
- Developing/debugging issues in the boot process in QEMU - Developing/debugging issues in the boot process in QEMU
menuconfig IMAGE_ITB_QCOW_SIZE menuconfig IX_IMAGE_ITB_QCOW_SIZE
string "Image size" string "Image size"
depends on IMAGE_ITB_QCOW depends on IX_IMAGE_ITB_QCOW
default "512M" default "512M"
help help
Create a disk image of this size. A K/M/G suffix may be used Create a disk image of this size. A K/M/G suffix may be used
@@ -26,22 +26,22 @@ menuconfig IMAGE_ITB_QCOW_SIZE
choice choice
prompt "Bootloader" prompt "Bootloader"
depends on IMAGE_ITB_QCOW depends on IX_IMAGE_ITB_QCOW
default IMAGE_ITB_QCOW_BOOT_EFI if BR2_x86_64 default IX_IMAGE_ITB_QCOW_BOOT_EFI if BR2_x86_64
default IMAGE_ITB_QCOW_BOOT_NONE default IX_IMAGE_ITB_QCOW_BOOT_NONE
config IMAGE_ITB_QCOW_BOOT_NONE config IX_IMAGE_ITB_QCOW_BOOT_NONE
bool "None" bool "None"
help help
Do not create any bootloader partition in the disk image. Do not create any bootloader partition in the disk image.
config IMAGE_ITB_QCOW_BOOT_EFI config IX_IMAGE_ITB_QCOW_BOOT_EFI
bool "EFI" bool "EFI"
help help
Create a boot partition from a directory containing an EFI Create a boot partition from a directory containing an EFI
boot application, e.g. GRUB. boot application, e.g. GRUB.
config IMAGE_ITB_QCOW_BOOT_BIN config IX_IMAGE_ITB_QCOW_BOOT_BIN
bool "Binary" bool "Binary"
help help
Create a boot partition from a raw image containing the boot Create a boot partition from a raw image containing the boot
@@ -49,18 +49,18 @@ config IMAGE_ITB_QCOW_BOOT_BIN
endchoice endchoice
config IMAGE_ITB_QCOW_BOOT_DATA config IX_IMAGE_ITB_QCOW_BOOT_DATA
string "Bootloader data" string "Bootloader data"
depends on IMAGE_ITB_QCOW depends on IX_IMAGE_ITB_QCOW
depends on IMAGE_ITB_QCOW_BOOT_EFI || IMAGE_ITB_QCOW_BOOT_BIN depends on IX_IMAGE_ITB_QCOW_BOOT_EFI || IX_IMAGE_ITB_QCOW_BOOT_BIN
default "${BINARIES_DIR}/efi-part/EFI" if BR2_x86_64 default "${BINARIES_DIR}/efi-part/EFI" if BR2_x86_64
help help
Path to the directory or file holding the bootloader data. Path to the directory or file holding the bootloader data.
config IMAGE_ITB_QCOW_BOOT_OFFSET config IX_IMAGE_ITB_QCOW_BOOT_OFFSET
hex "Bootloader offset" hex "Bootloader offset"
depends on IMAGE_ITB_QCOW depends on IX_IMAGE_ITB_QCOW
depends on IMAGE_ITB_QCOW_BOOT_EFI || IMAGE_ITB_QCOW_BOOT_BIN depends on IX_IMAGE_ITB_QCOW_BOOT_EFI || IX_IMAGE_ITB_QCOW_BOOT_BIN
default 0x8000 default 0x8000
help help
Offset at which the bootloader partition is placed. Remember Offset at which the bootloader partition is placed. Remember
@@ -6,8 +6,8 @@
# We can source the rootfs+aux from a local build, or from a # We can source the rootfs+aux from a local build, or from a
# downloaded release; so adjust our dependencies accordingly. # downloaded release; so adjust our dependencies accordingly.
IMAGE_ITB_QCOW_SRC-$(IMAGE_ITB_ROOTFS) := image-itb-rootfs image-itb-aux IMAGE_ITB_QCOW_SRC-$(IX_IMAGE_ITB_ROOTFS) := image-itb-rootfs image-itb-aux
IMAGE_ITB_QCOW_SRC-$(IMAGE_ITB_DL_RELEASE) := image-itb-dl-release IMAGE_ITB_QCOW_SRC-$(IX_IMAGE_ITB_DL_RELEASE) := image-itb-dl-release
IMAGE_ITB_QCOW_DEPENDENCIES := host-genimage $(IMAGE_ITB_QCOW_SRC-y) IMAGE_ITB_QCOW_DEPENDENCIES := host-genimage $(IMAGE_ITB_QCOW_SRC-y)
IMAGE_ITB_QCOW_CONFIG_VARS := BOOT_DATA BOOT_OFFSET SIZE IMAGE_ITB_QCOW_CONFIG_VARS := BOOT_DATA BOOT_OFFSET SIZE
+6 -6
View File
@@ -1,23 +1,23 @@
menuconfig IMAGE_ITB_RAUC menuconfig IX_IMAGE_ITB_RAUC
bool "RAUC upgrade bundle (ITB)" bool "RAUC upgrade bundle (ITB)"
select IMAGE_ITB_ROOTFS select IX_IMAGE_ITB_ROOTFS
select BR2_PACKAGE_HOST_RAUC select BR2_PACKAGE_HOST_RAUC
help help
Create RAUC upgrade bundle, for targets using ITB images, Create RAUC upgrade bundle, for targets using ITB images,
that can be used to upgrade a running system to this version that can be used to upgrade a running system to this version
of Infix. of Infix.
config IMAGE_ITB_RAUC_KEY config IX_IMAGE_ITB_RAUC_KEY
string "signing key" string "signing key"
depends on IMAGE_ITB_RAUC depends on IX_IMAGE_ITB_RAUC
default "${BR2_EXTERNAL_INFIX_PATH}/board/common/signing-keys/development/infix.key" default "${BR2_EXTERNAL_INFIX_PATH}/board/common/signing-keys/development/infix.key"
help help
Path to the private key, in PKCS#8 format, used to sign Path to the private key, in PKCS#8 format, used to sign
the RAUC bundle; or a PKCS#11 URI. the RAUC bundle; or a PKCS#11 URI.
config IMAGE_ITB_RAUC_CERT config IX_IMAGE_ITB_RAUC_CERT
string "signing certificate" string "signing certificate"
depends on IMAGE_ITB_RAUC depends on IX_IMAGE_ITB_RAUC
default "${BR2_EXTERNAL_INFIX_PATH}/board/common/signing-keys/development/infix.crt" default "${BR2_EXTERNAL_INFIX_PATH}/board/common/signing-keys/development/infix.crt"
help help
Path to the X509 certificate which will be associated with Path to the X509 certificate which will be associated with
@@ -1,4 +1,4 @@
menuconfig IMAGE_ITB_ROOTFS menuconfig IX_IMAGE_ITB_ROOTFS
bool "rootfs.itb+.itbh" bool "rootfs.itb+.itbh"
select BR2_TARGET_ROOTFS_SQUASHFS select BR2_TARGET_ROOTFS_SQUASHFS
select BR2_PACKAGE_HOST_UBOOT_TOOLS select BR2_PACKAGE_HOST_UBOOT_TOOLS
@@ -7,9 +7,9 @@ menuconfig IMAGE_ITB_ROOTFS
and extract detached header (.itbh) for U-Boot based and extract detached header (.itbh) for U-Boot based
targets. targets.
config IMAGE_ITB_ROOTFS_KEY config IX_IMAGE_ITB_ROOTFS_KEY
string "signing key" string "signing key"
depends on IMAGE_ITB_ROOTFS depends on IX_IMAGE_ITB_ROOTFS
default "${BR2_EXTERNAL_INFIX_PATH}/board/common/signing-keys/development/infix.key" default "${BR2_EXTERNAL_INFIX_PATH}/board/common/signing-keys/development/infix.key"
help help
Path to the private RSA key, in PKCS#8 format, used to sign Path to the private RSA key, in PKCS#8 format, used to sign
+1 -1
View File
@@ -1,4 +1,4 @@
config IMAGE_README config IX_IMAGE_README
bool "Install README.md in images" bool "Install README.md in images"
help help
Install a README.md with useful information about getting Install a README.md with useful information about getting
@@ -13,6 +13,6 @@ $(BINARIES_DIR)/README.md: $(IMAGE_README_DIR)/README.md
@mkdir -p $(BINARIES_DIR) @mkdir -p $(BINARIES_DIR)
@cp $< $@ @cp $< $@
ifeq ($(IMAGE_README),y) ifeq ($(IX_IMAGE_README),y)
TARGETS_ROOTFS += image-readme TARGETS_ROOTFS += image-readme
endif endif
+6 -4
View File
@@ -12,15 +12,17 @@ $(1): $$($(2)_DEPENDENCIES)
BINARIES_DIR=$$(BINARIES_DIR) \ BINARIES_DIR=$$(BINARIES_DIR) \
BR2_EXTERNAL_INFIX_PATH=$$(BR2_EXTERNAL_INFIX_PATH) \ BR2_EXTERNAL_INFIX_PATH=$$(BR2_EXTERNAL_INFIX_PATH) \
ARTIFACT=$$(INFIX_ARTIFACT) \ ARTIFACT=$$(INFIX_ARTIFACT) \
COMPATIBLE=$$(INFIX_COMPATIBLE) \ COMPATIBLE=$$(IX_COMPATIBLE) \
VERSION=$$(INFIX_VERSION) \ VERSION=$$(INFIX_VERSION) \
$$(foreach var,$$($(2)_CONFIG_VARS),$$(var)=$$($(2)_$$(var)) ) \ $$(foreach var,$$($(2)_CONFIG_VARS),$$(var)=$$($(3)_$$(var)) ) \
$$($(2)_DIR)/generate.sh $$($(2)_OPTS) $$($(2)_DIR)/generate.sh $$($(2)_OPTS)
ifeq ($$($(2)),y) ifeq ($$($(3)),y)
TARGETS_ROOTFS += $(1) TARGETS_ROOTFS += $(1)
endif endif
endef endef
ix-image = $(call inner-ix-image,$(pkgname),$(call UPPERCASE,$(pkgname))) # $(2) is the package-local make-var prefix (UPPERCASE pkgname); $(3) is
# the IX_ prefixed Kconfig symbol name, see issue #1305.
ix-image = $(call inner-ix-image,$(pkgname),$(call UPPERCASE,$(pkgname)),IX_$(call UPPERCASE,$(pkgname)))
+1 -1
View File
@@ -16,7 +16,7 @@ die()
# DISK_IMAGE_SIZE="512" # DISK_IMAGE_SIZE="512"
# etc. # etc.
# #
# Nested variables, like INFIX_COMPATIBLE="${INFIX_IMAGE_ID}" # Nested variables, like IX_COMPATIBLE="${IX_IMAGE_ID}"
# are handled by sourcing the file in a subshell. # are handled by sourcing the file in a subshell.
# #
# shellcheck disable=SC1090 # shellcheck disable=SC1090
+19 -19
View File
@@ -37,7 +37,7 @@ EOF
} }
if [ -f "$TARGET_DIR/etc/rauc/system.conf" ]; then if [ -f "$TARGET_DIR/etc/rauc/system.conf" ]; then
sed -i "s/compatible=.*/compatible=$INFIX_COMPATIBLE/" "$TARGET_DIR/etc/rauc/system.conf" sed -i "s/compatible=.*/compatible=$IX_COMPATIBLE/" "$TARGET_DIR/etc/rauc/system.conf"
fi fi
if [ -n "${ID_LIKE}" ]; then if [ -n "${ID_LIKE}" ]; then
@@ -53,40 +53,40 @@ cp "$TARGET_DIR/etc/hostname" "$TARGET_DIR/etc/hostname.d/10-default"
ixmsg "Creating /etc/os-release" ixmsg "Creating /etc/os-release"
rm -f "$TARGET_DIR/etc/os-release" rm -f "$TARGET_DIR/etc/os-release"
{ {
echo "NAME=\"$INFIX_NAME\"" echo "NAME=\"$IX_NAME\""
echo "ID=$INFIX_ID" echo "ID=$IX_ID"
echo "PRETTY_NAME=\"$INFIX_TAGLINE $INFIX_VERSION\"" echo "PRETTY_NAME=\"$IX_TAGLINE $INFIX_VERSION\""
echo "ID_LIKE=\"${ID}\"" echo "ID_LIKE=\"${ID}\""
echo "DEFAULT_HOSTNAME=$BR2_TARGET_GENERIC_HOSTNAME" echo "DEFAULT_HOSTNAME=$BR2_TARGET_GENERIC_HOSTNAME"
echo "VERSION=\"${INFIX_VERSION}\"" echo "VERSION=\"${INFIX_VERSION}\""
echo "VERSION_ID=${INFIX_VERSION}" echo "VERSION_ID=${INFIX_VERSION}"
echo "BUILD_ID=\"${INFIX_BUILD_ID}\"" echo "BUILD_ID=\"${INFIX_BUILD_ID}\""
if [ -n "$INFIX_IMAGE_ID" ]; then if [ -n "$IX_IMAGE_ID" ]; then
echo "IMAGE_ID=\"$INFIX_IMAGE_ID\"" echo "IMAGE_ID=\"$IX_IMAGE_ID\""
fi fi
if [ -n "$INFIX_RELEASE" ]; then if [ -n "$INFIX_RELEASE" ]; then
echo "IMAGE_VERSION=\"$INFIX_RELEASE\"" echo "IMAGE_VERSION=\"$INFIX_RELEASE\""
fi fi
echo "ARCHITECTURE=\"${INFIX_ARCH}\"" echo "ARCHITECTURE=\"${IX_ARCH}\""
echo "HOME_URL=$INFIX_HOME" echo "HOME_URL=$IX_HOME"
if [ -n "$INFIX_VENDOR" ]; then if [ -n "$IX_VENDOR" ]; then
echo "VENDOR_NAME=\"$INFIX_VENDOR\"" echo "VENDOR_NAME=\"$IX_VENDOR\""
fi fi
if [ -n "$INFIX_VENDOR_HOME" ]; then if [ -n "$IX_VENDOR_HOME" ]; then
echo "VENDOR_HOME=\"$INFIX_VENDOR_HOME\"" echo "VENDOR_HOME=\"$IX_VENDOR_HOME\""
fi fi
if [ -n "$INFIX_DOC" ]; then if [ -n "$IX_DOC" ]; then
echo "DOCUMENTATION_URL=\"$INFIX_DOC\"" echo "DOCUMENTATION_URL=\"$IX_DOC\""
fi fi
if [ -n "$INFIX_SUPPORT" ]; then if [ -n "$IX_SUPPORT" ]; then
echo "SUPPORT_URL=\"$INFIX_SUPPORT\"" echo "SUPPORT_URL=\"$IX_SUPPORT\""
fi fi
if [ -n "$INFIX_DESC" ]; then if [ -n "$IX_DESC" ]; then
echo "INFIX_DESC=\"$INFIX_DESC\"" echo "INFIX_DESC=\"$IX_DESC\""
fi fi
} > "$TARGET_DIR/etc/os-release" } > "$TARGET_DIR/etc/os-release"
echo "$INFIX_TAGLINE $INFIX_VERSION -- $(date +"%b %e %H:%M %Z %Y")" > "$TARGET_DIR/etc/version" echo "$IX_TAGLINE $INFIX_VERSION -- $(date +"%b %e %H:%M %Z %Y")" > "$TARGET_DIR/etc/version"
ixmsg "Creating /etc/version: $(cat "$TARGET_DIR/etc/version")" ixmsg "Creating /etc/version: $(cat "$TARGET_DIR/etc/version")"
# In case of ambguities, this is what the image was built from # In case of ambguities, this is what the image was built from
+92 -92
View File
@@ -1,85 +1,85 @@
mainmenu "QEMU Virtualization" mainmenu "QEMU Virtualization"
config QEMU_ARCH_IS_32 config IX_QEMU_ARCH_IS_32
bool bool
config QEMU_ARCH_IS_64 config IX_QEMU_ARCH_IS_64
bool bool
choice choice
prompt "Target Architecture" prompt "Target Architecture"
default @ARCH@ default @ARCH@
config QEMU_riscv64 config IX_QEMU_riscv64
bool "risv64" bool "riscv64"
select QEMU_ARCH_IS_64 select IX_QEMU_ARCH_IS_64
config QEMU_x86_64 config IX_QEMU_x86_64
bool "x86_64" bool "x86_64"
select QEMU_ARCH_IS_64 select IX_QEMU_ARCH_IS_64
config QEMU_arm config IX_QEMU_arm
bool "AArch32 (little endian)" bool "AArch32 (little endian)"
select QEMU_ARCH_IS_32 select IX_QEMU_ARCH_IS_32
config QEMU_aarch64 config IX_QEMU_aarch64
bool "AArch64 (little endian)" bool "AArch64 (little endian)"
select QEMU_ARCH_IS_64 select IX_QEMU_ARCH_IS_64
endchoice endchoice
choice choice
prompt "Loader" prompt "Loader"
default QEMU_LOADER_KERNEL default IX_QEMU_LOADER_KERNEL
config QEMU_LOADER_KERNEL config IX_QEMU_LOADER_KERNEL
bool "Kernel" bool "Kernel"
config QEMU_LOADER_UBOOT config IX_QEMU_LOADER_UBOOT
bool "U-Boot" bool "U-Boot"
depends on QEMU_aarch64 depends on IX_QEMU_aarch64
config QEMU_LOADER_OVMF config IX_QEMU_LOADER_OVMF
bool "OVMF (UEFI)" bool "OVMF (UEFI)"
depends on QEMU_x86_64 depends on IX_QEMU_x86_64
endchoice endchoice
choice choice
prompt "Rootfs type" prompt "Rootfs type"
default QEMU_ROOTFS_INITRD default IX_QEMU_ROOTFS_INITRD
config QEMU_ROOTFS_MMC config IX_QEMU_ROOTFS_MMC
bool "MMC" bool "MMC"
depends on QEMU_aarch64 depends on IX_QEMU_aarch64
config QEMU_ROOTFS_INITRD config IX_QEMU_ROOTFS_INITRD
bool "Initrd" bool "Initrd"
depends on QEMU_LOADER_KERNEL depends on IX_QEMU_LOADER_KERNEL
config QEMU_ROOTFS_VSCSI config IX_QEMU_ROOTFS_VSCSI
bool "Virtio SCSI" bool "Virtio SCSI"
endchoice endchoice
choice choice
prompt "Console" prompt "Console"
default QEMU_CONSOLE_SERIAL if QEMU_arm default IX_QEMU_CONSOLE_SERIAL if IX_QEMU_arm
default QEMU_CONSOLE_VIRTIO default IX_QEMU_CONSOLE_VIRTIO
config QEMU_CONSOLE_VIRTIO config IX_QEMU_CONSOLE_VIRTIO
bool "Virtio (hvc0)" bool "Virtio (hvc0)"
config QEMU_CONSOLE_SERIAL config IX_QEMU_CONSOLE_SERIAL
bool "Serial (ttyS0/ttyAMA0)" bool "Serial (ttyS0/ttyAMA0)"
depends on !QEMU_LOADER_OVMF depends on !IX_QEMU_LOADER_OVMF
endchoice endchoice
config QEMU_MACHINE config IX_QEMU_MACHINE
string "Select emulated machine" string "Select emulated machine"
default "qemu-system-arm -M virt,accel=kvm:tcg -cpu max" if QEMU_arm default "qemu-system-arm -M virt,accel=kvm:tcg -cpu max" if IX_QEMU_arm
default "qemu-system-aarch64 -M virt,accel=kvm:tcg -cpu cortex-a53" if QEMU_aarch64 default "qemu-system-aarch64 -M virt,accel=kvm:tcg -cpu cortex-a53" if IX_QEMU_aarch64
default "qemu-system-x86_64 -M pc,accel=kvm:tcg -cpu max" if QEMU_x86_64 default "qemu-system-x86_64 -M pc,accel=kvm:tcg -cpu max" if IX_QEMU_x86_64
help help
You should not have to change this setting, although you may You should not have to change this setting, although you may
want to tweak it, or change the acceleration. want to tweak it, or change the acceleration.
@@ -88,7 +88,7 @@ config QEMU_MACHINE
the defconfig you started with. Currently Infix supports the defconfig you started with. Currently Infix supports
arm (AArch32), aarch64 (ARM64), and x86_64 (AMD64). arm (AArch32), aarch64 (ARM64), and x86_64 (AMD64).
config QEMU_MACHINE_RAM config IX_QEMU_MACHINE_RAM
string "RAM size (k/M/G)" string "RAM size (k/M/G)"
default "448M" default "448M"
help help
@@ -96,62 +96,62 @@ config QEMU_MACHINE_RAM
if you get kernel panic with: "System is deadlocked on memory", if you get kernel panic with: "System is deadlocked on memory",
try increasing this one. try increasing this one.
config QEMU_KERNEL config IX_QEMU_KERNEL
string string
depends on QEMU_LOADER_KERNEL depends on IX_QEMU_LOADER_KERNEL
default "../zImage" if QEMU_arm default "../zImage" if IX_QEMU_arm
default "../Image" if QEMU_aarch64 default "../Image" if IX_QEMU_aarch64
default "../bzImage" if QEMU_x86_64 default "../bzImage" if IX_QEMU_x86_64
config QEMU_BIOS config IX_QEMU_BIOS
string string
depends on !QEMU_LOADER_KERNEL depends on !IX_QEMU_LOADER_KERNEL
default "../u-boot.bin" if QEMU_LOADER_UBOOT default "../u-boot.bin" if IX_QEMU_LOADER_UBOOT
default "../OVMF.fd" if QEMU_LOADER_OVMF default "../OVMF.fd" if IX_QEMU_LOADER_OVMF
config QEMU_ROOTFS config IX_QEMU_ROOTFS
string string
default "@DISK_IMG@" if !QEMU_ROOTFS_INITRD default "@DISK_IMG@" if !IX_QEMU_ROOTFS_INITRD
default "../rootfs.squashfs" if QEMU_ROOTFS_INITRD default "../rootfs.squashfs" if IX_QEMU_ROOTFS_INITRD
config QEMU_DTB_EXTEND config IX_QEMU_DTB_EXTEND
bool bool
depends on QEMU_LOADER_UBOOT depends on IX_QEMU_LOADER_UBOOT
default y if QEMU_aarch64 default y if IX_QEMU_aarch64
if QEMU_ROOTFS_INITRD if IX_QEMU_ROOTFS_INITRD
config QEMU_RW config IX_QEMU_RW
string "Writable /cfg layer" string "Writable /cfg layer"
depends on QEMU_ROOTFS_INITRD depends on IX_QEMU_ROOTFS_INITRD
default "cfg.ext4" default "cfg.ext4"
config QEMU_RW_VAR_OPT config IX_QEMU_RW_VAR_OPT
bool "Separate writable /var" bool "Separate writable /var"
if QEMU_RW_VAR_OPT if IX_QEMU_RW_VAR_OPT
config QEMU_RW_VAR_SIZE config IX_QEMU_RW_VAR_SIZE
string "Size of /var" string "Size of /var"
default "256M" default "256M"
config QEMU_RW_VAR config IX_QEMU_RW_VAR
string "Writable /var layer" string "Writable /var layer"
default "var.ext4" default "var.ext4"
endif endif
endif endif
config QEMU_VPD config IX_QEMU_VPD
bool "Emulate a Vital Product Data (VPD) Memory" bool "Emulate a Vital Product Data (VPD) Memory"
config QEMU_HOST config IX_QEMU_HOST
string "Export host filesystem path" string "Export host filesystem path"
default "/tmp" default "/tmp"
config QEMU_APPEND config IX_QEMU_APPEND
string "Extra kernel options" string "Extra kernel options"
depends on !QEMU_ROOTFS_MMC depends on !IX_QEMU_ROOTFS_MMC
config QEMU_EXTRA config IX_QEMU_EXTRA
string "Extra QEMU options" string "Extra QEMU options"
@@ -159,72 +159,72 @@ comment "RTC"
choice choice
prompt "Mode" prompt "Mode"
default QEMU_RTC_UTC default IX_QEMU_RTC_UTC
config QEMU_RTC_UTC config IX_QEMU_RTC_UTC
bool "UTC" bool "UTC"
config QEMU_RTC_LOCAL config IX_QEMU_RTC_LOCAL
bool "Local time" bool "Local time"
config QEMU_RTC_RANDOM config IX_QEMU_RTC_RANDOM
bool "Random" bool "Random"
endchoice endchoice
choice choice
prompt "Clock" prompt "Clock"
default QEMU_CLOCK_HOST default IX_QEMU_CLOCK_HOST
config QEMU_CLOCK_HOST config IX_QEMU_CLOCK_HOST
bool "Host clock" bool "Host clock"
config QEMU_CLOCK_RT config IX_QEMU_CLOCK_RT
bool "Independent (monotonic)" bool "Independent (monotonic)"
config QEMU_CLOCK_VM config IX_QEMU_CLOCK_VM
bool "Virtual" bool "Virtual"
endchoice endchoice
config QEMU_RTC config IX_QEMU_RTC
string string
default "utc" if QEMU_RTC_UTC default "utc" if IX_QEMU_RTC_UTC
default "localtime" if QEMU_RTC_LOCAL default "localtime" if IX_QEMU_RTC_LOCAL
default "random" if QEMU_RTC_RANDOM default "random" if IX_QEMU_RTC_RANDOM
config QEMU_CLOCK config IX_QEMU_CLOCK
string string
default "host" if QEMU_CLOCK_HOST default "host" if IX_QEMU_CLOCK_HOST
default "rt" if QEMU_CLOCK_RT default "rt" if IX_QEMU_CLOCK_RT
default "vm" if QEMU_CLOCK_VM default "vm" if IX_QEMU_CLOCK_VM
comment "Networking" comment "Networking"
choice choice
prompt "Network Mode" prompt "Network Mode"
default QEMU_NET_USER default IX_QEMU_NET_USER
config QEMU_NET_NONE config IX_QEMU_NET_NONE
bool "None" bool "None"
config QEMU_NET_BRIDGE config IX_QEMU_NET_BRIDGE
bool "Bridged" bool "Bridged"
config QEMU_NET_USER config IX_QEMU_NET_USER
bool "User" bool "User"
config QEMU_NET_TAP config IX_QEMU_NET_TAP
bool "TAP" bool "TAP"
config QEMU_NET_ROCKER config IX_QEMU_NET_ROCKER
bool "Rocker" bool "Rocker"
endchoice endchoice
config QEMU_NET_MODEL config IX_QEMU_NET_MODEL
string "Interface model" string "Interface model"
default "virtio-net-device" if QEMU_arm default "virtio-net-device" if IX_QEMU_arm
default "virtio-net-pci" default "virtio-net-pci"
help help
The default, virtio-net-pci, NIC works for most use-cases, but The default, virtio-net-pci, NIC works for most use-cases, but
@@ -233,23 +233,23 @@ config QEMU_NET_MODEL
Note: ARM 32-bit uses virtio-net-device (MMIO) by default. Note: ARM 32-bit uses virtio-net-device (MMIO) by default.
config QEMU_NET_BRIDGE_DEV config IX_QEMU_NET_BRIDGE_DEV
string "Bridge device" string "Bridge device"
depends on QEMU_NET_BRIDGE depends on IX_QEMU_NET_BRIDGE
default "virbr0" default "virbr0"
config QEMU_NET_USER_OPTS config IX_QEMU_NET_USER_OPTS
string "User mode options" string "User mode options"
depends on QEMU_NET_USER depends on IX_QEMU_NET_USER
help help
Extra -nic user,<OPTIONS> Extra -nic user,<OPTIONS>
config QEMU_NET_TAP_N config IX_QEMU_NET_TAP_N
int "Number of TAPs" int "Number of TAPs"
depends on QEMU_NET_TAP depends on IX_QEMU_NET_TAP
default 1 default 1
config QEMU_NET_PORTS config IX_QEMU_NET_PORTS
int "Number of Rocker switch ports" int "Number of Rocker switch ports"
depends on QEMU_NET_ROCKER depends on IX_QEMU_NET_ROCKER
default 10 default 10
+2 -2
View File
@@ -10,7 +10,7 @@ qemu-kconfig = \
BR2_CONFIG="$(BINARIES_DIR)/qemu/.config" \ BR2_CONFIG="$(BINARIES_DIR)/qemu/.config" \
$(BUILD_DIR)/buildroot-config/$(1) $(2) "$(BINARIES_DIR)/qemu/Config.in" $(BUILD_DIR)/buildroot-config/$(1) $(2) "$(BINARIES_DIR)/qemu/Config.in"
ifeq ($(QEMU_SCRIPTS),y) ifeq ($(IX_QEMU_SCRIPTS),y)
.PHONY: run .PHONY: run
run: run:
@@ -33,7 +33,7 @@ $(BINARIES_DIR)/qemu/run.sh: $(QEMU_SCRIPTS_DIR)/run.sh
$(BINARIES_DIR)/qemu/Config.in: $(QEMU_SCRIPTS_DIR)/Config.in.in $(BINARIES_DIR)/qemu/Config.in: $(QEMU_SCRIPTS_DIR)/Config.in.in
@mkdir -p $(dir $@) @mkdir -p $(dir $@)
@sed \ @sed \
-e "s:@ARCH@:QEMU_$(BR2_ARCH):" \ -e "s:@ARCH@:IX_QEMU_$(BR2_ARCH):" \
-e "s:@DISK_IMG@:../$(INFIX_ARTIFACT).qcow2:" \ -e "s:@DISK_IMG@:../$(INFIX_ARTIFACT).qcow2:" \
< $< >$@ < $< >$@
+73 -73
View File
@@ -40,7 +40,7 @@ usage()
echo " ARGS1 Args before the '--' separator are for kernel space" echo " ARGS1 Args before the '--' separator are for kernel space"
echo " -- Separator" echo " -- Separator"
echo " ARGS2 Args after the '--' separator are for the init process" echo " ARGS2 Args after the '--' separator are for the init process"
echo " Also, qemu.cfg has QEMU_APPEND which can affect this." echo " Also, qemu.cfg has IX_QEMU_APPEND which can affect this."
echo echo
echo "Example:" echo "Example:"
echo " $prognm -- finit.debug" echo " $prognm -- finit.debug"
@@ -62,45 +62,45 @@ load_qemucfg()
# shellcheck disable=SC1090 # shellcheck disable=SC1090
. "./.config" . "./.config"
[ "$CONFIG_QEMU_MACHINE" ] || die "Missing QEMU_MACHINE" [ "$CONFIG_IX_QEMU_MACHINE" ] || die "Missing IX_QEMU_MACHINE"
[ "$CONFIG_QEMU_ROOTFS" ] || die "Missing QEMU_ROOTFS" [ "$CONFIG_IX_QEMU_ROOTFS" ] || die "Missing IX_QEMU_ROOTFS"
[ -n "$CONFIG_QEMU_KERNEL" ] && [ -n "$CONFIG_QEMU_BIOS" ] \ [ -n "$CONFIG_IX_QEMU_KERNEL" ] && [ -n "$CONFIG_IX_QEMU_BIOS" ] \
&& die "QEMU_KERNEL conflicts with QEMU_BIOS" && die "IX_QEMU_KERNEL conflicts with IX_QEMU_BIOS"
[ -z "$CONFIG_QEMU_KERNEL" ] && [ -z "$CONFIG_QEMU_BIOS" ] \ [ -z "$CONFIG_IX_QEMU_KERNEL" ] && [ -z "$CONFIG_IX_QEMU_BIOS" ] \
&& die "QEMU_KERNEL or QEMU_BIOS must be set" && die "IX_QEMU_KERNEL or IX_QEMU_BIOS must be set"
} }
loader_args() loader_args()
{ {
if [ "$CONFIG_QEMU_BIOS" ]; then if [ "$CONFIG_IX_QEMU_BIOS" ]; then
echo -n "-bios $CONFIG_QEMU_BIOS " echo -n "-bios $CONFIG_IX_QEMU_BIOS "
elif [ "$CONFIG_QEMU_KERNEL" ]; then elif [ "$CONFIG_IX_QEMU_KERNEL" ]; then
echo -n "-kernel $CONFIG_QEMU_KERNEL " echo -n "-kernel $CONFIG_IX_QEMU_KERNEL "
fi fi
} }
append_args() append_args()
{ {
# ARM 32-bit doesn't support virtio console properly, always use serial # ARM 32-bit doesn't support virtio console properly, always use serial
if [ "$CONFIG_QEMU_arm" ]; then if [ "$CONFIG_IX_QEMU_arm" ]; then
echo -n "console=ttyAMA0 " echo -n "console=ttyAMA0 "
elif [ "$CONFIG_QEMU_CONSOLE_VIRTIO" ]; then elif [ "$CONFIG_IX_QEMU_CONSOLE_VIRTIO" ]; then
echo -n "console=hvc0 " echo -n "console=hvc0 "
elif [ "$CONFIG_QEMU_x86_64" ]; then elif [ "$CONFIG_IX_QEMU_x86_64" ]; then
echo -n "console=ttyS0 " echo -n "console=ttyS0 "
elif [ "$CONFIG_QEMU_aarch64" ]; then elif [ "$CONFIG_IX_QEMU_aarch64" ]; then
echo -n "console=ttyAMA0 " echo -n "console=ttyAMA0 "
else else
die "Unknown console" die "Unknown console"
fi fi
if [ "$CONFIG_QEMU_ROOTFS_INITRD" = "y" ]; then if [ "$CONFIG_IX_QEMU_ROOTFS_INITRD" = "y" ]; then
# Size of initrd, rounded up to nearest kb # Size of initrd, rounded up to nearest kb
size=$((($(stat -c %s "$CONFIG_QEMU_ROOTFS") + 1023) >> 10)) size=$((($(stat -c %s "$CONFIG_IX_QEMU_ROOTFS") + 1023) >> 10))
echo -n "root=/dev/ram0 ramdisk_size=${size} " echo -n "root=/dev/ram0 ramdisk_size=${size} "
elif [ "$CONFIG_QEMU_ROOTFS_VSCSI" = "y" ]; then elif [ "$CONFIG_IX_QEMU_ROOTFS_VSCSI" = "y" ]; then
echo -n "root=PARTLABEL=primary " echo -n "root=PARTLABEL=primary "
fi fi
@@ -110,20 +110,20 @@ append_args()
echo -n "debug " echo -n "debug "
fi fi
echo -n "${QEMU_APPEND} ${QEMU_EXTRA_APPEND} " echo -n "${IX_QEMU_APPEND} ${QEMU_EXTRA_APPEND} "
} }
rootfs_args() rootfs_args()
{ {
if [ "$CONFIG_QEMU_ROOTFS_INITRD" = "y" ]; then if [ "$CONFIG_IX_QEMU_ROOTFS_INITRD" = "y" ]; then
echo -n "-initrd $CONFIG_QEMU_ROOTFS " echo -n "-initrd $CONFIG_IX_QEMU_ROOTFS "
elif [ "$CONFIG_QEMU_ROOTFS_MMC" = "y" ]; then elif [ "$CONFIG_IX_QEMU_ROOTFS_MMC" = "y" ]; then
echo -n "-device sdhci-pci " echo -n "-device sdhci-pci "
echo -n "-device sd-card,drive=mmc " echo -n "-device sd-card,drive=mmc "
echo -n "-drive id=mmc,file=$CONFIG_QEMU_ROOTFS,if=none,format=raw " echo -n "-drive id=mmc,file=$CONFIG_IX_QEMU_ROOTFS,if=none,format=raw "
elif [ "$CONFIG_QEMU_ROOTFS_VSCSI" = "y" ]; then elif [ "$CONFIG_IX_QEMU_ROOTFS_VSCSI" = "y" ]; then
# ARM 32-bit virt machine uses MMIO virtio devices, not PCI # ARM 32-bit virt machine uses MMIO virtio devices, not PCI
if [ "$CONFIG_QEMU_arm" ]; then if [ "$CONFIG_IX_QEMU_arm" ]; then
echo -n "-drive file=qemu.qcow2,if=none,format=qcow2,id=rootfs " echo -n "-drive file=qemu.qcow2,if=none,format=qcow2,id=rootfs "
echo -n "-device virtio-blk-device,drive=rootfs " echo -n "-device virtio-blk-device,drive=rootfs "
else else
@@ -140,9 +140,9 @@ serial_args()
echo -n "-chardev stdio,id=console0,mux=on " echo -n "-chardev stdio,id=console0,mux=on "
echo -n "-mon chardev=console0 " echo -n "-mon chardev=console0 "
if [ "$CONFIG_QEMU_CONSOLE_VIRTIO" ]; then if [ "$CONFIG_IX_QEMU_CONSOLE_VIRTIO" ]; then
echo -n "-device virtconsole,nr=0,name=console,chardev=console0 " echo -n "-device virtconsole,nr=0,name=console,chardev=console0 "
elif [ "$CONFIG_QEMU_CONSOLE_SERIAL" ]; then elif [ "$CONFIG_IX_QEMU_CONSOLE_SERIAL" ]; then
echo -n "-serial chardev:console0 " echo -n "-serial chardev:console0 "
else else
die "Unknown console" die "Unknown console"
@@ -175,7 +175,7 @@ usb_args()
rw_args() rw_args()
{ {
[ "$CONFIG_QEMU_RW" ] || return [ "$CONFIG_IX_QEMU_RW" ] || return
command -v mkfs.ext4 >/dev/null || die "$prognm: cannot find mkfs.ext4" command -v mkfs.ext4 >/dev/null || die "$prognm: cannot find mkfs.ext4"
@@ -184,45 +184,45 @@ rw_args()
mkfs.ext4 -L aux "aux.ext4" >/dev/null 2>&1 mkfs.ext4 -L aux "aux.ext4" >/dev/null 2>&1
fi fi
if ! [ -f "$CONFIG_QEMU_RW" ]; then if ! [ -f "$CONFIG_IX_QEMU_RW" ]; then
dd if=/dev/zero of="$CONFIG_QEMU_RW" bs=16M count=1 >/dev/null 2>&1 dd if=/dev/zero of="$CONFIG_IX_QEMU_RW" bs=16M count=1 >/dev/null 2>&1
mkfs.ext4 -L cfg "$CONFIG_QEMU_RW" >/dev/null 2>&1 mkfs.ext4 -L cfg "$CONFIG_IX_QEMU_RW" >/dev/null 2>&1
fi fi
# ARM 32-bit virt machine uses MMIO virtio devices, not PCI # ARM 32-bit virt machine uses MMIO virtio devices, not PCI
if [ "$CONFIG_QEMU_arm" ]; then if [ "$CONFIG_IX_QEMU_arm" ]; then
echo -n "-drive file=aux.ext4,if=none,format=raw,id=aux " echo -n "-drive file=aux.ext4,if=none,format=raw,id=aux "
echo -n "-device virtio-blk-device,drive=aux " echo -n "-device virtio-blk-device,drive=aux "
echo -n "-drive file=$CONFIG_QEMU_RW,if=none,format=raw,id=cfg " echo -n "-drive file=$CONFIG_IX_QEMU_RW,if=none,format=raw,id=cfg "
echo -n "-device virtio-blk-device,drive=cfg " echo -n "-device virtio-blk-device,drive=cfg "
if [ "$CONFIG_QEMU_RW_VAR_OPT" ]; then if [ "$CONFIG_IX_QEMU_RW_VAR_OPT" ]; then
if ! [ -f "$CONFIG_QEMU_RW_VAR" ]; then if ! [ -f "$CONFIG_IX_QEMU_RW_VAR" ]; then
dd if=/dev/zero of="$CONFIG_QEMU_RW_VAR" bs=$CONFIG_QEMU_RW_VAR_SIZE count=1 >/dev/null 2>&1 dd if=/dev/zero of="$CONFIG_IX_QEMU_RW_VAR" bs=$CONFIG_IX_QEMU_RW_VAR_SIZE count=1 >/dev/null 2>&1
mkfs.ext4 -L var "$CONFIG_QEMU_RW_VAR" >/dev/null 2>&1 mkfs.ext4 -L var "$CONFIG_IX_QEMU_RW_VAR" >/dev/null 2>&1
fi fi
echo -n "-drive file=$CONFIG_QEMU_RW_VAR,if=none,format=raw,id=var " echo -n "-drive file=$CONFIG_IX_QEMU_RW_VAR,if=none,format=raw,id=var "
echo -n "-device virtio-blk-device,drive=var " echo -n "-device virtio-blk-device,drive=var "
fi fi
else else
echo -n "-drive file=aux.ext4,if=virtio,format=raw,bus=0,unit=3 " echo -n "-drive file=aux.ext4,if=virtio,format=raw,bus=0,unit=3 "
echo -n "-drive file=$CONFIG_QEMU_RW,if=virtio,format=raw,bus=0,unit=1 " echo -n "-drive file=$CONFIG_IX_QEMU_RW,if=virtio,format=raw,bus=0,unit=1 "
if [ "$CONFIG_QEMU_RW_VAR_OPT" ]; then if [ "$CONFIG_IX_QEMU_RW_VAR_OPT" ]; then
if ! [ -f "$CONFIG_QEMU_RW_VAR" ]; then if ! [ -f "$CONFIG_IX_QEMU_RW_VAR" ]; then
dd if=/dev/zero of="$CONFIG_QEMU_RW_VAR" bs=$CONFIG_QEMU_RW_VAR_SIZE count=1 >/dev/null 2>&1 dd if=/dev/zero of="$CONFIG_IX_QEMU_RW_VAR" bs=$CONFIG_IX_QEMU_RW_VAR_SIZE count=1 >/dev/null 2>&1
mkfs.ext4 -L var "$CONFIG_QEMU_RW_VAR" >/dev/null 2>&1 mkfs.ext4 -L var "$CONFIG_IX_QEMU_RW_VAR" >/dev/null 2>&1
fi fi
echo -n "-drive file=$CONFIG_QEMU_RW_VAR,if=virtio,format=raw,bus=0,unit=2 " echo -n "-drive file=$CONFIG_IX_QEMU_RW_VAR,if=virtio,format=raw,bus=0,unit=2 "
fi fi
fi fi
} }
host_args() host_args()
{ {
[ "$CONFIG_QEMU_HOST" ] || return [ "$CONFIG_IX_QEMU_HOST" ] || return
echo -n "-virtfs local,path=$CONFIG_QEMU_HOST,security_model=none,writeout=immediate,mount_tag=hostfs " echo -n "-virtfs local,path=$CONFIG_IX_QEMU_HOST,security_model=none,writeout=immediate,mount_tag=hostfs "
} }
net_dev_args() net_dev_args()
@@ -230,7 +230,7 @@ net_dev_args()
name="e$1" name="e$1"
mac=$(printf "02:00:00:00:00:%02x" "$1") mac=$(printf "02:00:00:00:00:%02x" "$1")
echo -n "-device $CONFIG_QEMU_NET_MODEL,netdev=$name,mac=$mac " echo -n "-device $CONFIG_IX_QEMU_NET_MODEL,netdev=$name,mac=$mac "
echo "$name $mac" >>"$mactab" echo "$name $mac" >>"$mactab"
} }
@@ -252,29 +252,29 @@ net_args()
:> "$mactab" :> "$mactab"
echo -n "-fw_cfg name=opt/mactab,file=$mactab " echo -n "-fw_cfg name=opt/mactab,file=$mactab "
if [ "$CONFIG_QEMU_NET_BRIDGE" = "y" ]; then if [ "$CONFIG_IX_QEMU_NET_BRIDGE" = "y" ]; then
echo -n "-netdev bridge,id=e1,br=$CONFIG_QEMU_NET_BRIDGE_DEV " echo -n "-netdev bridge,id=e1,br=$CONFIG_IX_QEMU_NET_BRIDGE_DEV "
net_dev_args 1 net_dev_args 1
elif [ "$CONFIG_QEMU_NET_TAP" = "y" ]; then elif [ "$CONFIG_IX_QEMU_NET_TAP" = "y" ]; then
for i in $(seq 1 "$CONFIG_QEMU_NET_TAP_N"); do for i in $(seq 1 "$CONFIG_IX_QEMU_NET_TAP_N"); do
echo -n "-netdev tap,id=e$i,ifname=qtap$i " echo -n "-netdev tap,id=e$i,ifname=qtap$i "
net_dev_args "$i" net_dev_args "$i"
done done
elif [ "$CONFIG_QEMU_NET_ROCKER" = "y" ]; then elif [ "$CONFIG_IX_QEMU_NET_ROCKER" = "y" ]; then
sw=sw0 # Only single switch support atm. sw=sw0 # Only single switch support atm.
echo -n "-device '{\"driver\":\"rocker\", \"name\":\"${sw}\", " echo -n "-device '{\"driver\":\"rocker\", \"name\":\"${sw}\", "
echo -n "\"fp_start_macaddr\":\"02:00:00:00:00:01\", " echo -n "\"fp_start_macaddr\":\"02:00:00:00:00:01\", "
echo -n "\"ports\":[" echo -n "\"ports\":["
for i in $(seq 1 "$CONFIG_QEMU_NET_PORTS"); do for i in $(seq 1 "$CONFIG_IX_QEMU_NET_PORTS"); do
[ "$i" -gt 1 ] && echo -n ", " [ "$i" -gt 1 ] && echo -n ", "
echo -n "\"${sw}p${i}\"" echo -n "\"${sw}p${i}\""
done done
echo -n "]}' " echo -n "]}' "
for i in $(seq 1 "$CONFIG_QEMU_NET_PORTS"); do for i in $(seq 1 "$CONFIG_IX_QEMU_NET_PORTS"); do
rocker_port_args 0 "$i" rocker_port_args 0 "$i"
done done
elif [ "$CONFIG_QEMU_NET_USER" = "y" ]; then elif [ "$CONFIG_IX_QEMU_NET_USER" = "y" ]; then
[ "$CONFIG_QEMU_NET_USER_OPTS" ] && useropts=",$CONFIG_QEMU_NET_USER_OPTS" [ "$CONFIG_IX_QEMU_NET_USER_OPTS" ] && useropts=",$CONFIG_IX_QEMU_NET_USER_OPTS"
echo -n "-netdev user,id=e1${useropts} " echo -n "-netdev user,id=e1${useropts} "
net_dev_args 1 net_dev_args 1
else else
@@ -285,7 +285,7 @@ net_args()
# Vital Product data # Vital Product data
vpd_args() vpd_args()
{ {
[ "$CONFIG_QEMU_VPD" = "y" ] || return [ "$CONFIG_IX_QEMU_VPD" = "y" ] || return
vpd_file="${qdir}/vpd" vpd_file="${qdir}/vpd"
@@ -324,8 +324,8 @@ random_date()
rtc_args() rtc_args()
{ {
rtc="${CONFIG_QEMU_RTC:-utc}" rtc="${CONFIG_IX_QEMU_RTC:-utc}"
clock="${CONFIG_QEMU_CLOCK:-host}" clock="${CONFIG_IX_QEMU_CLOCK:-host}"
if [ "$rtc" = "random" ]; then if [ "$rtc" = "random" ]; then
rtc=$(random_date) rtc=$(random_date)
fi fi
@@ -358,28 +358,28 @@ extract_squashfs()
run_qemu() run_qemu()
{ {
# Auto-extract rootfs.squashfs from rootfs.itb if needed for initrd mode # Auto-extract rootfs.squashfs from rootfs.itb if needed for initrd mode
if [ "$CONFIG_QEMU_ROOTFS_INITRD" = "y" ] && [ ! -f "$CONFIG_QEMU_ROOTFS" ]; then if [ "$CONFIG_IX_QEMU_ROOTFS_INITRD" = "y" ] && [ ! -f "$CONFIG_IX_QEMU_ROOTFS" ]; then
itb="${CONFIG_QEMU_ROOTFS%.squashfs}.itb" itb="${CONFIG_IX_QEMU_ROOTFS%.squashfs}.itb"
if [ -f "$itb" ]; then if [ -f "$itb" ]; then
extract_squashfs "$itb" "$CONFIG_QEMU_ROOTFS" extract_squashfs "$itb" "$CONFIG_IX_QEMU_ROOTFS"
else else
die "Missing $CONFIG_QEMU_ROOTFS and cannot find $itb to extract it from" die "Missing $CONFIG_IX_QEMU_ROOTFS and cannot find $itb to extract it from"
fi fi
fi fi
if [ "$CONFIG_QEMU_ROOTFS_VSCSI" = "y" ]; then if [ "$CONFIG_IX_QEMU_ROOTFS_VSCSI" = "y" ]; then
if ! qemu-img check "qemu.qcow2"; then if ! qemu-img check "qemu.qcow2"; then
rm -f "qemu.qcow2" rm -f "qemu.qcow2"
fi fi
if [ ! -f "qemu.qcow2" ]; then if [ ! -f "qemu.qcow2" ]; then
echo "Creating qcow2 disk image for Qemu ..." echo "Creating qcow2 disk image for Qemu ..."
qemu-img create -f qcow2 -o backing_file="$CONFIG_QEMU_ROOTFS" \ qemu-img create -f qcow2 -o backing_file="$CONFIG_IX_QEMU_ROOTFS" \
-F qcow2 "qemu.qcow2" > /dev/null -F qcow2 "qemu.qcow2" > /dev/null
fi fi
fi fi
read -r qemu <<EOF read -r qemu <<EOF
$CONFIG_QEMU_MACHINE -nodefaults -m $CONFIG_QEMU_MACHINE_RAM \ $CONFIG_IX_QEMU_MACHINE -nodefaults -m $CONFIG_IX_QEMU_MACHINE_RAM \
$(loader_args) \ $(loader_args) \
$(rootfs_args) \ $(rootfs_args) \
$(serial_args) \ $(serial_args) \
@@ -391,14 +391,14 @@ run_qemu()
$(rtc_args) \ $(rtc_args) \
$(vpd_args) \ $(vpd_args) \
$(gdb_args) \ $(gdb_args) \
$CONFIG_QEMU_EXTRA $CONFIG_IX_QEMU_EXTRA
EOF EOF
# Save resulting command to a script, because I cannot for the life # Save resulting command to a script, because I cannot for the life
# of me figure out how to embed the JSON snippet for Rocker and run # of me figure out how to embed the JSON snippet for Rocker and run
# it here without issues, spent way too much time on it -- Joachim # it here without issues, spent way too much time on it -- Joachim
run=$(mktemp -t run.qemu.XXX) run=$(mktemp -t run.qemu.XXX)
echo "#!/bin/sh" > "$run" echo "#!/bin/sh" > "$run"
if [ "$CONFIG_QEMU_KERNEL" ]; then if [ "$CONFIG_IX_QEMU_KERNEL" ]; then
echo "$qemu -append \"$(append_args)\" $*" >> "$run" echo "$qemu -append \"$(append_args)\" $*" >> "$run"
else else
echo "$qemu $*" >> "$run" echo "$qemu $*" >> "$run"
@@ -419,9 +419,9 @@ EOF
dtb_args() dtb_args()
{ {
[ "$CONFIG_QEMU_LOADER_UBOOT" ] || return [ "$CONFIG_IX_QEMU_LOADER_UBOOT" ] || return
if [ "$CONFIG_QEMU_DTB_EXTEND" ]; then if [ "$CONFIG_IX_QEMU_DTB_EXTEND" ]; then
# On the current architecture, QEMU will generate an internal # On the current architecture, QEMU will generate an internal
# DT based on the system configuration. # DT based on the system configuration.
@@ -445,12 +445,12 @@ dtb_args()
generate_dot() generate_dot()
{ {
[ "$CONFIG_QEMU_NET_TAP" = "y" ] || return [ "$CONFIG_IX_QEMU_NET_TAP" = "y" ] || return
hostports="<qtap1> qtap1" hostports="<qtap1> qtap1"
targetports="<e1> e1" targetports="<e1> e1"
edges="host:qtap1 -- target:e1 [kind=mgmt];" edges="host:qtap1 -- target:e1 [kind=mgmt];"
for tap in $(seq 2 $((CONFIG_QEMU_NET_TAP_N - 1))); do for tap in $(seq 2 $((CONFIG_IX_QEMU_NET_TAP_N - 1))); do
hostports="$hostports | <qtap$tap> qtap$tap " hostports="$hostports | <qtap$tap> qtap$tap "
targetports="$targetports | <e$tap> e$tap " targetports="$targetports | <e$tap> e$tap "
edges="$edges host:qtap$tap -- target:e$tap;" edges="$edges host:qtap$tap -- target:e$tap;"
@@ -485,7 +485,7 @@ EOF
menuconfig() menuconfig()
{ {
grep -q QEMU_MACHINE Config.in || die "$prognm: must be run from the $$O/images/qemu directory" grep -q IX_QEMU_MACHINE Config.in || die "$prognm: must be run from the $$O/images/qemu directory"
command -v kconfig-mconf >/dev/null || die "$prognm: cannot find kconfig-mconf for menuconfig" command -v kconfig-mconf >/dev/null || die "$prognm: cannot find kconfig-mconf for menuconfig"
exec kconfig-mconf Config.in exec kconfig-mconf Config.in
} }
@@ -24,6 +24,10 @@ net.ipv4.conf.all.arp_ignore=1
net.ipv4.ip_forward=1 net.ipv4.ip_forward=1
net.ipv4.ip_forward_update_priority=0 net.ipv4.ip_forward_update_priority=0
# Allow binding to non-local addresses, e.g. floating VIPs not yet
# configured on an interface, see issue #1022
net.ipv4.ip_nonlocal_bind=1
net.ipv4.conf.all.forwarding=0 net.ipv4.conf.all.forwarding=0
net.ipv4.conf.default.forwarding=0 net.ipv4.conf.default.forwarding=0
@@ -7,6 +7,10 @@ net.ipv6.conf.all.ignore_routes_with_linkdown=1
net.ipv6.conf.all.forwarding=0 net.ipv6.conf.all.forwarding=0
net.ipv6.conf.default.forwarding=0 net.ipv6.conf.default.forwarding=0
# Allow binding to non-local addresses, e.g. floating VIPs not yet
# configured on an interface, see issue #1022
net.ipv6.ip_nonlocal_bind=1
# Accept router advertisements even when forwarding is enabled # Accept router advertisements even when forwarding is enabled
net.ipv6.conf.all.accept_ra=2 net.ipv6.conf.all.accept_ra=2
net.ipv6.conf.default.accept_ra=2 net.ipv6.conf.default.accept_ra=2
+10 -1
View File
@@ -25,7 +25,7 @@ define UBOOT_PRE_BUILD_INSTALL_KEY
$(HOST_DIR)/bin/dtc -a 1024 <(echo '/dts-v1/; / { signature {}; };') \ $(HOST_DIR)/bin/dtc -a 1024 <(echo '/dts-v1/; / { signature {}; };') \
>$(@D)/infix-key.dtb >$(@D)/infix-key.dtb
$(foreach key, \ $(foreach key, \
$(call qstrip,$(TRUSTED_KEYS_DEVELOPMENT_PATH)) $(call qstrip,$(TRUSTED_KEYS_EXTRA_PATH)),\ $(call qstrip,$(IX_TRUSTED_KEYS_DEVELOPMENT_PATH)) $(call qstrip,$(IX_TRUSTED_KEYS_EXTRA_PATH)),\
$(call uboot-add-pubkey,$(key),$(@D)/infix-key.dtb)) $(call uboot-add-pubkey,$(key),$(@D)/infix-key.dtb))
$(HOST_DIR)/bin/dtc -I dtb -O dts \ $(HOST_DIR)/bin/dtc -I dtb -O dts \
<$(@D)/infix-key.dtb \ <$(@D)/infix-key.dtb \
@@ -46,3 +46,12 @@ define UBOOT_PRE_BUILD_INSTALL_ENV
$(@D)/arch/$(UBOOT_ARCH)/dts/ $(@D)/arch/$(UBOOT_ARCH)/dts/
endef endef
UBOOT_PRE_BUILD_HOOKS += UBOOT_PRE_BUILD_INSTALL_ENV UBOOT_PRE_BUILD_HOOKS += UBOOT_PRE_BUILD_INSTALL_ENV
# Stamp non-release builds so they cannot be mistaken for a release,
# U-Boot's setlocalversion picks up .scmversion, see issue #919.
define UBOOT_PRE_BUILD_DEVEL_VERSION
echo "-DEVEL" >$(@D)/.scmversion
endef
ifeq ($(INFIX_RELEASE),)
UBOOT_PRE_BUILD_HOOKS += UBOOT_PRE_BUILD_DEVEL_VERSION
endif
+11 -11
View File
@@ -143,11 +143,11 @@ BR2_PACKAGE_MARVELL_CN9130_CRB=y
BR2_PACKAGE_MARVELL_ESPRESSOBIN=y BR2_PACKAGE_MARVELL_ESPRESSOBIN=y
BR2_PACKAGE_RASPBERRYPI_RPI64=y BR2_PACKAGE_RASPBERRYPI_RPI64=y
BR2_PACKAGE_STYX_DCP_SC_28P=y BR2_PACKAGE_STYX_DCP_SC_28P=y
INFIX_VENDOR_HOME="https://www.kernelkit.org" IX_VENDOR_HOME="https://www.kernelkit.org"
INFIX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers." IX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers."
INFIX_HOME="https://github.com/kernelkit/infix/" IX_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://www.kernelkit.org/infix/" IX_DOC="https://www.kernelkit.org/infix/"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" IX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_FEATURE_GPS=y
BR2_PACKAGE_FEATURE_WIFI=y BR2_PACKAGE_FEATURE_WIFI=y
BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y
@@ -191,9 +191,9 @@ BR2_PACKAGE_WEBUI=y
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
BR2_PACKAGE_PCIUTILS=y BR2_PACKAGE_PCIUTILS=y
IMAGE_ITB_AUX=y IX_IMAGE_ITB_AUX=y
IMAGE_ITB_QCOW=y IX_IMAGE_ITB_QCOW=y
IMAGE_ITB_RAUC=y IX_IMAGE_ITB_RAUC=y
IMAGE_README=y IX_IMAGE_README=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+11 -11
View File
@@ -121,11 +121,11 @@ BR2_PACKAGE_MARVELL_CN9130_CRB=y
BR2_PACKAGE_MARVELL_ESPRESSOBIN=y BR2_PACKAGE_MARVELL_ESPRESSOBIN=y
BR2_PACKAGE_RASPBERRYPI_RPI64=y BR2_PACKAGE_RASPBERRYPI_RPI64=y
BR2_PACKAGE_STYX_DCP_SC_28P=y BR2_PACKAGE_STYX_DCP_SC_28P=y
INFIX_VENDOR_HOME="https://www.kernelkit.org" IX_VENDOR_HOME="https://www.kernelkit.org"
INFIX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers." IX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers."
INFIX_HOME="https://github.com/kernelkit/infix/" IX_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://www.kernelkit.org/infix/" IX_DOC="https://www.kernelkit.org/infix/"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" IX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_CONFD=y BR2_PACKAGE_CONFD=y
BR2_PACKAGE_NETD=y BR2_PACKAGE_NETD=y
BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_CONFD_TEST_MODE=y
@@ -151,9 +151,9 @@ BR2_PACKAGE_MDNS_ALIAS=y
BR2_PACKAGE_ONIEPROM=y BR2_PACKAGE_ONIEPROM=y
BR2_PACKAGE_ROUSETTE=y BR2_PACKAGE_ROUSETTE=y
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
IMAGE_ITB_AUX=y IX_IMAGE_ITB_AUX=y
IMAGE_ITB_QCOW=y IX_IMAGE_ITB_QCOW=y
IMAGE_ITB_RAUC=y IX_IMAGE_ITB_RAUC=y
IMAGE_README=y IX_IMAGE_README=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+2 -2
View File
@@ -25,5 +25,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+12 -11
View File
@@ -129,6 +129,7 @@ BR2_PACKAGE_HOST_DOSFSTOOLS=y
BR2_PACKAGE_HOST_E2FSPROGS=y BR2_PACKAGE_HOST_E2FSPROGS=y
BR2_PACKAGE_HOST_ENVIRONMENT_SETUP=y BR2_PACKAGE_HOST_ENVIRONMENT_SETUP=y
BR2_PACKAGE_HOST_GENEXT2FS=y BR2_PACKAGE_HOST_GENEXT2FS=y
BR2_PACKAGE_HOST_GO_BIN=y
BR2_PACKAGE_HOST_KMOD_XZ=y BR2_PACKAGE_HOST_KMOD_XZ=y
BR2_PACKAGE_HOST_MTOOLS=y BR2_PACKAGE_HOST_MTOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
@@ -136,11 +137,11 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y
BR2_PACKAGE_MICROCHIP_SAMA7G54_EK=y BR2_PACKAGE_MICROCHIP_SAMA7G54_EK=y
BR2_PACKAGE_RASPBERRYPI_RPI2=y BR2_PACKAGE_RASPBERRYPI_RPI2=y
INFIX_VENDOR_HOME="https://www.kernelkit.org" IX_VENDOR_HOME="https://www.kernelkit.org"
INFIX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers." IX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers."
INFIX_HOME="https://github.com/kernelkit/infix/" IX_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://www.kernelkit.org/infix/" IX_DOC="https://www.kernelkit.org/infix/"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" IX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_FEATURE_GPS=y
BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y
BR2_PACKAGE_FEATURE_WIFI_REALTEK=y BR2_PACKAGE_FEATURE_WIFI_REALTEK=y
@@ -173,9 +174,9 @@ BR2_PACKAGE_ROUSETTE=y
BR2_PACKAGE_WEBUI=y BR2_PACKAGE_WEBUI=y
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
IMAGE_ITB_AUX=y IX_IMAGE_ITB_AUX=y
IMAGE_ITB_QCOW=y IX_IMAGE_ITB_QCOW=y
IMAGE_ITB_RAUC=y IX_IMAGE_ITB_RAUC=y
IMAGE_README=y IX_IMAGE_README=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+11 -11
View File
@@ -119,11 +119,11 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y
BR2_PACKAGE_MICROCHIP_SAMA7G54_EK=y BR2_PACKAGE_MICROCHIP_SAMA7G54_EK=y
BR2_PACKAGE_RASPBERRYPI_RPI2=y BR2_PACKAGE_RASPBERRYPI_RPI2=y
INFIX_VENDOR_HOME="https://www.kernelkit.org" IX_VENDOR_HOME="https://www.kernelkit.org"
INFIX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers." IX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers."
INFIX_HOME="https://github.com/kernelkit/infix/" IX_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://www.kernelkit.org/infix/" IX_DOC="https://www.kernelkit.org/infix/"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" IX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_CONFD=y BR2_PACKAGE_CONFD=y
BR2_PACKAGE_NETD=y BR2_PACKAGE_NETD=y
BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_CONFD_TEST_MODE=y
@@ -149,9 +149,9 @@ BR2_PACKAGE_MDNS_ALIAS=y
BR2_PACKAGE_ONIEPROM=y BR2_PACKAGE_ONIEPROM=y
BR2_PACKAGE_ROUSETTE=y BR2_PACKAGE_ROUSETTE=y
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
IMAGE_ITB_AUX=y IX_IMAGE_ITB_AUX=y
IMAGE_ITB_QCOW=y IX_IMAGE_ITB_QCOW=y
IMAGE_ITB_RAUC=y IX_IMAGE_ITB_RAUC=y
IMAGE_README=y IX_IMAGE_README=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+2 -2
View File
@@ -40,5 +40,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+2 -2
View File
@@ -38,5 +38,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+2 -2
View File
@@ -38,5 +38,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+2 -2
View File
@@ -38,5 +38,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+2 -2
View File
@@ -38,5 +38,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+2 -2
View File
@@ -38,5 +38,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+2 -2
View File
@@ -38,5 +38,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+2 -2
View File
@@ -45,5 +45,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y
BR2_PACKAGE_BOOTLOADER_SPLASHSCREEN=y BR2_PACKAGE_BOOTLOADER_SPLASHSCREEN=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+12 -11
View File
@@ -164,14 +164,15 @@ BR2_TARGET_UBOOT_SPL=y
BR2_TARGET_UBOOT_SPL_NAME="spl/u-boot-spl.bin.normal.out" BR2_TARGET_UBOOT_SPL_NAME="spl/u-boot-spl.bin.normal.out"
BR2_TARGET_UBOOT_CUSTOM_DTS_PATH="$(BR2_EXTERNAL_INFIX_PATH)/board/riscv64/visionfive2/uboot/visionfive2-env.dtsi" BR2_TARGET_UBOOT_CUSTOM_DTS_PATH="$(BR2_EXTERNAL_INFIX_PATH)/board/riscv64/visionfive2/uboot/visionfive2-env.dtsi"
BR2_PACKAGE_HOST_BMAP_TOOLS=y BR2_PACKAGE_HOST_BMAP_TOOLS=y
BR2_PACKAGE_HOST_GO_BIN=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y
INFIX_VENDOR_HOME="https://www.kernelkit.org" IX_VENDOR_HOME="https://www.kernelkit.org"
INFIX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers." IX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers."
INFIX_HOME="https://github.com/kernelkit/infix/" IX_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://www.kernelkit.org/infix/" IX_DOC="https://www.kernelkit.org/infix/"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" IX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_FEATURE_GPS=y
BR2_PACKAGE_FEATURE_WIFI=y BR2_PACKAGE_FEATURE_WIFI=y
BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y
@@ -210,9 +211,9 @@ BR2_PACKAGE_WEBUI=y
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
BR2_PACKAGE_PCIUTILS=y BR2_PACKAGE_PCIUTILS=y
IMAGE_ITB_AUX=y IX_IMAGE_ITB_AUX=y
IMAGE_ITB_QCOW=y IX_IMAGE_ITB_QCOW=y
IMAGE_ITB_RAUC=y IX_IMAGE_ITB_RAUC=y
IMAGE_README=y IX_IMAGE_README=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+2 -2
View File
@@ -40,5 +40,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y
BR2_PACKAGE_BOOTLOADER_SPLASHSCREEN=y BR2_PACKAGE_BOOTLOADER_SPLASHSCREEN=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+2 -2
View File
@@ -34,5 +34,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+2 -2
View File
@@ -34,5 +34,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+1 -1
View File
@@ -1,4 +1,4 @@
BR2_TARGET_ROOTFS_EXT2=y BR2_TARGET_ROOTFS_EXT2=y
BR2_TARGET_ROOTFS_EXT2_4=y BR2_TARGET_ROOTFS_EXT2_4=y
BR2_TARGET_ROOTFS_EXT2_SIZE="512M" BR2_TARGET_ROOTFS_EXT2_SIZE="512M"
IMAGE_EXT4_RAUC=y IX_IMAGE_EXT4_RAUC=y
+5
View File
@@ -0,0 +1,5 @@
# Prefer internal download mirror over official upstream sites. If
# the mirror is unreachable, e.g., off-site without VPN, Buildroot
# falls back to the upstream URL and then sources.buildroot.net
# Set up a local mirror and add IP to /etc/hosts to override
BR2_PRIMARY_SITE="http://mirror.internal/pub"
+11 -11
View File
@@ -139,11 +139,11 @@ BR2_PACKAGE_HOST_MTOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y
INFIX_VENDOR_HOME="https://www.kernelkit.org" IX_VENDOR_HOME="https://www.kernelkit.org"
INFIX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers." IX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers."
INFIX_HOME="https://github.com/kernelkit/infix/" IX_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://www.kernelkit.org/infix/" IX_DOC="https://www.kernelkit.org/infix/"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" IX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_FEATURE_GPS=y
BR2_PACKAGE_FEATURE_WIFI=y BR2_PACKAGE_FEATURE_WIFI=y
BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y
@@ -185,9 +185,9 @@ BR2_PACKAGE_WEBUI=y
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
BR2_PACKAGE_HOST_PYTHON_YANGDOC=y BR2_PACKAGE_HOST_PYTHON_YANGDOC=y
BR2_PACKAGE_PCIUTILS=y BR2_PACKAGE_PCIUTILS=y
IMAGE_ITB_AUX=y IX_IMAGE_ITB_AUX=y
IMAGE_ITB_QCOW=y IX_IMAGE_ITB_QCOW=y
IMAGE_ITB_RAUC=y IX_IMAGE_ITB_RAUC=y
IMAGE_README=y IX_IMAGE_README=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+11 -11
View File
@@ -118,11 +118,11 @@ BR2_PACKAGE_HOST_MTOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y
INFIX_VENDOR_HOME="https://www.kernelkit.org" IX_VENDOR_HOME="https://www.kernelkit.org"
INFIX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers." IX_DESC="Infix is an immutable, friendly, and secure operating system that turns any ARM or x86 device into a powerful, manageable network appliance. Deploy on anything from $35 Raspberry Pi boards to enterprise switches as routers, IoT gateways, or edge devices. Infix models Linux networking features using YANG so you can manage your devices using NETCONF/RESTCONF APIs and focus on your business logic running in isolated containers."
INFIX_HOME="https://github.com/kernelkit/infix/" IX_HOME="https://github.com/kernelkit/infix/"
INFIX_DOC="https://www.kernelkit.org/infix/" IX_DOC="https://www.kernelkit.org/infix/"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" IX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_CONFD=y BR2_PACKAGE_CONFD=y
BR2_PACKAGE_NETD=y BR2_PACKAGE_NETD=y
BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_CONFD_TEST_MODE=y
@@ -148,9 +148,9 @@ BR2_PACKAGE_MDNS_ALIAS=y
BR2_PACKAGE_ONIEPROM=y BR2_PACKAGE_ONIEPROM=y
BR2_PACKAGE_ROUSETTE=y BR2_PACKAGE_ROUSETTE=y
BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y
IMAGE_ITB_AUX=y IX_IMAGE_ITB_AUX=y
IMAGE_ITB_QCOW=y IX_IMAGE_ITB_QCOW=y
IMAGE_ITB_RAUC=y IX_IMAGE_ITB_RAUC=y
IMAGE_README=y IX_IMAGE_README=y
TRUSTED_KEYS=y IX_TRUSTED_KEYS=y
TRUSTED_KEYS_DEVELOPMENT=y IX_TRUSTED_KEYS_DEVELOPMENT=y
+16
View File
@@ -3,6 +3,21 @@ Change Log
All notable changes to the project are documented in this file. All notable changes to the project are documented in this file.
[v26.08.0][UNRELEASED]
-------------------------
### Changes
- Add support for firewall address-set (ipset): named sets of IP addresses and
networks, usable as zone sources for per-IP access control, issue #1189
- Build RPi64 SD card images in release builds
- Include .pkg files in release builds
### Fixes
- Fix annoying "cannot deselect all services" or reset to YANG default in the
web interface's firewall configuration page
[v26.06.0][] - 2026-07-01 [v26.06.0][] - 2026-07-01
------------------------- -------------------------
@@ -2206,6 +2221,7 @@ Supported YANG models in addition to those used by sysrepo and netopeer:
[buildroot]: https://buildroot.org/ [buildroot]: https://buildroot.org/
[UNRELEASED]: https://github.com/kernelkit/infix/compare/v26.06.0...HEAD [UNRELEASED]: https://github.com/kernelkit/infix/compare/v26.06.0...HEAD
[v26.08.0]: https://github.com/kernelkit/infix/compare/v26.06.0...v26.08.0
[v26.06.0]: https://github.com/kernelkit/infix/compare/v26.05.0...v26.06.0 [v26.06.0]: https://github.com/kernelkit/infix/compare/v26.05.0...v26.06.0
[v26.05.0]: https://github.com/kernelkit/infix/compare/v26.04.0...v26.05.0 [v26.05.0]: https://github.com/kernelkit/infix/compare/v26.04.0...v26.05.0
[v26.04.0]: https://github.com/kernelkit/infix/compare/v26.03.0...v26.04.0 [v26.04.0]: https://github.com/kernelkit/infix/compare/v26.03.0...v26.04.0
+2 -2
View File
@@ -28,7 +28,7 @@ Verify the result after a build by inspecting:
> [!IMPORTANT] > [!IMPORTANT]
> To get a proper GIT revision (hash) from your OS spin, remember to set > To get a proper GIT revision (hash) from your OS spin, remember to set
> in menuconfig `INFIX_OEM_PATH`. When unset, the Infix `post-build.sh` > in menuconfig `IX_OEM_PATH`. When unset, the Infix `post-build.sh`
> script defaults to the Infix base path. The revision is stored in the > script defaults to the Infix base path. The revision is stored in the
> file `/etc/os-release` as `BUILD_ID`, also in the file `/etc/version`. > file `/etc/os-release` as `BUILD_ID`, also in the file `/etc/version`.
> See below for more info. > See below for more info.
@@ -427,7 +427,7 @@ Used for `BUILD_ID` in `/etc/os-release`.
**Default:** `$(git describe --always --dirty --tags)`, from the _top **Default:** `$(git describe --always --dirty --tags)`, from the _top
directory_. By default, the top directory refers to the root of the directory_. By default, the top directory refers to the root of the
Infix source tree, but this can be changed by setting the branding Infix source tree, but this can be changed by setting the branding
variable `INFIX_OEM_PATH`, e.g. in a `defconfig` file or via `make variable `IX_OEM_PATH`, e.g. in a `defconfig` file or via `make
menuconfig`, to the path of an enclosing br2-external. menuconfig`, to the path of an enclosing br2-external.
#### `INFIX_RELEASE` #### `INFIX_RELEASE`
+3
View File
@@ -190,6 +190,9 @@ To apply a single snippet to the current output directory:
make apply-ext4 # build an ext4 rootfs (needed for boards make apply-ext4 # build an ext4 rootfs (needed for boards
# whose bootloader lacks squashfs support, # whose bootloader lacks squashfs support,
# e.g. Marvell ESPRESSObin) # e.g. Marvell ESPRESSObin)
make apply-mirror # prefer an internal download mirror
# (BR2_PRIMARY_SITE) over upstream sites,
# see utils/mirror-sync.sh for populating it
The `apply-*` targets require an existing `.config` (i.e. you must have The `apply-*` targets require an existing `.config` (i.e. you must have
already run a `make <board>_defconfig`). The snippet is merged using already run a `make <board>_defconfig`). The snippet is merged using
+90
View File
@@ -223,6 +223,96 @@ The firewall includes over 100 pre-defined services, such as:
> See the YANG model for the full list, or tap the ++question++ key > See the YANG model for the full list, or tap the ++question++ key
> when setting up an allowed host service in a zone `set service` > when setting up an allowed host service in a zone `set service`
## Address Sets
Address sets are named collections of IP addresses and networks that can be
used as zone *sources*, alongside the `network` setting. Traffic from a
member of the set is classified into that zone regardless of which interface
it arrives on. Since source matching takes precedence over interface
matching, an address set in a trusted zone can selectively lift devices out
of a restrictive interface zone.
This enables per-IP access control: block everything by default and grant
individual end devices access at runtime.
> [!IMPORTANT]
> Assigning an address set to a zone only decides which zone the source IP
> belongs to. It does **not** by itself grant access to the device. Access
> to HOST services is still controlled by the zone's `action` and `service`
> settings. A common pattern is to keep the interface or default zone
> restrictive (`reject`/`drop`) and attach the address set to a separate
> trusted zone with `action accept`, as shown below.
<pre class="cli"><code>admin@example:/> <b>configure</b>
admin@example:/config/> <b>edit firewall address-set allowed</b>
admin@example:/config/firewall/…/allowed/> <b>set description "End devices granted access"</b>
admin@example:/config/firewall/…/allowed/> <b>set entry 192.168.1.40</b>
admin@example:/config/firewall/…/allowed/> <b>end</b>
admin@example:/config/firewall/> <b>edit zone trusted</b>
admin@example:/config/firewall/…/trusted/> <b>set action accept</b>
admin@example:/config/firewall/…/trusted/> <b>set address-set allowed</b>
admin@example:/config/firewall/…/trusted/> <b>leave</b>
</code></pre>
### Static and Dynamic Entries
Entries come in two kinds:
- **Static** entries are set in the configuration, like `192.168.1.40`
above, and are restored at boot
- **Dynamic** entries are added and removed at runtime using the `add`,
`remove`, and `flush` actions. They take effect immediately and survive
firewall configuration changes, but are *not* saved to the configuration,
so a reboot starts from a clean slate
From admin-exec context in the CLI:
<pre class="cli"><code>admin@example:/> <b>firewall address-set allowed add 192.168.1.42</b>
admin@example:/> <b>show firewall address-set allowed</b>
name : allowed
family : ipv4
timeout : none
ENTRY TYPE EXPIRES
192.168.1.40 static
192.168.1.42 dynamic
admin@example:/> <b>firewall address-set allowed remove 192.168.1.42</b>
</code></pre>
The same actions are available over NETCONF and RESTCONF, e.g., allowing a
device from a network management system:
```json
~$ curl -kX POST -u admin:admin -H "Content-Type: application/yang-data+json" \
-d '{"infix-firewall:input": {"entry": "192.168.1.42"}}' \
https://example.local/restconf/data/infix-firewall:firewall/address-set=allowed/add
```
Static entries can only be removed by changing the configuration, the
`remove` action manages dynamic entries only. The `flush` action removes
all dynamic entries at once, leaving static entries in place.
### Expiring Entries
An address set can be created with a `timeout`, giving every dynamic entry a
limited lifetime. Such sets are dynamic-only: static entries cannot be
configured, and entries cannot be removed manually, they expire on their
own. This suits time-limited access grants and automated ban lists.
<pre class="cli"><code>admin@example:/config/firewall/> <b>edit address-set banned</b>
admin@example:/config/firewall/…/banned/> <b>set timeout 3600</b>
admin@example:/config/firewall/…/banned/> <b>leave</b>
admin@example:/> <b>firewall address-set banned add 203.0.113.99</b>
</code></pre>
The remaining lifetime of each entry is shown in the `EXPIRES` column of
<kbd>show firewall address-set</kbd>.
> [!NOTE]
> Entries in timeout sets do not survive firewall configuration changes,
> the set is flushed when the firewall configuration is rebuilt. Regular
> (non-timeout) sets keep their dynamic entries over configuration changes.
## Examples ## Examples
### End Device Protection ### End Device Protection
+2 -2
View File
@@ -1,6 +1,6 @@
IXMSG = printf "\e[37;44m>>> $(call qstrip,$(1))\e[0m\n" IXMSG = printf "\e[37;44m>>> $(call qstrip,$(1))\e[0m\n"
oem-dir := $(call qstrip,$(INFIX_OEM_PATH)) oem-dir := $(call qstrip,$(IX_OEM_PATH))
INFIX_TOPDIR = $(if $(oem-dir),$(oem-dir),$(BR2_EXTERNAL_INFIX_PATH)) INFIX_TOPDIR = $(if $(oem-dir),$(oem-dir),$(BR2_EXTERNAL_INFIX_PATH))
# Unless the user specifies an explicit build id, source it from git. # Unless the user specifies an explicit build id, source it from git.
@@ -10,6 +10,6 @@ INFIX_TOPDIR = $(if $(oem-dir),$(oem-dir),$(BR2_EXTERNAL_INFIX_PATH))
# artifacts (see INFIX_ARTIFACT below). # artifacts (see INFIX_ARTIFACT below).
export INFIX_BUILD_ID ?= $(shell git -C $(INFIX_TOPDIR) describe --dirty --always --tags --exclude 'latest*') export INFIX_BUILD_ID ?= $(shell git -C $(INFIX_TOPDIR) describe --dirty --always --tags --exclude 'latest*')
export INFIX_VERSION = $(INFIX_BUILD_ID) export INFIX_VERSION = $(INFIX_BUILD_ID)
export INFIX_ARTIFACT = $(call qstrip,$(INFIX_IMAGE_ID)$(if $(INFIX_RELEASE),-$(INFIX_RELEASE))) export INFIX_ARTIFACT = $(call qstrip,$(IX_IMAGE_ID)$(if $(INFIX_RELEASE),-$(INFIX_RELEASE)))
INFIX_CFLAGS:=-Wall -Werror -Wextra -Wno-unused-parameter -Wformat=2 -Wformat-overflow=2 -Winit-self -Wstrict-overflow=4 -Wno-format-truncation -Wno-format-nonliteral INFIX_CFLAGS:=-Wall -Werror -Wextra -Wno-unused-parameter -Wformat=2 -Wformat-overflow=2 -Winit-self -Wstrict-overflow=4 -Wno-format-truncation -Wno-format-nonliteral
+1
View File
@@ -0,0 +1 @@
sha256 49c4d5edbd3ee0a0f6b15ef097b79539530a574ceed8a4dffec1d02455933029 LICENSE
+1
View File
@@ -0,0 +1 @@
sha256 8cdb4def6e91fde66ed0880a6e6ecc364aa96012e53b3cfb990b53b9dab976d4 LICENSE
+30
View File
@@ -103,6 +103,20 @@ panic_off()
logger -p user.emerg "LOCKDOWN MODE DEACTIVATED - Normal network operation restored" logger -p user.emerg "LOCKDOWN MODE DEACTIVATED - Normal network operation restored"
} }
ipset_call()
{
method=$1
name=$2
entry=$3
if ! output=$(gdbus call --system --dest "$DEST" --object-path "$OBJECT" \
--method "$INTERFACE.ipset.$method" "$name" "$entry" 2>&1); then
logger -t firewall -p daemon.err "ipset $method $name $entry: $output"
print "Error: $output" >&2
return 1
fi
}
panic_status() panic_status()
{ {
if is_panic_enabled; then if is_panic_enabled; then
@@ -294,6 +308,7 @@ OPTIONS:
COMMANDS: COMMANDS:
reload Reload firewall configuration reload Reload firewall configuration
panic OPERATION Emergency panic mode: <on | off | status> panic OPERATION Emergency panic mode: <on | off | status>
ipset CMD SET ENTRY Runtime ipset operation: <add | del> SET ENTRY
show Show comprehensive firewall status and configuration show Show comprehensive firewall status and configuration
help Show this help message help Show this help message
@@ -386,6 +401,21 @@ main()
;; ;;
esac esac
;; ;;
ipset)
case "${2:-}" in
add)
ipset_call addEntry "$3" "$4"
;;
del)
ipset_call removeEntry "$3" "$4"
;;
*)
echo "Error: Invalid ipset operation '$2'" >&2
echo "Use: $0 ipset {add|del} SET ENTRY" >&2
exit 1
;;
esac
;;
show) show)
show_status show_status
;; ;;
+370 -1
View File
@@ -6,6 +6,7 @@
#include <unistd.h> #include <unistd.h>
#include <errno.h> #include <errno.h>
#include <dirent.h> #include <dirent.h>
#include <arpa/inet.h>
#include <srx/common.h> #include <srx/common.h>
#include <srx/lyx.h> #include <srx/lyx.h>
@@ -24,6 +25,11 @@
#define FIREWALLD_ZONES_DIR FIREWALLD_DIR_NEXT "/zones" #define FIREWALLD_ZONES_DIR FIREWALLD_DIR_NEXT "/zones"
#define FIREWALLD_SERVICES_DIR FIREWALLD_DIR_NEXT "/services" #define FIREWALLD_SERVICES_DIR FIREWALLD_DIR_NEXT "/services"
#define FIREWALLD_POLICIES_DIR FIREWALLD_DIR_NEXT "/policies" #define FIREWALLD_POLICIES_DIR FIREWALLD_DIR_NEXT "/policies"
#define FIREWALLD_IPSETS_DIR FIREWALLD_DIR_NEXT "/ipsets"
#define IPSETS_ACTIVE_DIR FIREWALLD_DIR "/ipsets"
#define ADDRSET_RUNDIR "/run/confd/address-sets"
#define ENTRY_STRLEN 64 /* worst-case ip-prefix + margin */
static struct { static struct {
const char *yang; const char *yang;
@@ -64,6 +70,148 @@ static const char *policy_action_to_target(const char *action)
return policy_action_map[0].yang; return policy_action_map[0].yang;
} }
struct prefix {
int af;
uint8_t addr[16];
int len;
};
static int prefix_parse(const char *str, struct prefix *p)
{
char buf[ENTRY_STRLEN];
char *sep;
strlcpy(buf, str, sizeof(buf));
sep = strchr(buf, '/');
if (sep) {
*sep++ = 0;
p->len = atoi(sep);
} else {
p->len = -1;
}
if (inet_pton(AF_INET, buf, p->addr) == 1) {
p->af = AF_INET;
if (p->len < 0)
p->len = 32;
return 0;
}
if (inet_pton(AF_INET6, buf, p->addr) == 1) {
p->af = AF_INET6;
if (p->len < 0)
p->len = 128;
return 0;
}
return -1;
}
static bool prefix_overlap(const char *a, const char *b)
{
struct prefix pa, pb;
int len, i;
if (prefix_parse(a, &pa) || prefix_parse(b, &pb) || pa.af != pb.af)
return false;
len = pa.len < pb.len ? pa.len : pb.len;
for (i = 0; i < len / 8; i++) {
if (pa.addr[i] != pb.addr[i])
return false;
}
if (len % 8) {
uint8_t mask = 0xff << (8 - len % 8);
if ((pa.addr[i] & mask) != (pb.addr[i] & mask))
return false;
}
return true;
}
static bool shadow_has(const char *name, const char *entry)
{
char line[ENTRY_STRLEN];
bool found = false;
FILE *fp;
fp = fopenf("r", ADDRSET_RUNDIR "/%s", name);
if (!fp)
return false;
while (fgets(line, sizeof(line), fp)) {
chomp(line);
if (!strcmp(line, entry)) {
found = true;
break;
}
}
fclose(fp);
return found;
}
static int shadow_add(const char *name, const char *entry)
{
char line[ENTRY_STRLEN];
FILE *fp;
if (fmkpath(0755, ADDRSET_RUNDIR) && errno != EEXIST) {
ERRNO("Failed creating " ADDRSET_RUNDIR);
return -1;
}
fp = fopenf("a+", ADDRSET_RUNDIR "/%s", name);
if (!fp) {
ERRNO("Failed recording dynamic entry for address-set %s", name);
return -1;
}
while (fgets(line, sizeof(line), fp)) {
chomp(line);
if (!strcmp(line, entry)) {
fclose(fp);
return 0;
}
}
fprintf(fp, "%s\n", entry);
fclose(fp);
return 0;
}
static int shadow_del(const char *name, const char *entry)
{
char curr[sizeof(ADDRSET_RUNDIR) + ENTRY_STRLEN], next[sizeof(curr) + 1];
char line[ENTRY_STRLEN];
FILE *in, *out;
snprintf(curr, sizeof(curr), ADDRSET_RUNDIR "/%s", name);
snprintf(next, sizeof(next), "%s+", curr);
in = fopen(curr, "r");
if (!in)
return -1;
out = fopen(next, "w");
if (!out) {
fclose(in);
return -1;
}
while (fgets(line, sizeof(line), in)) {
chomp(line);
if (!strcmp(line, entry))
continue;
fprintf(out, "%s\n", line);
}
fclose(in);
fclose(out);
return rename(next, curr);
}
static void mark_interfaces_used(struct lyd_node *cfg, char **ifaces) static void mark_interfaces_used(struct lyd_node *cfg, char **ifaces)
{ {
struct lyd_node *node; struct lyd_node *node;
@@ -177,6 +325,9 @@ static int generate_zone(struct lyd_node *cfg, const char *name, char **ifaces)
LYX_LIST_FOR_EACH(lyd_child(cfg), node, "network") LYX_LIST_FOR_EACH(lyd_child(cfg), node, "network")
fprintf(fp, " <source address=\"%s\"/>\n", lyd_get_value(node)); fprintf(fp, " <source address=\"%s\"/>\n", lyd_get_value(node));
LYX_LIST_FOR_EACH(lyd_child(cfg), node, "address-set")
fprintf(fp, " <source ipset=\"%s\"/>\n", lyd_get_value(node));
LYX_LIST_FOR_EACH(lyd_child(cfg), node, "service") LYX_LIST_FOR_EACH(lyd_child(cfg), node, "service")
fprintf(fp, " <service name=\"%s\"/>\n", lyd_get_value(node)); fprintf(fp, " <service name=\"%s\"/>\n", lyd_get_value(node));
@@ -219,6 +370,85 @@ static int generate_zone(struct lyd_node *cfg, const char *name, char **ifaces)
return close_file(fp); return close_file(fp);
} }
/*
* Dynamic entries, added at runtime with the add action, are folded
* into the generated ipset as regular entries so they survive the
* firewalld reload triggered by configuration changes. Entries that
* overlap new static configuration are dropped -- config wins, and
* nftables refuses overlapping elements in interval sets.
*/
static void merge_dynamic(FILE *fp, struct lyd_node *cfg, const char *name)
{
char line[ENTRY_STRLEN];
FILE *sf;
sf = fopenf("r", ADDRSET_RUNDIR "/%s", name);
if (!sf)
return;
while (fgets(line, sizeof(line), sf)) {
struct lyd_node *node;
bool skip = false;
chomp(line);
if (!line[0])
continue;
LYX_LIST_FOR_EACH(lyd_child(cfg), node, "entry") {
if (prefix_overlap(line, lyd_get_value(node))) {
skip = true;
break;
}
}
if (skip) {
NOTE("address-set %s: dropping dynamic entry %s, overlaps static entry",
name, line);
continue;
}
fprintf(fp, " <entry>%s</entry>\n", line);
}
fclose(sf);
}
static int generate_ipset(struct lyd_node *cfg, const char *name)
{
const char *family, *timeout, *desc;
struct lyd_node *node;
FILE *fp;
fp = open_file(FIREWALLD_IPSETS_DIR, name);
if (!fp)
return SR_ERR_SYS;
desc = lydx_get_cattr(cfg, "description");
family = lydx_get_cattr(cfg, "family");
timeout = lydx_get_cattr(cfg, "timeout");
fprintf(fp, "<ipset type=\"hash:net\">\n");
fprintf(fp, " <short>%s</short>\n", name);
if (desc)
fprintf(fp, " <description>%s</description>\n", desc);
fprintf(fp, " <option name=\"family\" value=\"%s\"/>\n",
family && !strcmp(family, "ipv6") ? "inet6" : "inet");
if (timeout)
fprintf(fp, " <option name=\"timeout\" value=\"%s\"/>\n", timeout);
LYX_LIST_FOR_EACH(lyd_child(cfg), node, "entry")
fprintf(fp, " <entry>%s</entry>\n", lyd_get_value(node));
if (!timeout)
merge_dynamic(fp, cfg, name);
fprintf(fp, "</ipset>\n");
return close_file(fp);
}
static int generate_service(struct lyd_node *cfg, const char *name) static int generate_service(struct lyd_node *cfg, const char *name)
{ {
const char *desc; const char *desc;
@@ -511,9 +741,17 @@ int firewall_change(sr_session_ctx_t *session, struct lyd_node *config, struct l
if (!fisdir(FIREWALLD_DIR_NEXT)) { if (!fisdir(FIREWALLD_DIR_NEXT)) {
/* Firewall is disabled */ /* Firewall is disabled */
finit_disable("firewalld"); finit_disable("firewalld");
rmrf(ADDRSET_RUNDIR);
return SR_ERR_OK; return SR_ERR_OK;
} }
/* Drop dynamic state of deleted address-sets */
clist = lydx_get_descendant(diff, "firewall", "address-set", NULL);
LYX_LIST_FOR_EACH(clist, cnode, "address-set") {
if (lydx_get_op(cnode) == LYDX_OP_DELETE)
erasef(ADDRSET_RUNDIR "/%s", lydx_get_cattr(cnode, "name"));
}
/* Firewall is enabled, roll in new configuration */ /* Firewall is enabled, roll in new configuration */
rmrf(FIREWALLD_DIR); rmrf(FIREWALLD_DIR);
if (rename(FIREWALLD_DIR_NEXT, FIREWALLD_DIR)) { if (rename(FIREWALLD_DIR_NEXT, FIREWALLD_DIR)) {
@@ -551,7 +789,8 @@ int firewall_change(sr_session_ctx_t *session, struct lyd_node *config, struct l
if (fmkpath(0755, FIREWALLD_DIR_NEXT) || if (fmkpath(0755, FIREWALLD_DIR_NEXT) ||
fmkpath(0755, FIREWALLD_ZONES_DIR) || fmkpath(0755, FIREWALLD_ZONES_DIR) ||
fmkpath(0755, FIREWALLD_SERVICES_DIR) || fmkpath(0755, FIREWALLD_SERVICES_DIR) ||
fmkpath(0755, FIREWALLD_POLICIES_DIR)) { fmkpath(0755, FIREWALLD_POLICIES_DIR) ||
fmkpath(0755, FIREWALLD_IPSETS_DIR)) {
ERRNO("Failed creating " FIREWALLD_DIR_NEXT " directory structure"); ERRNO("Failed creating " FIREWALLD_DIR_NEXT " directory structure");
err = SR_ERR_SYS; err = SR_ERR_SYS;
goto done; goto done;
@@ -622,6 +861,11 @@ int firewall_change(sr_session_ctx_t *session, struct lyd_node *config, struct l
LYX_LIST_FOR_EACH(clist, cnode, "service") LYX_LIST_FOR_EACH(clist, cnode, "service")
generate_service(cnode, lydx_get_cattr(cnode, "name")); generate_service(cnode, lydx_get_cattr(cnode, "name"));
/* Regenerate all address-sets, incl. dynamic entries */
clist = lydx_get_descendant(tree, "firewall", "address-set", NULL);
LYX_LIST_FOR_EACH(clist, cnode, "address-set")
generate_ipset(cnode, lydx_get_cattr(cnode, "name"));
/* Regenerate all policies with sequential priority allocation */ /* Regenerate all policies with sequential priority allocation */
clist = lydx_get_descendant(tree, "firewall", "policy", NULL); clist = lydx_get_descendant(tree, "firewall", "policy", NULL);
LYX_LIST_FOR_EACH(clist, cnode, "policy") { LYX_LIST_FOR_EACH(clist, cnode, "policy") {
@@ -701,6 +945,128 @@ static int cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module,
return SR_ERR_OK; return SR_ERR_OK;
} }
static int addrset_flush(const char *name)
{
char line[ENTRY_STRLEN];
FILE *fp;
fp = fopenf("r", ADDRSET_RUNDIR "/%s", name);
if (!fp)
return SR_ERR_OK; /* no dynamic entries */
while (fgets(line, sizeof(line), fp)) {
chomp(line);
if (!line[0])
continue;
if (systemf("firewall ipset del %s %s", name, line))
ERROR("address-set %s: failed removing dynamic entry %s", name, line);
}
fclose(fp);
erasef(ADDRSET_RUNDIR "/%s", name);
return SR_ERR_OK;
}
/*
* Canonicalize like firewalld: host entries lose their prefix length,
* IPv6 is compressed. Keeps shadow file lookups exact-match.
*/
static const char *entry_canon(const char *entry, char *buf, size_t len)
{
char addr[INET6_ADDRSTRLEN];
struct prefix p;
if (prefix_parse(entry, &p) || !inet_ntop(p.af, p.addr, addr, sizeof(addr)))
return entry;
if ((p.af == AF_INET && p.len == 32) || (p.af == AF_INET6 && p.len == 128))
snprintf(buf, len, "%s", addr);
else
snprintf(buf, len, "%s/%d", addr, p.len);
return buf;
}
static int addrset(sr_session_ctx_t *session, uint32_t sub_id, const char *xpath,
const sr_val_t *input, const size_t input_cnt, sr_event_t event,
uint32_t request_id, sr_val_t **output, size_t *output_cnt, void *priv)
{
char buf[strlen(xpath) + 1], canon[ENTRY_STRLEN], name[65];
const char *cmd = (const char *)priv;
const char *entry = NULL;
sr_xpath_ctx_t state = {};
sr_session_ctx_t *cfg;
char *val;
bool timeout;
/* /infix-firewall:firewall/address-set[name='allowed']/add */
strlcpy(buf, xpath, sizeof(buf));
val = sr_xpath_key_value(buf, "address-set", "name", &state);
if (!val)
return SR_ERR_INTERNAL;
strlcpy(name, val, sizeof(name));
if (input_cnt > 0)
entry = entry_canon(input[0].data.string_val, canon, sizeof(canon));
if (sr_session_start(sr_session_get_connection(session), SR_DS_RUNNING, &cfg))
return SR_ERR_INTERNAL;
val = srx_get_str(cfg, XPATH "/address-set[name='%s']/name", name);
if (!val) {
sr_session_stop(cfg);
sr_session_set_error_message(session, "No such address-set: %s", name);
return SR_ERR_INVAL_ARG;
}
free(val);
val = srx_get_str(cfg, XPATH "/address-set[name='%s']/timeout", name);
timeout = val != NULL;
free(val);
sr_session_stop(cfg);
DEBUG("address-set %s: %s %s", name, cmd, entry ?: "");
if (!strcmp(cmd, "add")) {
if (systemf("firewall ipset add %s %s", name, entry)) {
sr_session_set_error_message(session, "Failed adding %s to address-set %s, "
"see log for details", entry, name);
return SR_ERR_OPERATION_FAILED;
}
if (!timeout)
shadow_add(name, entry);
return SR_ERR_OK;
}
if (timeout) {
sr_session_set_error_message(session, "Entries in address-set %s expire on "
"their own (timeout set)", name);
return SR_ERR_UNSUPPORTED;
}
if (!strcmp(cmd, "flush"))
return addrset_flush(name);
/* remove */
if (!shadow_has(name, entry)) {
sr_session_set_error_message(session, "%s is not a dynamic entry in address-set %s, "
"static entries are removed via configuration", entry, name);
return SR_ERR_INVAL_ARG;
}
if (systemf("firewall ipset del %s %s", name, entry)) {
sr_session_set_error_message(session, "Failed removing %s from address-set %s, "
"see log for details", entry, name);
return SR_ERR_OPERATION_FAILED;
}
shadow_del(name, entry);
return SR_ERR_OK;
}
static int lockdown(sr_session_ctx_t *session, uint32_t sub_id, const char *xpath, static int lockdown(sr_session_ctx_t *session, uint32_t sub_id, const char *xpath,
const sr_val_t *input, const size_t input_cnt, sr_event_t event, const sr_val_t *input, const size_t input_cnt, sr_event_t event,
uint32_t request_id, sr_val_t **output, size_t *output_cnt, void *priv) uint32_t request_id, sr_val_t **output, size_t *output_cnt, void *priv)
@@ -723,6 +1089,9 @@ int firewall_rpc_init(struct confd *confd)
int rc; int rc;
REGISTER_RPC(confd->session, XPATH "/lockdown-mode", lockdown, NULL, &confd->sub); REGISTER_RPC(confd->session, XPATH "/lockdown-mode", lockdown, NULL, &confd->sub);
REGISTER_RPC(confd->session, XPATH "/address-set/add", addrset, "add", &confd->sub);
REGISTER_RPC(confd->session, XPATH "/address-set/remove", addrset, "remove", &confd->sub);
REGISTER_RPC(confd->session, XPATH "/address-set/flush", addrset, "flush", &confd->sub);
return SR_ERR_OK; return SR_ERR_OK;
fail: fail:
+1 -1
View File
@@ -38,7 +38,7 @@ MODULES=(
"infix-dhcp-client@2025-11-09.yang" "infix-dhcp-client@2025-11-09.yang"
"infix-dhcpv6-client@2025-11-09.yang" "infix-dhcpv6-client@2025-11-09.yang"
"infix-dhcp-server@2025-10-28.yang" "infix-dhcp-server@2025-10-28.yang"
"infix-firewall@2025-04-26.yang" "infix-firewall@2026-07-02.yang"
"infix-firewall-services@2025-04-26.yang" "infix-firewall-services@2025-04-26.yang"
"infix-firewall-icmp-types@2025-04-26.yang" "infix-firewall-icmp-types@2025-04-26.yang"
"infix-meta@2025-12-10.yang" "infix-meta@2025-12-10.yang"
+153
View File
@@ -27,6 +27,13 @@ module infix-firewall {
contact "kernelkit@googlegroups.com"; contact "kernelkit@googlegroups.com";
description "Zone-based firewall inspired by firewalld concepts."; description "Zone-based firewall inspired by firewalld concepts.";
revision 2026-07-02 {
description "Add address-set support: named sets of IP addresses and
networks, usable as zone sources. Includes add/remove/flush
actions for managing dynamic entries at runtime.";
reference "internal";
}
revision 2025-04-26 { revision 2025-04-26 {
description "Initial revision."; description "Initial revision.";
reference "internal"; reference "internal";
@@ -95,6 +102,14 @@ module infix-firewall {
description "Action for traffic that does not match any specific service or port entry."; description "Action for traffic that does not match any specific service or port entry.";
} }
typedef set-entry {
description "A member of an address-set: a host address or a network prefix.";
type union {
type inet:ip-address;
type inet:ip-prefix;
}
}
typedef protocol-type { typedef protocol-type {
description "Network protocols supported for services and port definitions."; description "Network protocols supported for services and port definitions.";
@@ -220,6 +235,21 @@ module infix-firewall {
type inet:ip-prefix; type inet:ip-prefix;
} }
leaf-list address-set {
description "Address sets whose members are sources for this zone.
Like 'network', but members can also be added and removed
at runtime using the address-set add/remove actions.
Source matching takes precedence over interface matching,
so a member of an address set is classified into this zone
even when its traffic arrives on an interface assigned to
another zone.";
type leafref {
path "../../address-set/name";
}
}
leaf-list service { leaf-list service {
description "Services allowed from this zone to HOST (INPUT chain only). description "Services allowed from this zone to HOST (INPUT chain only).
@@ -460,6 +490,129 @@ module infix-firewall {
} }
} }
list address-set {
description "Named sets of IP addresses and networks, usable as zone sources.
Entries configured here are static: part of the configuration
and restored at boot. Entries can also be added and removed
at runtime with the add/remove actions. Dynamic entries are
not saved to the configuration and are lost on reboot.
The 'current' list shows the actual contents of the set,
both static and dynamic entries.";
key "name";
must "not(timeout) or count(entry) = 0" {
error-message "Address sets with a timeout cannot have static entries";
}
leaf name {
description "Name of the address set.";
type ident;
}
leaf description {
description "Free-form description of the address set.";
type string {
pattern '[^\p{Cc}<>&]*';
}
}
leaf family {
description "Address family of the set. A set holds either IPv4 or
IPv6 entries, never both.";
type enumeration {
enum ipv4;
enum ipv6;
}
default ipv4;
}
leaf timeout {
description "Lifetime for dynamic entries, making the set dynamic-only.
Entries added with the add action expire after this many
seconds. Static entries cannot be configured, and dynamic
entries cannot be removed or flushed, they expire on their
own. Changes to the firewall configuration flush all
entries from timeout sets early.";
type uint32 {
range "1..max";
}
units "seconds";
}
leaf-list entry {
description "Static entries, part of the configuration.";
type set-entry;
must "not(contains(., ':')) or ../family = 'ipv6'" {
error-message "IPv6 entry in an IPv4 address-set";
}
must "contains(., ':') or ../family = 'ipv4'" {
error-message "IPv4 entry in an IPv6 address-set";
}
}
list current {
description "Entries currently active in the set.";
config false;
key "entry";
leaf entry {
description "Set member.";
type set-entry;
}
leaf dynamic {
description "Entry was added at runtime, not part of the configuration.";
type boolean;
}
leaf expires {
description "Seconds until this entry expires and is removed.";
type uint32;
units "seconds";
}
}
action add {
description "Add a dynamic entry to the set.
The entry takes effect immediately but is not saved to the
configuration, i.e., it is lost on reboot. For permanent
entries, use the 'entry' list instead.";
input {
leaf entry {
description "Host address or network prefix to add.";
type set-entry;
mandatory true;
}
}
}
action remove {
description "Remove a dynamic entry from the set.
Static entries cannot be removed with this action, only
by changing the configuration. Entries in timeout sets
cannot be removed, they expire on their own.";
input {
leaf entry {
description "Host address or network prefix to remove.";
type set-entry;
mandatory true;
}
}
}
action flush {
description "Remove all dynamic entries from the set. Static entries
remain. Not supported for timeout sets, their entries
expire on their own.";
}
}
leaf lockdown { leaf lockdown {
description "Current state of emergency lockdown mode."; description "Current state of emergency lockdown mode.";
config false; config false;
+60
View File
@@ -297,6 +297,64 @@ int infix_firewall_services(kcontext_t *ctx)
"| sed 's/^(//; s/,)$//' | tr \"'\" '\"' | jq -r '.[]' 2>/dev/null"); "| sed 's/^(//; s/,)$//' | tr \"'\" '\"' | jq -r '.[]' 2>/dev/null");
} }
/*
* Completion function for firewall address-sets (ipsets).
* D-Bus returns variant format: (['set1', 'set2'],)
*/
int infix_firewall_addrsets(kcontext_t *ctx)
{
(void)ctx;
return firewall_dbus_completion("ipset", "getIPSets",
"sed 's/^(//; s/,)$//' | tr \"'\" '\"' | jq -r '.[]' 2>/dev/null");
}
/*
* Send address-set action (add/remove/flush) using the rpc tool, as the
* logged-in user to honor NACM. srp_rpc from klish-plugin-sysrepo only
* handles fixed xpaths, these actions live on list entries.
*/
int infix_firewall_addrset_action(kcontext_t *ctx)
{
kpargv_t *pargv = kcontext_pargv(ctx);
const char *name, *op;
char xpath[128];
char *argv[8];
kparg_t *parg;
int i = 0;
parg = kpargv_find(pargv, "setname");
if (!parg) {
fprintf(stderr, ERRMSG "missing address-set name\n");
return -1;
}
name = kparg_value(parg);
if (kpargv_find(pargv, "add"))
op = "add";
else if (kpargv_find(pargv, "remove"))
op = "remove";
else
op = "flush";
snprintf(xpath, sizeof(xpath),
"/infix-firewall:firewall/address-set[name='%s']/%s", name, op);
argv[i++] = "doas";
argv[i++] = "-u";
argv[i++] = (char *)cd_home(ctx);
argv[i++] = "rpc";
argv[i++] = xpath;
parg = kpargv_find(pargv, "entry");
if (parg) {
argv[i++] = "entry";
argv[i++] = (char *)kparg_value(parg);
}
argv[i] = NULL;
return run(argv);
}
int infix_copy(kcontext_t *ctx) int infix_copy(kcontext_t *ctx)
{ {
kpargv_t *pargv = kcontext_pargv(ctx); kpargv_t *pargv = kcontext_pargv(ctx);
@@ -689,6 +747,8 @@ int kplugin_infix_init(kcontext_t *ctx)
kplugin_add_syms(plugin, ksym_new("firewall_zones", infix_firewall_zones)); kplugin_add_syms(plugin, ksym_new("firewall_zones", infix_firewall_zones));
kplugin_add_syms(plugin, ksym_new("firewall_policies", infix_firewall_policies)); kplugin_add_syms(plugin, ksym_new("firewall_policies", infix_firewall_policies));
kplugin_add_syms(plugin, ksym_new("firewall_services", infix_firewall_services)); kplugin_add_syms(plugin, ksym_new("firewall_services", infix_firewall_services));
kplugin_add_syms(plugin, ksym_new("firewall_addrsets", infix_firewall_addrsets));
kplugin_add_syms(plugin, ksym_new("firewall_addrset_action", infix_firewall_addrset_action));
kplugin_add_syms(plugin, ksym_new("set_boot_order", infix_set_boot_order)); kplugin_add_syms(plugin, ksym_new("set_boot_order", infix_set_boot_order));
kplugin_add_syms(plugin, ksym_new("shell", infix_shell)); kplugin_add_syms(plugin, ksym_new("shell", infix_shell));
kplugin_add_syms(plugin, ksym_new("ssh_connect", infix_ssh_connect)); kplugin_add_syms(plugin, ksym_new("ssh_connect", infix_ssh_connect));
+31
View File
@@ -134,6 +134,13 @@
<ACTION sym="STRING"/> <ACTION sym="STRING"/>
</PTYPE> </PTYPE>
<PTYPE name="FIREWALL_ADDRSETS">
<COMPL>
<ACTION sym="firewall_addrsets@infix"/>
</COMPL>
<ACTION sym="STRING"/>
</PTYPE>
<PTYPE name="BOOT_TARGET"> <PTYPE name="BOOT_TARGET">
<COMPL> <COMPL>
<ACTION sym="boot_targets@infix"/> <ACTION sym="boot_targets@infix"/>
@@ -811,6 +818,14 @@ echo "Public: $pub"
copy operational -x /infix-firewall:firewall | /usr/libexec/statd/cli-pretty show-firewall-service "$KLISH_PARAM_name" |pager copy operational -x /infix-firewall:firewall | /usr/libexec/statd/cli-pretty show-firewall-service "$KLISH_PARAM_name" |pager
</ACTION> </ACTION>
</COMMAND> </COMMAND>
<COMMAND name="address-set" help="Show firewall address-sets">
<SWITCH name="optional" min="0">
<PARAM name="name" ptype="/FIREWALL_ADDRSETS" help="Address-set name"/>
</SWITCH>
<ACTION sym="script" in="tty" out="tty" interrupt="true">
copy operational -x /infix-firewall:firewall | /usr/libexec/statd/cli-pretty show-firewall-address-set "$KLISH_PARAM_name" |pager
</ACTION>
</COMMAND>
</SWITCH> </SWITCH>
<ACTION sym="script" in="tty" out="tty" interrupt="true"> <ACTION sym="script" in="tty" out="tty" interrupt="true">
copy operational -x /infix-firewall:firewall | /usr/libexec/statd/cli-pretty show-firewall |pager copy operational -x /infix-firewall:firewall | /usr/libexec/statd/cli-pretty show-firewall |pager
@@ -826,6 +841,22 @@ echo "Public: $pub"
</COMMAND> </COMMAND>
<COMMAND name="firewall" help="Control the firewall" mode="switch"> <COMMAND name="firewall" help="Control the firewall" mode="switch">
<COMMAND name="address-set" help="Manage dynamic address-set entries">
<PARAM name="setname" ptype="/FIREWALL_ADDRSETS" help="Address-set name"/>
<SWITCH name="op">
<COMMAND name="add" help="Add dynamic entry, not saved to configuration">
<PARAM name="entry" ptype="/STRING" help="IP address or network prefix"/>
<ACTION sym="firewall_addrset_action@infix"/>
</COMMAND>
<COMMAND name="remove" help="Remove dynamic entry">
<PARAM name="entry" ptype="/STRING" help="IP address or network prefix"/>
<ACTION sym="firewall_addrset_action@infix"/>
</COMMAND>
<COMMAND name="flush" help="Remove all dynamic entries">
<ACTION sym="firewall_addrset_action@infix"/>
</COMMAND>
</SWITCH>
</COMMAND>
<COMMAND name="lockdown" help="Emergency lockdown mode"> <COMMAND name="lockdown" help="Emergency lockdown mode">
<PARAM name="operation" ptype="/STRING" help="Lockdown commands."> <PARAM name="operation" ptype="/STRING" help="Lockdown commands.">
<COMPL> <COMPL>
+103 -11
View File
@@ -4149,6 +4149,7 @@ def show_firewall(json):
# Create tables # Create tables
zone_table = firewall_zone_table(json) zone_table = firewall_zone_table(json)
policy_table = firewall_policy_table(json) policy_table = firewall_policy_table(json)
address_set_table = firewall_address_set_table(json)
# Add zone table # Add zone table
if zone_table: if zone_table:
@@ -4156,6 +4157,11 @@ def show_firewall(json):
canvas.add_table(zone_table) canvas.add_table(zone_table)
canvas.add_spacing() canvas.add_spacing()
if address_set_table:
canvas.add_title("Address Sets")
canvas.add_table(address_set_table)
canvas.add_spacing()
# Add policy table # Add policy table
if policy_table: if policy_table:
canvas.add_title("Policies") canvas.add_title("Policies")
@@ -4447,13 +4453,13 @@ def firewall_matrix(fw, width=None):
zones = fw.get('zone', []) zones = fw.get('zone', [])
policies = fw.get('policy', []) policies = fw.get('policy', [])
# Build zone list - include zones with interfaces OR networks # Build zone list - include zones with interfaces, networks, or address-set sources
zone_names = [] zone_names = []
for z in zones: for z in zones:
interfaces = z.get('interface', []) interfaces = z.get('interface', [])
networks = z.get('network', []) networks = z.get('network', [])
# Include if zone has interfaces OR networks (non-empty lists) address_sets = z.get('address-set', [])
if interfaces or networks: if interfaces or networks or address_sets:
zone_names.append(z['name']) zone_names.append(z['name'])
# Always add the implicit HOST zone # Always add the implicit HOST zone
@@ -4558,6 +4564,7 @@ def firewall_zone_table(json):
Column('NAME', flexible=True), Column('NAME', flexible=True),
Column('TYPE'), Column('TYPE'),
Column('DATA', flexible=True), Column('DATA', flexible=True),
Column('ADDR SET'),
Column('ALLOWED HOST SERVICES', flexible=True) Column('ALLOWED HOST SERVICES', flexible=True)
]) ])
@@ -4566,6 +4573,7 @@ def firewall_zone_table(json):
action = zone.get('action', 'reject') action = zone.get('action', 'reject')
interface_list = zone.get('interface', []) interface_list = zone.get('interface', [])
network_list = zone.get('network', []) network_list = zone.get('network', [])
address_set_list = zone.get('address-set', [])
port_forwards = zone.get('port-forward', []) port_forwards = zone.get('port-forward', [])
services = zone.get('service', []) services = zone.get('service', [])
@@ -4586,7 +4594,7 @@ def firewall_zone_table(json):
if interface_list: if interface_list:
interfaces = compress_interface_list(interface_list) interfaces = compress_interface_list(interface_list)
config_lines.append(("iif", interfaces)) config_lines.append(("iif", interfaces))
else: elif not network_list and not address_set_list and not port_forwards:
config_lines.append(("iif", "(none)")) config_lines.append(("iif", "(none)"))
# Networks # Networks
@@ -4599,14 +4607,21 @@ def firewall_zone_table(json):
pf_display = format_port_forwards(port_forwards) pf_display = format_port_forwards(port_forwards)
config_lines.append(("fwd", pf_display)) config_lines.append(("fwd", pf_display))
# Add first line with zone name and services addr_rows = address_set_list[:] if address_set_list else ["(none)"]
if config_lines: row_count = max(len(config_lines), len(addr_rows))
first_type, first_data = config_lines[0] if not config_lines:
zone_table.row(locked, name, first_type, first_data, services_display) config_lines = [("", "")]
# Add additional configuration lines as separate rows for i in range(row_count):
for config_type, config_data in config_lines[1:]: if i < len(config_lines):
zone_table.row('', '', config_type, config_data, '') config_type, config_data = config_lines[i]
else:
config_type, config_data = "", ""
addr_set = addr_rows[i] if i < len(addr_rows) else ""
if i == 0:
zone_table.row(locked, name, config_type, config_data, addr_set, services_display)
else:
zone_table.row('', '', config_type, config_data, addr_set, '')
return zone_table return zone_table
@@ -4645,6 +4660,9 @@ def show_firewall_zone(json, zone_name=None):
networks = zone.get('network', []) networks = zone.get('network', [])
if not networks: if not networks:
networks = "" networks = ""
address_sets = zone.get('address-set', [])
if not address_sets:
address_sets = ""
services = zone.get('service', []) services = zone.get('service', [])
action = zone.get('action', 'reject') action = zone.get('action', 'reject')
@@ -4660,6 +4678,7 @@ def show_firewall_zone(json, zone_name=None):
print(f"{'action':<20}: {action}") print(f"{'action':<20}: {action}")
print(f"{'interface':<20}: {compress_interface_list(interfaces)}") print(f"{'interface':<20}: {compress_interface_list(interfaces)}")
print(f"{'networks':<20}: {', '.join(networks)}") print(f"{'networks':<20}: {', '.join(networks)}")
print(f"{'address-sets':<20}: {', '.join(address_sets)}")
print(f"{'services (to HOST)':<20}: {services_display}") print(f"{'services (to HOST)':<20}: {services_display}")
# Show port forwards if any # Show port forwards if any
@@ -4928,6 +4947,75 @@ def show_firewall_service(json, name=None):
service_table.print() service_table.print()
def firewall_address_set_table(json):
"""Create firewall address-sets table (returns SimpleTable or None)"""
fw = json.get('infix-firewall:firewall', {})
sets = fw.get('address-set', [])
if not sets:
return None
set_table = SimpleTable([
Column('NAME', flexible=True),
Column('FAMILY'),
Column('TIMEOUT'),
Column('STATIC'),
Column('DYNAMIC')
])
for aset in sets:
current = aset.get('current', [])
dynamic = sum(1 for cur in current if cur.get('dynamic'))
timeout = aset.get('timeout')
set_table.row(aset.get('name', ''),
aset.get('family', 'ipv4'),
f"{timeout} sec" if timeout else '',
str(len(current) - dynamic),
str(dynamic))
return set_table
def show_firewall_address_set(json, name=None):
"""Show firewall address-sets table or specific set details"""
fw = json.get('infix-firewall:firewall', {})
sets = fw.get('address-set', [])
if name:
aset = next((s for s in sets if s.get('name') == name), None)
if not aset:
print(f"Address-set '{name}' not found")
return
timeout = aset.get('timeout')
print(format_description('description', aset.get('description', '')))
print(f"{'name':<20}: {name}")
print(f"{'family':<20}: {aset.get('family', 'ipv4')}")
print(f"{'timeout':<20}: {f'{timeout} sec' if timeout else 'none'}")
print()
entry_table = SimpleTable([
Column('ENTRY'),
Column('TYPE'),
Column('EXPIRES')
])
for cur in aset.get('current', []):
expires = cur.get('expires')
entry_table.row(cur.get('entry', ''),
'dynamic' if cur.get('dynamic') else 'static',
f"{expires} sec" if expires is not None else '')
if entry_table.rows:
entry_table.min_width = 56
entry_table.print()
else:
print("(no entries)")
else:
set_table = firewall_address_set_table(json)
if set_table and set_table.rows:
set_table.min_width = 72
set_table.print()
else:
print("No address-sets configured")
def show_ospf(json_data): def show_ospf(json_data):
"""Show OSPF general instance information""" """Show OSPF general instance information"""
routing = json_data.get('ietf-routing:routing', {}) routing = json_data.get('ietf-routing:routing', {})
@@ -6018,6 +6106,8 @@ def main():
.add_argument('name', nargs='?', help='Policy name') .add_argument('name', nargs='?', help='Policy name')
subparsers.add_parser('show-firewall-service', help='Show firewall services') \ subparsers.add_parser('show-firewall-service', help='Show firewall services') \
.add_argument('name', nargs='?', help='Service name') .add_argument('name', nargs='?', help='Service name')
subparsers.add_parser('show-firewall-address-set', help='Show firewall address-sets') \
.add_argument('name', nargs='?', help='Address-set name')
subparsers.add_parser('show-firewall-log', help='Show firewall log') \ subparsers.add_parser('show-firewall-log', help='Show firewall log') \
.add_argument('limit', nargs='?', help='Last N lines, default: all') .add_argument('limit', nargs='?', help='Last N lines, default: all')
@@ -6095,6 +6185,8 @@ def main():
show_firewall_policy(json_data, args.name) show_firewall_policy(json_data, args.name)
elif args.command == "show-firewall-service": elif args.command == "show-firewall-service":
show_firewall_service(json_data, args.name) show_firewall_service(json_data, args.name)
elif args.command == "show-firewall-address-set":
show_firewall_address_set(json_data, args.name)
elif args.command == "show-firewall-log": elif args.command == "show-firewall-log":
show_firewall_logs(args.limit) show_firewall_logs(args.limit)
elif args.command == "show-nacm": elif args.command == "show-nacm":
+133 -2
View File
@@ -7,8 +7,37 @@ for the full API, see:
--object-path /org/fedoraproject/FirewallD1 --object-path /org/fedoraproject/FirewallD1
""" """
import dbus import dbus
import ipaddress
import re import re
from . import common from . import common
from .host import HOST
SHADOW_DIR = "/run/confd/address-sets"
def normalize_entry(entry):
"""Match firewalld entry normalization: bare address for host entries"""
try:
net = ipaddress.ip_network(str(entry), strict=False)
except ValueError:
return str(entry)
if net.prefixlen == net.max_prefixlen:
return str(net.network_address)
return str(net)
def split_sources(sources):
"""Zone sources are IP networks or 'ipset:NAME' address-set references"""
networks = []
ipsets = []
for src in sources:
src = str(src)
if src.startswith("ipset:"):
ipsets.append(src[len("ipset:"):])
else:
networks.append(src)
return networks, ipsets
def get_interface(interface="org.fedoraproject.FirewallD1"): def get_interface(interface="org.fedoraproject.FirewallD1"):
@@ -52,13 +81,15 @@ def get_zone_data(fw, name):
elif not short: elif not short:
short = "" short = ""
networks, ipsets = split_sources(settings.get('sources', []))
zone = { zone = {
"name": name, "name": name,
"short": short, "short": short,
"immutable": immutable, "immutable": immutable,
"description": settings.get('description', ''), "description": settings.get('description', ''),
"interface": list(settings.get('interfaces', [])), "interface": list(settings.get('interfaces', [])),
"network": list(settings.get('sources', [])), "network": networks,
"address-set": ipsets,
"action": action.get(target, "accept"), "action": action.get(target, "accept"),
"service": list(settings.get('services', [])) "service": list(settings.get('services', []))
} }
@@ -132,8 +163,10 @@ def get_zones(fw):
for name, zone_info in active_zones.items(): for name, zone_info in active_zones.items():
zone_data = get_zone_data(fwz, name) zone_data = get_zone_data(fwz, name)
if zone_data: if zone_data:
networks, ipsets = split_sources(zone_info.get('sources', []))
zone_data['interface'] = list(zone_info.get('interfaces', [])) zone_data['interface'] = list(zone_info.get('interfaces', []))
zone_data['network'] = list(zone_info.get('sources', [])) zone_data['network'] = networks
zone_data['address-set'] = ipsets
zones.append(zone_data) zones.append(zone_data)
except Exception as e: except Exception as e:
@@ -281,6 +314,100 @@ def get_policies(fw):
return policies return policies
def nft_set_elems(name):
"""Live contents of firewalld's nftables set
The kernel is the only source that sees entries in timeout sets,
and the only one tracking per-entry expiry. The firewalld table
is owner-protected, but reading is fine.
"""
data = HOST.run_json(("nft", "-j", "list", "set", "inet", "firewalld", name),
default={})
for obj in data.get("nftables", []):
if "set" in obj:
return obj["set"].get("elem", [])
return []
def nft_elem_parse(elem):
"""Return (entry, expires) from an nft JSON set element"""
expires = None
if isinstance(elem, dict) and "elem" in elem:
expires = elem["elem"].get("expires")
elem = elem["elem"].get("val")
if isinstance(elem, dict) and "prefix" in elem:
entry = f"{elem['prefix']['addr']}/{elem['prefix']['len']}"
elif isinstance(elem, dict) and "range" in elem:
entry = f"{elem['range'][0]}-{elem['range'][1]}"
else:
entry = str(elem)
return normalize_entry(entry), expires
def get_address_set(fwi, name):
try:
settings = fwi.getIPSetSettings(name)
# (version, short, description, type, options, entries)
options = settings[4]
tracked = [normalize_entry(e) for e in settings[5]]
except Exception as e:
common.LOG.warning("Failed querying ipset %s via D-Bus: %s", name, e)
return None
aset = {"name": str(name)}
description = str(settings[2])
if description:
aset["description"] = description
aset["family"] = "ipv6" if options.get("family") == "inet6" else "ipv4"
timeout = int(options.get("timeout", 0))
if timeout:
aset["timeout"] = timeout
lines = HOST.read_multiline(f"{SHADOW_DIR}/{name}", default=[])
shadow = {normalize_entry(line) for line in lines if line}
static = [e for e in tracked if e not in shadow]
if static:
aset["entry"] = static
current = []
for elem in nft_set_elems(name):
entry, expires = nft_elem_parse(elem)
cur = {"entry": entry, "dynamic": bool(timeout) or entry in shadow}
if expires is not None:
cur["expires"] = int(expires)
current.append(cur)
if current:
aset["current"] = current
return aset
def get_address_sets():
sets = []
fwi = get_interface("org.fedoraproject.FirewallD1.ipset")
if not fwi:
return sets
try:
names = fwi.getIPSets()
except Exception as e:
common.LOG.warning("Failed querying ipsets: %s", e)
return sets
for name in names:
data = get_address_set(fwi, name)
if data:
sets.append(data)
return sets
def get_service_data(fw, name): def get_service_data(fw, name):
try: try:
settings = fw.getServiceSettings2(name) settings = fw.getServiceSettings2(name)
@@ -360,4 +487,8 @@ def operational():
if services: if services:
data["infix-firewall:firewall"]["service"] = services data["infix-firewall:firewall"]["service"] = services
address_sets = get_address_sets()
if address_sets:
data["infix-firewall:firewall"]["address-set"] = address_sets
return data return data
+186 -81
View File
@@ -26,11 +26,6 @@ type cfgFwWrapper struct {
Firewall *firewallJSON `json:"infix-firewall:firewall,omitempty"` Firewall *firewallJSON `json:"infix-firewall:firewall,omitempty"`
} }
// cfgFwZoneWrapper is used when reading a single zone by path.
type cfgFwZoneWrapper struct {
Zone []zoneJSON `json:"infix-firewall:zone"`
}
// ─── Template display rows ──────────────────────────────────────────────────── // ─── Template display rows ────────────────────────────────────────────────────
type cfgZoneRow struct { type cfgZoneRow struct {
@@ -38,8 +33,10 @@ type cfgZoneRow struct {
IfaceCount int IfaceCount int
IfaceSet map[string]bool IfaceSet map[string]bool
ServiceSet map[string]bool ServiceSet map[string]bool
AddrSetSet map[string]bool
ServicesTxt string // fallback when ServiceOptions unavailable ServicesTxt string // fallback when ServiceOptions unavailable
NetworksTxt string // comma-separated, shown read-only when zone uses networks NetworksTxt string // comma-separated, shown read-only when zone uses networks
AddrSetsTxt string // comma-separated address-set sources
} }
type cfgPolicyRow struct { type cfgPolicyRow struct {
@@ -56,9 +53,43 @@ type cfgServiceRow struct {
PortsDisplay string // "tcp:80,443; udp:53" — at-a-glance PortsDisplay string // "tcp:80,443; udp:53" — at-a-glance
} }
// cfgFwSvcWrapper is used when reading a single service by path. type cfgAddrSetRow struct {
type cfgFwSvcWrapper struct { addressSetJSON
Service []fwServiceJSON `json:"infix-firewall:service"` EntriesTxt string // one entry per line for the textarea
}
func zoneConfigBody(cur zoneJSON) map[string]any {
zone := map[string]any{
"name": cur.Name,
}
if cur.Action != "" {
zone["action"] = cur.Action
}
if cur.Description != "" {
zone["description"] = cur.Description
}
if len(cur.Interface) > 0 {
zone["interface"] = cur.Interface
}
if len(cur.Network) > 0 {
zone["network"] = cur.Network
}
if len(cur.AddressSet) > 0 {
zone["address-set"] = cur.AddressSet
}
if len(cur.Service) > 0 {
zone["service"] = cur.Service
}
return zone
}
// toSet builds a membership map for template "index" lookups.
func toSet(ss []string) map[string]bool {
set := make(map[string]bool, len(ss))
for _, s := range ss {
set[s] = true
}
return set
} }
// ─── Template data ──────────────────────────────────────────────────────────── // ─── Template data ────────────────────────────────────────────────────────────
@@ -74,12 +105,15 @@ type cfgFirewallPageData struct {
ZoneNames []string // for policy ingress/egress multi-select ZoneNames []string // for policy ingress/egress multi-select
Policies []cfgPolicyRow Policies []cfgPolicyRow
Services []cfgServiceRow Services []cfgServiceRow
AddressSets []cfgAddrSetRow
AddressSetNames []string // for zone source multi-select
ProtoOptions []schema.IdentityOption ProtoOptions []schema.IdentityOption
Desc map[string]string Desc map[string]string
LoggingOptions []schema.IdentityOption LoggingOptions []schema.IdentityOption
ActionOptions []schema.IdentityOption ActionOptions []schema.IdentityOption
PolicyActionOptions []schema.IdentityOption PolicyActionOptions []schema.IdentityOption
ServiceOptions []schema.IdentityOption ServiceOptions []schema.IdentityOption
FamilyOptions []schema.IdentityOption
AllInterfaces []string AllInterfaces []string
Error string Error string
} }
@@ -107,6 +141,7 @@ func (h *ConfigureFirewallHandler) Overview(w http.ResponseWriter, r *http.Reque
zPath := fwPath + "/zone" zPath := fwPath + "/zone"
pPath := fwPath + "/policy" pPath := fwPath + "/policy"
sPath := fwPath + "/service" sPath := fwPath + "/service"
aPath := fwPath + "/address-set"
data.Desc = map[string]string{ data.Desc = map[string]string{
"enabled": schema.DescriptionOf(mgr, fwPath+"/enabled"), "enabled": schema.DescriptionOf(mgr, fwPath+"/enabled"),
"default": schema.DescriptionOf(mgr, fwPath+"/default"), "default": schema.DescriptionOf(mgr, fwPath+"/default"),
@@ -127,12 +162,19 @@ func (h *ConfigureFirewallHandler) Overview(w http.ResponseWriter, r *http.Reque
"service-port-lower": schema.DescriptionOf(mgr, sPath+"/port/lower"), "service-port-lower": schema.DescriptionOf(mgr, sPath+"/port/lower"),
"service-port-upper": schema.DescriptionOf(mgr, sPath+"/port/upper"), "service-port-upper": schema.DescriptionOf(mgr, sPath+"/port/upper"),
"service-port-proto": schema.DescriptionOf(mgr, sPath+"/port/proto"), "service-port-proto": schema.DescriptionOf(mgr, sPath+"/port/proto"),
"zone-address-set": schema.DescriptionOf(mgr, zPath+"/address-set"),
"addrset-name": schema.DescriptionOf(mgr, aPath+"/name"),
"addrset-description": schema.DescriptionOf(mgr, aPath+"/description"),
"addrset-family": schema.DescriptionOf(mgr, aPath+"/family"),
"addrset-timeout": schema.DescriptionOf(mgr, aPath+"/timeout"),
"addrset-entry": schema.DescriptionOf(mgr, aPath+"/entry"),
} }
data.LoggingOptions = schema.OptionsFor(mgr, fwPath+"/logging") data.LoggingOptions = schema.OptionsFor(mgr, fwPath+"/logging")
data.ActionOptions = schema.OptionsFor(mgr, zPath+"/action") data.ActionOptions = schema.OptionsFor(mgr, zPath+"/action")
data.PolicyActionOptions = schema.OptionsFor(mgr, pPath+"/action") data.PolicyActionOptions = schema.OptionsFor(mgr, pPath+"/action")
data.ServiceOptions = schema.OptionsFor(mgr, zPath+"/service") data.ServiceOptions = schema.OptionsFor(mgr, zPath+"/service")
data.ProtoOptions = schema.OptionsFor(mgr, sPath+"/port/proto") data.ProtoOptions = schema.OptionsFor(mgr, sPath+"/port/proto")
data.FamilyOptions = schema.OptionsFor(mgr, aPath+"/family")
} }
fw, active, err := h.fetchFirewall(r.Context()) fw, active, err := h.fetchFirewall(r.Context())
@@ -150,44 +192,45 @@ func (h *ConfigureFirewallHandler) Overview(w http.ResponseWriter, r *http.Reque
data.Default = fw.Default data.Default = fw.Default
data.Logging = fw.Logging data.Logging = fw.Logging
for _, z := range fw.Zone { for _, z := range fw.Zone {
ifaceSet := make(map[string]bool, len(z.Interface))
for _, iface := range z.Interface {
ifaceSet[iface] = true
}
svcSet := make(map[string]bool, len(z.Service))
for _, svc := range z.Service {
svcSet[svc] = true
}
data.Zones = append(data.Zones, cfgZoneRow{ data.Zones = append(data.Zones, cfgZoneRow{
zoneJSON: z, zoneJSON: z,
IfaceCount: len(z.Interface), IfaceCount: len(z.Interface),
IfaceSet: ifaceSet, IfaceSet: toSet(z.Interface),
ServiceSet: svcSet, ServiceSet: toSet(z.Service),
AddrSetSet: toSet(z.AddressSet),
ServicesTxt: strings.Join(z.Service, "\n"), ServicesTxt: strings.Join(z.Service, "\n"),
NetworksTxt: strings.Join(z.Network, ", "), NetworksTxt: strings.Join(z.Network, ", "),
AddrSetsTxt: strings.Join(z.AddressSet, ", "),
}) })
data.ZoneNames = append(data.ZoneNames, z.Name) data.ZoneNames = append(data.ZoneNames, z.Name)
} }
for _, s := range fw.AddressSet {
if s.Family == "" {
// Display the schema default when the leaf is unset.
for _, opt := range data.FamilyOptions {
if opt.IsDefault {
s.Family = opt.Value
}
}
}
data.AddressSets = append(data.AddressSets, cfgAddrSetRow{
addressSetJSON: s,
EntriesTxt: strings.Join(s.Entry, "\n"),
})
data.AddressSetNames = append(data.AddressSetNames, s.Name)
}
for _, p := range fw.Policy { for _, p := range fw.Policy {
masq := "—" masq := "—"
if p.Masquerade { if p.Masquerade {
masq = "Yes" masq = "Yes"
} }
ingressSet := make(map[string]bool, len(p.Ingress))
for _, z := range p.Ingress {
ingressSet[z] = true
}
egressSet := make(map[string]bool, len(p.Egress))
for _, z := range p.Egress {
egressSet[z] = true
}
data.Policies = append(data.Policies, cfgPolicyRow{ data.Policies = append(data.Policies, cfgPolicyRow{
policyJSON: p, policyJSON: p,
IngressDisplay: strings.Join(p.Ingress, ", "), IngressDisplay: strings.Join(p.Ingress, ", "),
EgressDisplay: strings.Join(p.Egress, ", "), EgressDisplay: strings.Join(p.Egress, ", "),
MasqDisplay: masq, MasqDisplay: masq,
IngressSet: ingressSet, IngressSet: toSet(p.Ingress),
EgressSet: egressSet, EgressSet: toSet(p.Egress),
}) })
} }
for _, s := range fw.Service { for _, s := range fw.Service {
@@ -303,7 +346,7 @@ func (h *ConfigureFirewallHandler) DeleteZone(w http.ResponseWriter, r *http.Req
renderSavedRedirect(w, "Zone deleted", "/configure/firewall") renderSavedRedirect(w, "Zone deleted", "/configure/firewall")
} }
// SaveZone updates a zone's action, description, interfaces, and services. // SaveZone updates a zone's action, description, interfaces, address-sets, and services.
// Uses read-modify-write to preserve fields not managed by this UI (network, // Uses read-modify-write to preserve fields not managed by this UI (network,
// port-forward). Note: port-forward entries are lost on save (Phase 3 limitation). // port-forward). Note: port-forward entries are lost on save (Phase 3 limitation).
// POST /configure/firewall/zones/{name} // POST /configure/firewall/zones/{name}
@@ -314,15 +357,15 @@ func (h *ConfigureFirewallHandler) SaveZone(w http.ResponseWriter, r *http.Reque
} }
name := r.PathValue("name") name := r.PathValue("name")
var wrap cfgFwZoneWrapper var wrap firewallWrapper // keyed GETs nest the zone under its full parent path
if err := h.RC.Get(r.Context(), fwConfigPath+"/zone="+url.PathEscape(name), &wrap); err != nil { if err := h.RC.Get(r.Context(), fwConfigPath+"/zone="+url.PathEscape(name), &wrap); err != nil {
log.Printf("configure firewall zone save %q: GET: %v", name, err) log.Printf("configure firewall zone save %q: GET: %v", name, err)
renderSaveError(w, err) renderSaveError(w, err)
return return
} }
cur := zoneJSON{Name: name} cur := zoneJSON{Name: name}
if len(wrap.Zone) > 0 { if len(wrap.Firewall.Zone) > 0 {
cur = wrap.Zone[0] cur = wrap.Firewall.Zone[0]
} }
cur.Action = r.FormValue("action") cur.Action = r.FormValue("action")
@@ -340,20 +383,13 @@ func (h *ConfigureFirewallHandler) SaveZone(w http.ResponseWriter, r *http.Reque
svcs = []string{} svcs = []string{}
} }
cur.Service = svcs cur.Service = svcs
sets := r.Form["address-sets"]
if sets == nil {
sets = []string{}
}
cur.AddressSet = sets
zone := map[string]any{ body := map[string]any{"infix-firewall:zone": []map[string]any{zoneConfigBody(cur)}}
"name": cur.Name,
"action": cur.Action,
"interface": cur.Interface,
"service": cur.Service,
}
if cur.Description != "" {
zone["description"] = cur.Description
}
if len(cur.Network) > 0 {
zone["network"] = cur.Network
}
body := map[string]any{"infix-firewall:zone": []map[string]any{zone}}
if err := h.RC.Put(r.Context(), fwConfigPath+"/zone="+url.PathEscape(name), body); err != nil { if err := h.RC.Put(r.Context(), fwConfigPath+"/zone="+url.PathEscape(name), body); err != nil {
log.Printf("configure firewall zone save %q: PUT: %v", name, err) log.Printf("configure firewall zone save %q: PUT: %v", name, err)
renderSaveError(w, err) renderSaveError(w, err)
@@ -362,52 +398,38 @@ func (h *ConfigureFirewallHandler) SaveZone(w http.ResponseWriter, r *http.Reque
renderSavedRedirect(w, "Zone saved", "/configure/firewall") renderSavedRedirect(w, "Zone saved", "/configure/firewall")
} }
// ResetZoneLeafList clears a leaf-list (interface or service) on a zone by // ResetZoneLeafList clears a leaf-list (interface or service) on a zone.
// re-PUTting the zone container without that field. RFC 8040 leaf-list // RFC 8040 leaf-list DELETE requires per-entry key predicates, so each
// DELETE requires per-entry key predicates, so a bulk clear has to go // instance is deleted individually. Deliberately avoids rebuilding the
// through the parent. // zone with PUT: the reset must not be able to touch anything else.
func (h *ConfigureFirewallHandler) resetZoneLeafList(w http.ResponseWriter, r *http.Request, leaf string) { func (h *ConfigureFirewallHandler) resetZoneLeafList(w http.ResponseWriter, r *http.Request, leaf string) {
name := r.PathValue("name") name := r.PathValue("name")
var wrap cfgFwZoneWrapper var wrap firewallWrapper // keyed GETs nest the zone under its full parent path
if err := h.RC.Get(r.Context(), fwConfigPath+"/zone="+url.PathEscape(name), &wrap); err != nil { if err := h.RC.Get(r.Context(), fwConfigPath+"/zone="+url.PathEscape(name), &wrap); err != nil {
log.Printf("configure firewall zone reset %s/%s: GET: %v", name, leaf, err) log.Printf("configure firewall zone reset %s/%s: GET: %v", name, leaf, err)
renderSaveError(w, err) renderSaveError(w, err)
return return
} }
cur := zoneJSON{Name: name} var values []string
if len(wrap.Zone) > 0 { if len(wrap.Firewall.Zone) > 0 {
cur = wrap.Zone[0] switch leaf {
} case "interface":
switch leaf { values = wrap.Firewall.Zone[0].Interface
case "interface": case "service":
cur.Interface = nil values = wrap.Firewall.Zone[0].Service
case "service": }
cur.Service = nil
} }
zone := map[string]any{ for _, val := range values {
"name": cur.Name, path := fmt.Sprintf("%s/zone=%s/%s=%s", fwConfigPath,
"action": cur.Action, restconf.EscapeKey(name), leaf, restconf.EscapeKey(val))
if err := h.RC.Delete(r.Context(), path); err != nil && !restconf.IsNotFound(err) {
log.Printf("configure firewall zone reset %s/%s=%s: %v", name, leaf, val, err)
renderSaveError(w, err)
return
}
} }
if cur.Description != "" { renderSavedRedirect(w, "Reset to default", "/configure/firewall")
zone["description"] = cur.Description
}
if len(cur.Interface) > 0 {
zone["interface"] = cur.Interface
}
if len(cur.Network) > 0 {
zone["network"] = cur.Network
}
if len(cur.Service) > 0 {
zone["service"] = cur.Service
}
body := map[string]any{"infix-firewall:zone": []map[string]any{zone}}
if err := h.RC.Put(r.Context(), fwConfigPath+"/zone="+url.PathEscape(name), body); err != nil {
log.Printf("configure firewall zone reset %s/%s: PUT: %v", name, leaf, err)
renderSaveError(w, err)
return
}
renderSaved(w, "Reset to default")
} }
// ResetZoneInterfaces clears the zone's interface leaf-list. // ResetZoneInterfaces clears the zone's interface leaf-list.
@@ -692,6 +714,89 @@ func (h *ConfigureFirewallHandler) DeleteService(w http.ResponseWriter, r *http.
renderSavedRedirect(w, "Service deleted", "/configure/firewall") renderSavedRedirect(w, "Service deleted", "/configure/firewall")
} }
// ─── Address-sets CRUD ───────────────────────────────────────────────────────
// parseAddressSet builds the RESTCONF address-set body from the add/save
// form. Entries come from a textarea, one address or prefix per line.
func parseAddressSet(r *http.Request, name string) (map[string]any, error) {
set := map[string]any{"name": name}
if desc := strings.TrimSpace(r.FormValue("description")); desc != "" {
set["description"] = desc
}
if fam := r.FormValue("family"); fam != "" {
set["family"] = fam
}
if t := strings.TrimSpace(r.FormValue("timeout")); t != "" {
tv, err := strconv.Atoi(t)
if err != nil || tv < 1 {
return nil, fmt.Errorf("timeout must be a positive number of seconds")
}
set["timeout"] = tv
}
entries := []string{}
for _, line := range strings.Split(r.FormValue("entries"), "\n") {
if line = strings.TrimSpace(line); line != "" {
entries = append(entries, line)
}
}
if len(entries) > 0 {
set["entry"] = entries
}
return set, nil
}
func (h *ConfigureFirewallHandler) putAddressSet(w http.ResponseWriter, r *http.Request, name, saved string) {
set, err := parseAddressSet(r, name)
if err != nil {
renderSaveError(w, err)
return
}
body := map[string]any{"infix-firewall:address-set": []map[string]any{set}}
if err := h.RC.Put(r.Context(), fwConfigPath+"/address-set="+restconf.EscapeKey(name), body); err != nil {
log.Printf("configure firewall address-set save %q: %v", name, err)
renderSaveError(w, err)
return
}
renderSavedRedirect(w, saved, "/configure/firewall")
}
// AddAddressSet creates a new address-set.
// POST /configure/firewall/address-sets
func (h *ConfigureFirewallHandler) AddAddressSet(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
name := strings.TrimSpace(r.FormValue("name"))
if name == "" {
renderSaveError(w, fmt.Errorf("address-set name is required"))
return
}
h.putAddressSet(w, r, name, "Address-set added")
}
// SaveAddressSet updates an existing address-set.
// POST /configure/firewall/address-sets/{name}
func (h *ConfigureFirewallHandler) SaveAddressSet(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
h.putAddressSet(w, r, r.PathValue("name"), "Address-set saved")
}
// DeleteAddressSet removes an address-set.
// DELETE /configure/firewall/address-sets/{name}
func (h *ConfigureFirewallHandler) DeleteAddressSet(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
if err := h.RC.Delete(r.Context(), fwConfigPath+"/address-set="+restconf.EscapeKey(name)); err != nil {
log.Printf("configure firewall address-set delete %q: %v", name, err)
renderSaveError(w, err)
return
}
renderSavedRedirect(w, "Address-set deleted", "/configure/firewall")
}
// ─── Helpers ───────────────────────────────────────────────────────────────── // ─── Helpers ─────────────────────────────────────────────────────────────────
// fetchInterfaceNames returns configured interface names from candidate (fallback running). // fetchInterfaceNames returns configured interface names from candidate (fallback running).
@@ -0,0 +1,277 @@
// SPDX-License-Identifier: MIT
package handlers
import (
"context"
"encoding/json"
"html/template"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"testing"
"infix/webui/internal/restconf"
"infix/webui/internal/schema"
"infix/webui/internal/security"
"infix/webui/internal/testutil"
)
var minimalCfgFwTmpl = template.Must(template.New("configure-firewall.html").Parse(
`{{define "configure-firewall.html"}}{{template "content" .}}{{end}}` +
`{{define "content"}}sets={{len .AddressSets}}` +
`{{range .AddressSets}};{{.Name}}:{{.EntriesTxt}}:{{if .Timeout}}{{.Timeout}}{{end}}{{end}}` +
`{{range .Zones}};zone-{{.Name}}:{{.AddrSetsTxt}}{{end}}{{end}}`,
))
func TestConfigureFirewallOverview_AddressSets(t *testing.T) {
mock := testutil.NewMockFetcher()
mock.SetResponse(candidatePath+"/infix-firewall:firewall", map[string]any{
"infix-firewall:firewall": map[string]any{
"default": "trusted",
"zone": []map[string]any{{
"name": "trusted",
"action": "accept",
"address-set": []string{"allowed"},
}},
"address-set": []map[string]any{{
"name": "allowed",
"entry": []string{"192.168.1.40", "10.0.0.0/24"},
}, {
"name": "banned",
"timeout": 3600,
}},
},
})
h := &ConfigureFirewallHandler{
Template: minimalCfgFwTmpl,
RC: mock,
Schema: schema.NewCache(mock, t.TempDir()),
}
req := httptest.NewRequest(http.MethodGet, "/configure/firewall", nil)
ctx := restconf.ContextWithCredentials(req.Context(), restconf.Credentials{
Username: "admin",
Password: "admin",
})
ctx = security.WithToken(ctx, "test-csrf-token")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.Overview(w, req)
if w.Code != http.StatusOK {
t.Fatalf("want 200 got %d; body: %s", w.Code, w.Body.String())
}
body := w.Body.String()
for _, want := range []string{
"sets=2",
";allowed:192.168.1.40\n10.0.0.0/24:",
";banned::3600",
";zone-trusted:allowed",
} {
if !strings.Contains(body, want) {
t.Errorf("body missing %q; body: %s", want, body)
}
}
}
type recordingFetcher struct {
*testutil.MockFetcher
putCalls int
lastPath string
lastBody any
deletePaths []string
}
func (r *recordingFetcher) Put(_ context.Context, path string, body any) error {
r.putCalls++
r.lastPath = path
r.lastBody = body
return nil
}
func (r *recordingFetcher) Delete(_ context.Context, path string) error {
r.deletePaths = append(r.deletePaths, path)
return nil
}
// zoneGetResponse mimics the server's response shape for a keyed zone GET:
// the zone is nested under its full parent path, not returned bare.
func zoneGetResponse(zone map[string]any) map[string]any {
return map[string]any{
"infix-firewall:firewall": map[string]any{
"zone": []map[string]any{zone},
},
}
}
func TestConfigureFirewallSaveZoneAllowsInterfacesWithAddressSets(t *testing.T) {
mock := &recordingFetcher{MockFetcher: testutil.NewMockFetcher()}
mock.SetResponse(candidatePath+"/infix-firewall:firewall/zone=public", zoneGetResponse(map[string]any{
"name": "public",
"action": "drop",
"interface": []string{"eth0"},
"network": []string{"10.0.0.0/24"},
}))
h := &ConfigureFirewallHandler{
Template: minimalCfgFwTmpl,
RC: mock,
Schema: schema.NewCache(mock, t.TempDir()),
}
form := url.Values{
"action": {"drop"},
"description": {"Public zone"},
"interfaces": {"eth0"},
"address-sets": {"allowed"},
}
req := httptest.NewRequest(http.MethodPost, "/configure/firewall/zones/public", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetPathValue("name", "public")
ctx := restconf.ContextWithCredentials(req.Context(), restconf.Credentials{
Username: "admin",
Password: "admin",
})
ctx = security.WithToken(ctx, "test-csrf-token")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.SaveZone(w, req)
if mock.putCalls != 1 {
t.Fatalf("want 1 PUT call got %d", mock.putCalls)
}
if w.Code != http.StatusNoContent {
t.Fatalf("want 204 got %d; body: %s", w.Code, w.Body.String())
}
if got, want := mock.lastPath, candidatePath+"/infix-firewall:firewall/zone=public"; got != want {
t.Fatalf("want PUT path %q got %q", want, got)
}
body, ok := mock.lastBody.(map[string]any)
if !ok {
t.Fatalf("unexpected PUT body type %T", mock.lastBody)
}
zones, ok := body["infix-firewall:zone"].([]map[string]any)
if !ok || len(zones) != 1 {
t.Fatalf("unexpected PUT zone payload %#v", body["infix-firewall:zone"])
}
zone := zones[0]
if got, want := zone["interface"], []string{"eth0"}; !reflect.DeepEqual(got, want) {
t.Fatalf("want interfaces %#v got %#v", want, got)
}
if got, want := zone["address-set"], []string{"allowed"}; !reflect.DeepEqual(got, want) {
t.Fatalf("want address-sets %#v got %#v", want, got)
}
if got, want := zone["network"], []string{"10.0.0.0/24"}; !reflect.DeepEqual(got, want) {
t.Fatalf("want networks preserved %#v got %#v", want, got)
}
var trig map[string]string
if err := json.Unmarshal([]byte(w.Header().Get("HX-Trigger")), &trig); err != nil {
t.Fatalf("unmarshal HX-Trigger: %v", err)
}
if got := trig["cfgSaved"]; !strings.Contains(got, "Zone saved") {
t.Fatalf("unexpected success message %q", got)
}
}
func TestConfigureFirewallSaveZoneClearsAllServices(t *testing.T) {
mock := &recordingFetcher{MockFetcher: testutil.NewMockFetcher()}
mock.SetResponse(candidatePath+"/infix-firewall:firewall/zone=public", zoneGetResponse(map[string]any{
"name": "public",
"action": "drop",
"interface": []string{"eth0"},
"service": []string{"ssh", "http"},
}))
h := &ConfigureFirewallHandler{
Template: minimalCfgFwTmpl,
RC: mock,
Schema: schema.NewCache(mock, t.TempDir()),
}
form := url.Values{
"action": {"drop"},
"description": {"Public zone"},
"interfaces": {"eth0"},
}
req := httptest.NewRequest(http.MethodPost, "/configure/firewall/zones/public", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetPathValue("name", "public")
ctx := restconf.ContextWithCredentials(req.Context(), restconf.Credentials{
Username: "admin",
Password: "admin",
})
ctx = security.WithToken(ctx, "test-csrf-token")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.SaveZone(w, req)
if mock.putCalls != 1 {
t.Fatalf("want 1 PUT call got %d", mock.putCalls)
}
body, ok := mock.lastBody.(map[string]any)
if !ok {
t.Fatalf("unexpected PUT body type %T", mock.lastBody)
}
zones, ok := body["infix-firewall:zone"].([]map[string]any)
if !ok || len(zones) != 1 {
t.Fatalf("unexpected PUT zone payload %#v", body["infix-firewall:zone"])
}
if _, ok := zones[0]["service"]; ok {
t.Fatalf("expected cleared services to be omitted from payload, got %#v", zones[0]["service"])
}
}
func TestConfigureFirewallResetZoneServicesOnlyDeletesServices(t *testing.T) {
mock := &recordingFetcher{MockFetcher: testutil.NewMockFetcher()}
mock.SetResponse(candidatePath+"/infix-firewall:firewall/zone=public", zoneGetResponse(map[string]any{
"name": "public",
"action": "drop",
"interface": []string{"eth0"},
"address-set": []string{"allowed"},
"service": []string{"ssh", "dhcpv6-client"},
}))
h := &ConfigureFirewallHandler{
Template: minimalCfgFwTmpl,
RC: mock,
Schema: schema.NewCache(mock, t.TempDir()),
}
req := httptest.NewRequest(http.MethodDelete, "/configure/firewall/zones/public/services", nil)
req.SetPathValue("name", "public")
ctx := restconf.ContextWithCredentials(req.Context(), restconf.Credentials{
Username: "admin",
Password: "admin",
})
ctx = security.WithToken(ctx, "test-csrf-token")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.ResetZoneServices(w, req)
if w.Code != http.StatusNoContent {
t.Fatalf("want 204 got %d; body: %s", w.Code, w.Body.String())
}
if got, want := w.Header().Get("HX-Location"), `{"path":"/configure/firewall","target":"#content"}`; got != want {
t.Fatalf("want HX-Location %q got %q", want, got)
}
if mock.putCalls != 0 {
t.Fatalf("reset must not rewrite the zone, got %d PUT call(s) with body %#v",
mock.putCalls, mock.lastBody)
}
want := []string{
candidatePath + "/infix-firewall:firewall/zone=public/service=ssh",
candidatePath + "/infix-firewall:firewall/zone=public/service=dhcpv6-client",
}
if !reflect.DeepEqual(mock.deletePaths, want) {
t.Fatalf("want DELETE paths %#v got %#v", want, mock.deletePaths)
}
}
@@ -1369,15 +1369,22 @@ func (h *ConfigureInterfacesHandler) ResetEthernetAdvertised(w http.ResponseWrit
name := r.PathValue("name") name := r.PathValue("name")
base := ifacePath(name) + "/ieee802-ethernet-interface:ethernet" base := ifacePath(name) + "/ieee802-ethernet-interface:ethernet"
// Keyed GETs nest the requested node under its full parent path.
var resp struct { var resp struct {
AN struct { Interfaces struct {
Enable *bool `json:"enable"` Interface []struct {
} `json:"ieee802-ethernet-interface:auto-negotiation"` Ethernet struct {
AN struct {
Enable *bool `json:"enable"`
} `json:"auto-negotiation"`
} `json:"ieee802-ethernet-interface:ethernet"`
} `json:"interface"`
} `json:"ietf-interfaces:interfaces"`
} }
enable := true // YANG default enable := true // YANG default
if err := h.RC.Get(r.Context(), base+"/auto-negotiation", &resp); err == nil { if err := h.RC.Get(r.Context(), base+"/auto-negotiation", &resp); err == nil {
if resp.AN.Enable != nil { if ifs := resp.Interfaces.Interface; len(ifs) > 0 && ifs[0].Ethernet.AN.Enable != nil {
enable = *resp.AN.Enable enable = *ifs[0].Ethernet.AN.Enable
} }
} else if !restconf.IsNotFound(err) { } else if !restconf.IsNotFound(err) {
log.Printf("configure interfaces %s reset advertised get: %v", name, err) log.Printf("configure interfaces %s reset advertised get: %v", name, err)
+5 -7
View File
@@ -46,11 +46,6 @@ type containerListWrapper struct {
} `json:"infix-containers:containers"` } `json:"infix-containers:containers"`
} }
// containerResourceUsageWrapper wraps the RESTCONF resource-usage response.
type containerResourceUsageWrapper struct {
ResourceUsage containerResourceUsageJSON `json:"infix-containers:resource-usage"`
}
// ContainerEntry holds display-ready data for a single container row. // ContainerEntry holds display-ready data for a single container row.
type ContainerEntry struct { type ContainerEntry struct {
Name string Name string
@@ -106,13 +101,16 @@ func (h *ContainersHandler) Overview(w http.ResponseWriter, r *http.Request) {
defer wg.Done() defer wg.Done()
path := fmt.Sprintf("/data/infix-containers:containers/container=%s/resource-usage", path := fmt.Sprintf("/data/infix-containers:containers/container=%s/resource-usage",
url.PathEscape(name)) url.PathEscape(name))
var w containerResourceUsageWrapper var w containerListWrapper // keyed GETs nest under the full parent path
if err := h.RC.Get(ctx, path, &w); err != nil { if err := h.RC.Get(ctx, path, &w); err != nil {
log.Printf("restconf resource-usage %s: %v", name, err) log.Printf("restconf resource-usage %s: %v", name, err)
return return
} }
if len(w.Containers.Container) == 0 {
return
}
mu.Lock() mu.Lock()
usages[idx] = w.ResourceUsage usages[idx] = w.Containers.Container[0].ResourceUsage
mu.Unlock() mu.Unlock()
}(i, c.Name) }(i, c.Name)
} }
+77 -17
View File
@@ -19,13 +19,32 @@ type firewallWrapper struct {
} }
type firewallJSON struct { type firewallJSON struct {
Enabled *yangBool `json:"enabled"` // YANG default: true; nil means enabled Enabled *yangBool `json:"enabled"` // YANG default: true; nil means enabled
Default string `json:"default"` Default string `json:"default"`
Logging string `json:"logging"` Logging string `json:"logging"`
Lockdown yangBool `json:"lockdown"` Lockdown yangBool `json:"lockdown"`
Zone []zoneJSON `json:"zone"` Zone []zoneJSON `json:"zone"`
Policy []policyJSON `json:"policy"` Policy []policyJSON `json:"policy"`
Service []fwServiceJSON `json:"service"` Service []fwServiceJSON `json:"service"`
AddressSet []addressSetJSON `json:"address-set"`
}
// addressSetJSON models a named set of IP addresses/networks usable as zone
// source. The current list is operational state: the live set contents,
// including dynamic entries added at runtime.
type addressSetJSON struct {
Name string `json:"name"`
Description string `json:"description"`
Family string `json:"family"`
Timeout yangInt64 `json:"timeout"`
Entry []string `json:"entry"`
Current []addrSetCurJSON `json:"current"`
}
type addrSetCurJSON struct {
Entry string `json:"entry"`
Dynamic bool `json:"dynamic"`
Expires *yangInt64 `json:"expires"`
} }
// fwServiceJSON models a user-defined firewall service (port + protocol bundle). // fwServiceJSON models a user-defined firewall service (port + protocol bundle).
@@ -50,6 +69,7 @@ type zoneJSON struct {
Description string `json:"description"` Description string `json:"description"`
Interface []string `json:"interface"` Interface []string `json:"interface"`
Network []string `json:"network"` Network []string `json:"network"`
AddressSet []string `json:"address-set"`
Service []string `json:"service"` Service []string `json:"service"`
PortForward []portForwardJSON `json:"port-forward"` PortForward []portForwardJSON `json:"port-forward"`
Immutable bool `json:"immutable"` Immutable bool `json:"immutable"`
@@ -92,6 +112,7 @@ type firewallData struct {
Matrix []matrixRow Matrix []matrixRow
Zones []zoneEntry Zones []zoneEntry
Policies []policyEntry Policies []policyEntry
AddressSets []addressSetEntry
Error string Error string
} }
@@ -110,11 +131,25 @@ type matrixCell struct {
} }
type zoneEntry struct { type zoneEntry struct {
Name string Name string
Action string Action string
Interfaces string Interfaces string
Networks string Networks string
Services string // services allowed to HOST from this zone AddressSets string
Services string // services allowed to HOST from this zone
}
type addressSetEntry struct {
Name string
Family string
Timeout string // "3600 s" for expiring sets, "" otherwise
Entries []addressSetEntryRow
}
type addressSetEntryRow struct {
Entry string
Dynamic bool
Expires string // remaining lifetime, "" when not expiring
} }
type policyEntry struct { type policyEntry struct {
@@ -169,14 +204,39 @@ func (h *FirewallHandler) Overview(w http.ResponseWriter, r *http.Request) {
for _, z := range f.Zone { for _, z := range f.Zone {
data.Zones = append(data.Zones, zoneEntry{ data.Zones = append(data.Zones, zoneEntry{
Name: z.Name, Name: z.Name,
Action: z.Action, Action: z.Action,
Interfaces: strings.Join(z.Interface, ", "), Interfaces: strings.Join(z.Interface, ", "),
Networks: strings.Join(z.Network, ", "), Networks: strings.Join(z.Network, ", "),
Services: strings.Join(z.Service, ", "), AddressSets: strings.Join(z.AddressSet, ", "),
Services: strings.Join(z.Service, ", "),
}) })
} }
for _, s := range f.AddressSet {
set := addressSetEntry{
Name: s.Name,
Family: s.Family,
}
if set.Family == "" {
set.Family = "ipv4" // YANG default
}
if s.Timeout > 0 {
set.Timeout = formatDuration(int64(s.Timeout))
}
for _, cur := range s.Current {
row := addressSetEntryRow{
Entry: cur.Entry,
Dynamic: cur.Dynamic,
}
if cur.Expires != nil {
row.Expires = formatDuration(int64(*cur.Expires))
}
set.Entries = append(set.Entries, row)
}
data.AddressSets = append(data.AddressSets, set)
}
for _, p := range f.Policy { for _, p := range f.Policy {
data.Policies = append(data.Policies, policyEntry{ data.Policies = append(data.Policies, policyEntry{
Name: p.Name, Name: p.Name,
+9 -3
View File
@@ -21,8 +21,13 @@ type wgConfigJSON struct {
} }
// wgIfaceConfigWrapper is used to fetch per-interface WireGuard config. // wgIfaceConfigWrapper is used to fetch per-interface WireGuard config.
// Keyed GETs nest the requested node under its full parent path.
type wgIfaceConfigWrapper struct { type wgIfaceConfigWrapper struct {
WireGuard *wgConfigJSON `json:"infix-interfaces:wireguard"` Interfaces struct {
Interface []struct {
WireGuard *wgConfigJSON `json:"infix-interfaces:wireguard"`
} `json:"interface"`
} `json:"ietf-interfaces:interfaces"`
} }
// WGPeer holds display-ready data for a single WireGuard peer. // WGPeer holds display-ready data for a single WireGuard peer.
@@ -128,8 +133,9 @@ func buildWGTunnel(ctx context.Context, rc *restconf.Client, iface ifaceJSON) WG
// Fetch ListenPort from config endpoint (separate from oper-state). // Fetch ListenPort from config endpoint (separate from oper-state).
var cfgWrap wgIfaceConfigWrapper var cfgWrap wgIfaceConfigWrapper
path := fmt.Sprintf("/data/ietf-interfaces:interfaces/interface=%s/infix-interfaces:wireguard", iface.Name) path := fmt.Sprintf("/data/ietf-interfaces:interfaces/interface=%s/infix-interfaces:wireguard", iface.Name)
if err := rc.Get(ctx, path, &cfgWrap); err == nil && cfgWrap.WireGuard != nil { if err := rc.Get(ctx, path, &cfgWrap); err == nil &&
tunnel.ListenPort = cfgWrap.WireGuard.ListenPort len(cfgWrap.Interfaces.Interface) > 0 && cfgWrap.Interfaces.Interface[0].WireGuard != nil {
tunnel.ListenPort = cfgWrap.Interfaces.Interface[0].WireGuard.ListenPort
} }
// Build peers from embedded peer-status. // Build peers from embedded peer-status.
+22 -1
View File
@@ -9,8 +9,10 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"net/url" "net/url"
"reflect"
"strings" "strings"
"time" "time"
) )
@@ -79,6 +81,12 @@ func (c *Client) doRequest(ctx context.Context, method, path string) (*http.Resp
// Get fetches a RESTCONF resource, decoding the JSON response into target. // Get fetches a RESTCONF resource, decoding the JSON response into target.
// User credentials are taken from the request context (set by auth middleware). // User credentials are taken from the request context (set by auth middleware).
// Get sends a GET request and decodes the JSON response into target.
//
// NOTE: deep-path GETs (keyed list entries, nested containers) return the
// requested node nested under its full parent path, not bare as the RFC
// 8040 examples may suggest. Wrapper structs must model the nesting from
// the top-level container down, or the decode silently matches nothing.
func (c *Client) Get(ctx context.Context, path string, target any) error { func (c *Client) Get(ctx context.Context, path string, target any) error {
resp, err := c.doRequest(ctx, http.MethodGet, path) resp, err := c.doRequest(ctx, http.MethodGet, path)
if err != nil { if err != nil {
@@ -88,7 +96,20 @@ func (c *Client) Get(ctx context.Context, path string, target any) error {
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return parseError(resp) return parseError(resp)
} }
return json.NewDecoder(resp.Body).Decode(target) data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if err := json.Unmarshal(data, target); err != nil {
return err
}
// Tripwire for the nesting trap above: content arrived but nothing
// in the wrapper matched it.
if v := reflect.ValueOf(target); len(data) > 2 &&
v.Kind() == reflect.Pointer && v.Elem().IsZero() {
log.Printf("restconf: GET %s decoded to zero %T, wrapper shape mismatch?", path, target)
}
return nil
} }
// Post sends a POST request to a RESTCONF RPC endpoint. // Post sends a POST request to a RESTCONF RPC endpoint.
+3
View File
@@ -424,6 +424,9 @@ func New(
mux.HandleFunc("POST /configure/firewall/services", cfgFw.AddService) mux.HandleFunc("POST /configure/firewall/services", cfgFw.AddService)
mux.HandleFunc("POST /configure/firewall/services/{name}", cfgFw.SaveService) mux.HandleFunc("POST /configure/firewall/services/{name}", cfgFw.SaveService)
mux.HandleFunc("DELETE /configure/firewall/services/{name}", cfgFw.DeleteService) mux.HandleFunc("DELETE /configure/firewall/services/{name}", cfgFw.DeleteService)
mux.HandleFunc("POST /configure/firewall/address-sets", cfgFw.AddAddressSet)
mux.HandleFunc("POST /configure/firewall/address-sets/{name}", cfgFw.SaveAddressSet)
mux.HandleFunc("DELETE /configure/firewall/address-sets/{name}", cfgFw.DeleteAddressSet)
mux.HandleFunc("GET /configure/hardware", cfgHw.Overview) mux.HandleFunc("GET /configure/hardware", cfgHw.Overview)
mux.HandleFunc("POST /configure/hardware", cfgHw.CreateHardware) mux.HandleFunc("POST /configure/hardware", cfgHw.CreateHardware)
mux.HandleFunc("POST /configure/hardware/usb/{name}", cfgHw.SaveUSBPort) mux.HandleFunc("POST /configure/hardware/usb/{name}", cfgHw.SaveUSBPort)
@@ -112,6 +112,7 @@
<th>Action{{template "field-info" (index $d "zone-action")}}</th> <th>Action{{template "field-info" (index $d "zone-action")}}</th>
<th>Description{{template "field-info" (index $d "zone-description")}}</th> <th>Description{{template "field-info" (index $d "zone-description")}}</th>
<th style="text-align:center">Interfaces</th> <th style="text-align:center">Interfaces</th>
<th>Address sets{{template "field-info" (index $d "zone-address-set")}}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@@ -128,6 +129,7 @@
<td>{{if .Action}}{{.Action}}{{else}}reject{{end}}</td> <td>{{if .Action}}{{.Action}}{{else}}reject{{end}}</td>
<td>{{if .Description}}{{.Description}}{{else}}<span class="text-muted"></span>{{end}}</td> <td>{{if .Description}}{{.Description}}{{else}}<span class="text-muted"></span>{{end}}</td>
<td style="text-align:center">{{.IfaceCount}}</td> <td style="text-align:center">{{.IfaceCount}}</td>
<td>{{if .AddressSet}}{{.AddrSetsTxt}}{{else}}<span class="text-muted"></span>{{end}}</td>
<td style="text-align:right"> <td style="text-align:right">
{{if not .Immutable}} {{if not .Immutable}}
<button type="button" class="btn-icon btn-icon-danger" <button type="button" class="btn-icon btn-icon-danger"
@@ -141,12 +143,13 @@
</td> </td>
</tr> </tr>
<tr class="key-detail-row" id="key-detail-zone-{{$i}}"> <tr class="key-detail-row" id="key-detail-zone-{{$i}}">
<td colspan="5" class="key-detail-cell"> <td colspan="6" class="key-detail-cell">
<div class="key-detail-body"> <div class="key-detail-body">
{{if .Immutable}} {{if .Immutable}}
<p class="empty-message">This zone is system-defined and cannot be modified.</p> <p class="empty-message">This zone is system-defined and cannot be modified.</p>
{{if .Interface}}<p class="text-muted" style="margin-top:0.5rem">Interfaces: {{range $j, $iface := .Interface}}{{if $j}}, {{end}}{{$iface}}{{end}}</p>{{end}} {{if .Interface}}<p class="text-muted" style="margin-top:0.5rem">Interfaces: {{range $j, $iface := .Interface}}{{if $j}}, {{end}}{{$iface}}{{end}}</p>{{end}}
{{if .Network}}<p class="text-muted" style="margin-top:0.25rem">Networks: {{.NetworksTxt}}</p>{{end}} {{if .Network}}<p class="text-muted" style="margin-top:0.25rem">Networks: {{.NetworksTxt}}</p>{{end}}
{{if .AddressSet}}<p class="text-muted" style="margin-top:0.25rem">Address sets: {{.AddrSetsTxt}}</p>{{end}}
{{if .Service}}<p class="text-muted" style="margin-top:0.25rem">Services: {{.ServicesTxt}}</p>{{end}} {{if .Service}}<p class="text-muted" style="margin-top:0.25rem">Services: {{.ServicesTxt}}</p>{{end}}
{{else}} {{else}}
<form hx-post="/configure/firewall/zones/{{.Name}}" hx-swap="none"> <form hx-post="/configure/firewall/zones/{{.Name}}" hx-swap="none">
@@ -214,6 +217,24 @@
</td> </td>
</tr> </tr>
{{end}} {{end}}
{{if $.AddressSetNames}}
<tr>
<th>Address sets{{template "field-info" (index $d "zone-address-set")}}</th>
<td colspan="2">
<details class="cfg-multi">
<summary class="cfg-multi-summary">{{if .AddressSet}}{{.AddrSetsTxt}}{{else}}(None){{end}}</summary>
<div class="cfg-multi-body">
{{range $.AddressSetNames}}
<label class="cfg-multi-item">
<input type="checkbox" name="address-sets" value="{{.}}" {{if index $z.AddrSetSet .}}checked{{end}}>
{{.}}
</label>
{{end}}
</div>
</details>
</td>
</tr>
{{end}}
<tr> <tr>
<th>Services{{template "field-info" (index $d "zone-service")}}</th> <th>Services{{template "field-info" (index $d "zone-service")}}</th>
<td> <td>
@@ -723,6 +744,160 @@
</div> </div>
</section> </section>
{{/* ── Address Sets ─────────────────────────────────────────────────── */}}
<section class="info-card info-grid-span-2">
<div class="card-header">Address Sets</div>
<p class="text-muted" style="font-size:0.85em;margin:0 1rem 0.5rem">
Named sets of IP addresses and networks, usable as zone sources.
Entries configured here are static; dynamic entries can be added at
runtime from the CLI or over NETCONF/RESTCONF, see the Firewall
status page for the live contents.
</p>
<div class="data-table-wrap">
<table class="data-table cfg-table">
<thead>
<tr>
<th>Name{{template "field-info" (index $d "addrset-name")}}</th>
<th>Description{{template "field-info" (index $d "addrset-description")}}</th>
<th>Family{{template "field-info" (index $d "addrset-family")}}</th>
<th>Timeout{{template "field-info" (index $d "addrset-timeout")}}</th>
<th style="text-align:center">Entries</th>
<th></th>
</tr>
</thead>
<tbody>
{{range $i, $s := .AddressSets}}
<tr>
<td>
<button class="key-row-toggle" type="button"
aria-expanded="false" data-target="addrset-{{$i}}">
<span class="key-row-arrow" aria-hidden="true"></span>{{.Name}}
</button>
</td>
<td>{{if .Description}}{{.Description}}{{else}}<span class="text-muted"></span>{{end}}</td>
<td>{{.Family}}</td>
<td>{{if .Timeout}}{{.Timeout}} s{{else}}<span class="text-muted"></span>{{end}}</td>
<td style="text-align:center">{{len .Entry}}</td>
<td style="text-align:right">
<button type="button" class="btn-icon btn-icon-danger"
hx-delete="/configure/firewall/address-sets/{{.Name}}"
hx-confirm="Delete address-set {{.Name}}?"
hx-swap="none"
title="Delete address-set">
{{template "icon-trash"}}
</button>
</td>
</tr>
<tr class="key-detail-row" id="key-detail-addrset-{{$i}}">
<td colspan="6" class="key-detail-cell">
<div class="key-detail-body">
<form hx-post="/configure/firewall/address-sets/{{.Name}}" hx-swap="none">
<table class="info-table">
<tr>
<th>Description{{template "field-info" (index $d "addrset-description")}}</th>
<td colspan="2"><input class="cfg-input" type="text" name="description"
value="{{.Description}}" placeholder="Optional description"></td>
</tr>
<tr>
<th>Family{{template "field-info" (index $d "addrset-family")}}</th>
<td colspan="2">
{{$fam := .Family}}
<div class="yt-bool-group">
{{range $.FamilyOptions}}
<label><input type="radio" name="family" value="{{.Value}}"
{{if or (eq $fam .Value) (and (eq $fam "") .IsDefault)}}checked{{end}}> {{.Label}}</label>
{{end}}
</div>
</td>
</tr>
<tr>
<th>Timeout{{template "field-info" (index $d "addrset-timeout")}}</th>
<td colspan="2">
<input class="cfg-input cfg-input-sm" type="number" name="timeout"
min="1" value="{{if .Timeout}}{{.Timeout}}{{end}}" placeholder="—" style="width:8rem"> seconds
<span class="text-muted" style="font-size:0.8em;display:block;margin-top:0.25em">Timeout sets are dynamic-only: entries expire on their own and static entries cannot be configured.</span>
</td>
</tr>
<tr>
<th>Entries{{template "field-info" (index $d "addrset-entry")}}</th>
<td colspan="2">
<textarea class="cfg-input" name="entries" rows="5"
placeholder="One address or prefix per line, e.g. 192.168.1.40 or 10.0.0.0/24">{{.EntriesTxt}}</textarea>
</td>
</tr>
</table>
<div class="cfg-card-footer" style="padding-left:0">
<button class="btn btn-primary btn-sm" type="submit">Save</button>
<button type="button" class="btn btn-sm" data-close-detail="addrset-{{$i}}">Cancel</button>
<span class="cfg-save-status"></span>
</div>
</form>
</div>
</td>
</tr>
{{end}}
{{if not .AddressSets}}
<tr><td colspan="6" class="yt-table-empty">No address-sets configured</td></tr>
{{end}}
{{/* Hidden add-address-set row */}}
<tr id="add-addrset-row" hidden>
<td colspan="6" class="key-detail-cell">
<form hx-post="/configure/firewall/address-sets" hx-swap="none">
<table class="info-table">
<tr>
<th>Name{{template "field-info" (index $d "addrset-name")}}</th>
<td colspan="2"><input class="cfg-input" type="text" name="name" required
pattern="[a-zA-Z0-9\-_]+" placeholder="e.g. allowed"></td>
</tr>
<tr>
<th>Description{{template "field-info" (index $d "addrset-description")}}</th>
<td colspan="2"><input class="cfg-input" type="text" name="description"
placeholder="Optional description"></td>
</tr>
<tr>
<th>Family{{template "field-info" (index $d "addrset-family")}}</th>
<td colspan="2">
<div class="yt-bool-group">
{{range .FamilyOptions}}
<label><input type="radio" name="family" value="{{.Value}}" {{if .IsDefault}}checked{{end}}> {{.Label}}</label>
{{end}}
</div>
</td>
</tr>
<tr>
<th>Timeout{{template "field-info" (index $d "addrset-timeout")}}</th>
<td colspan="2">
<input class="cfg-input cfg-input-sm" type="number" name="timeout"
min="1" placeholder="—" style="width:8rem"> seconds
<span class="text-muted" style="font-size:0.8em;display:block;margin-top:0.25em">Leave blank for a regular set with static entries.</span>
</td>
</tr>
<tr>
<th>Entries{{template "field-info" (index $d "addrset-entry")}}</th>
<td colspan="2">
<textarea class="cfg-input" name="entries" rows="5"
placeholder="One address or prefix per line, e.g. 192.168.1.40 or 10.0.0.0/24"></textarea>
</td>
</tr>
</table>
<div class="cfg-card-footer" style="padding-left:0">
<button class="btn btn-primary btn-sm" type="submit">Add</button>
<button type="button" class="btn btn-sm" data-hide="add-addrset-row">Cancel</button>
<span class="cfg-save-status"></span>
</div>
</form>
</td>
</tr>
</tbody>
</table>
</div>
<div class="cfg-card-footer">
<button type="button" class="btn-add-row" data-show="add-addrset-row">+ Add Address Set</button>
</div>
</section>
</section> </section>
{{end}} {{end}}
@@ -731,4 +906,3 @@
{{end}}{{/* end {{else}} — schema ready */}} {{end}}{{/* end {{else}} — schema ready */}}
{{end}} {{end}}
+36
View File
@@ -90,6 +90,7 @@
<th>Action</th> <th>Action</th>
<th>Interfaces</th> <th>Interfaces</th>
<th>Networks</th> <th>Networks</th>
<th>Address Sets</th>
<th>Host Services</th> <th>Host Services</th>
</tr> </tr>
</thead> </thead>
@@ -100,6 +101,7 @@
<td><span class="badge badge-{{.Action}}">{{.Action}}</span></td> <td><span class="badge badge-{{.Action}}">{{.Action}}</span></td>
<td>{{.Interfaces}}</td> <td>{{.Interfaces}}</td>
<td>{{if .Networks}}{{.Networks}}{{else}}<span class="text-muted"></span>{{end}}</td> <td>{{if .Networks}}{{.Networks}}{{else}}<span class="text-muted"></span>{{end}}</td>
<td>{{if .AddressSets}}{{.AddressSets}}{{else}}<span class="text-muted"></span>{{end}}</td>
<td>{{if .Services}}{{.Services}}{{else}}<span class="text-muted"></span>{{end}}</td> <td>{{if .Services}}{{.Services}}{{else}}<span class="text-muted"></span>{{end}}</td>
</tr> </tr>
{{end}} {{end}}
@@ -109,6 +111,40 @@
</div> </div>
{{end}} {{end}}
{{if .AddressSets}}
<div class="info-card info-grid-span-2">
<div class="card-header">Address Sets</div>
<div class="data-table-wrap">
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Family</th>
<th>Timeout</th>
<th>Entries</th>
</tr>
</thead>
<tbody>
{{range .AddressSets}}
<tr>
<td>{{.Name}}</td>
<td>{{.Family}}</td>
<td>{{if .Timeout}}{{.Timeout}}{{else}}<span class="text-muted"></span>{{end}}</td>
<td>
{{if .Entries}}
{{range $i, $e := .Entries}}{{if $i}}, {{end}}<code>{{.Entry}}</code>{{if .Dynamic}} <span class="badge badge-neutral" title="Added at runtime, not saved to configuration{{if .Expires}}, expires in {{.Expires}}{{end}}">dyn{{if .Expires}} {{.Expires}}{{end}}</span>{{end}}{{end}}
{{else}}
<span class="text-muted">(empty)</span>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{end}}
{{if .Policies}} {{if .Policies}}
<div class="info-card info-grid-span-2"> <div class="info-card info-grid-span-2">
<div class="card-header">Policies</div> <div class="card-header">Policies</div>
+3
View File
@@ -37,3 +37,6 @@
- "json/bloated.json" - "json/bloated.json"
- "show-interfaces" - "show-interfaces"
- "-n br0" - "-n br0"
- case: firewall_overview.sh
name: "firewall-overview"
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh
SCRIPT_PATH="$(dirname "$(readlink -f "$0")")"
JSON="$SCRIPT_PATH/json/firewall-overview.json"
CLI="$SCRIPT_PATH/../../../src/statd/python/cli_pretty/cli_pretty.py"
strip_ansi() {
sed 's/\x1b\[[0-9;]*m//g'
}
echo "1..2"
OUT1="$(cat "$JSON" | "$CLI" show-firewall | strip_ansi)"
if printf '%s\n' "$OUT1" | grep -q "Address Sets" &&
printf '%s\n' "$OUT1" | grep -q "allowed" &&
printf '%s\n' "$OUT1" | grep -q "greylist" &&
printf '%s\n' "$OUT1" | grep -q "ADDR SET" &&
printf '%s\n' "$OUT1" | grep -q "trusted" &&
printf '%s\n' "$OUT1" | grep -q "greylist" ; then
echo "ok 1 - show-firewall includes address-set summaries"
else
echo "not ok 1 - show-firewall missing address-set overview"
exit 1
fi
OUT2="$(cat "$JSON" | "$CLI" show-firewall-zone trusted | strip_ansi)"
if printf '%s\n' "$OUT2" | grep -q "address-sets" &&
printf '%s\n' "$OUT2" | grep -q "allowed, greylist" ; then
echo "ok 2 - show-firewall-zone shows zone address-sets"
exit 0
fi
echo "not ok 2 - show-firewall-zone missing zone address-sets"
exit 1
@@ -0,0 +1,61 @@
{
"infix-firewall:firewall": {
"default": "public",
"logging": "all",
"zone": [
{
"name": "mgmt",
"action": "accept",
"interface": ["e0"],
"service": ["ssh", "netconf", "restconf"]
},
{
"name": "public",
"action": "drop",
"interface": ["e1"]
},
{
"name": "trusted",
"action": "accept",
"address-set": ["allowed", "greylist"]
}
],
"address-set": [
{
"name": "allowed",
"family": "ipv4",
"current": [
{
"entry": "192.168.1.40",
"dynamic": false
},
{
"entry": "192.168.1.50",
"dynamic": true
}
]
},
{
"name": "greylist",
"family": "ipv4",
"timeout": 10,
"current": [
{
"entry": "192.168.1.60",
"dynamic": true,
"expires": 7
}
]
}
],
"policy": [
{
"name": "public-to-mgmt",
"action": "reject",
"priority": 100,
"ingress": ["public"],
"egress": ["mgmt"]
}
]
}
}
+1
View File
@@ -0,0 +1 @@
test.adoc
+39
View File
@@ -0,0 +1,39 @@
=== Firewall Address-Sets with Dynamic Entries
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/firewall/address-set]
==== Description
Verifies firewall address-sets used as zone sources, in a setup where
all traffic from the data network is dropped by default and end devices
are granted access per-IP at runtime.
image::topology.svg[align=center, scaledwidth=50%]
- The default zone "public" drops all traffic on the data interface
- The "trusted" zone accepts traffic from members of the "allowed"
address-set: one static entry from the configuration, and dynamic
entries managed at runtime with the add/remove/flush actions
- Dynamic entries must survive unrelated firewall configuration
changes, but are not saved to the configuration
- Static entries cannot be removed with the remove action
- Entries in timeout sets ("greylist") expire on their own
==== Topology
image::topology.svg[Firewall Address-Sets with Dynamic Entries topology, align=center, scaledwidth=75%]
==== Sequence
. Set up topology and attach to target
. Configure firewall with address-set as zone source
. Verify host is blocked by default
. Add dynamic entry for host, verify it is allowed
. Verify operational state distinguishes static/dynamic
. Verify dynamic entry survives configuration change
. Verify static entry cannot be removed with action
. Remove dynamic entry, verify host is blocked
. Flush dynamic entries, static remain
. Verify entries in timeout set expire on their own
+194
View File
@@ -0,0 +1,194 @@
#!/usr/bin/env python3
"""Firewall Address-Sets with Dynamic Entries
Verifies firewall address-sets used as zone sources, in a setup where
all traffic from the data network is dropped by default and end devices
are granted access per-IP at runtime.
image::topology.svg[align=center, scaledwidth=50%]
- The default zone "public" drops all traffic on the data interface
- The "trusted" zone accepts traffic from members of the "allowed"
address-set: one static entry from the configuration, and dynamic
entries managed at runtime with the add/remove/flush actions
- Dynamic entries must survive unrelated firewall configuration
changes, but are not saved to the configuration
- Static entries cannot be removed with the remove action
- Entries in timeout sets ("greylist") expire on their own
"""
import infamy
from infamy.util import until
def get_address_set(target, name):
"""Fetch operational state for a named address-set"""
data = target.get_data("/infix-firewall:firewall")
sets = data["firewall"].get("address-set", [])
return next((s for s in sets if s["name"] == name), None)
def current_entries(target, name):
"""Return {entry: dynamic} for the live contents of an address-set"""
aset = get_address_set(target, name)
if not aset:
return {}
return {c["entry"]: c["dynamic"] for c in aset.get("current", [])}
def must_fail(fn, *args):
"""Assert that an action call raises an error"""
try:
fn(*args)
except Exception:
return
raise AssertionError(f"{args} unexpectedly succeeded")
with infamy.Test() as test:
ALLOWED = "/infix-firewall:firewall/address-set[name='allowed']"
GREYLIST = "/infix-firewall:firewall/address-set[name='greylist']"
with test.step("Set up topology and attach to target"):
env = infamy.Env()
target = env.attach("target", "mgmt")
_, data_if = env.ltop.xlate("target", "data")
_, mgmt_if = env.ltop.xlate("target", "mgmt")
_, host_data = env.ltop.xlate("host", "data")
TARGET_IP = "192.168.1.1"
HOST_IP = "192.168.1.42"
STATIC_IP = "192.168.1.40"
EXTRA_IP = "192.168.1.50"
with test.step("Configure firewall with address-set as zone source"):
target.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [{
"name": data_if,
"enabled": True,
"ipv4": {
"address": [{
"ip": TARGET_IP,
"prefix-length": 24
}]
}
}]
}
},
"infix-firewall": {
"firewall": {
"default": "public",
"logging": "all",
"address-set": [{
"name": "allowed",
"description": "End devices allowed to access the target",
"entry": [STATIC_IP]
}, {
"name": "greylist",
"description": "Entries expire on their own",
"timeout": 10
}],
"zone": [{
"name": "mgmt",
"description": "Management network - for test automation",
"action": "accept",
"interface": [mgmt_if],
"service": ["ssh", "netconf", "restconf"]
}, {
"name": "public",
"description": "Untrusted data network, drop everything",
"action": "drop",
"interface": [data_if]
}, {
"name": "trusted",
"description": "Allowed end devices",
"action": "accept",
"address-set": ["allowed"]
}]
}
}
})
infamy.Firewall.wait_for_operational(target, {
"public": {"action": "drop"},
"trusted": {"action": "accept"},
"mgmt": {"action": "accept"}
})
aset = get_address_set(target, "allowed")
assert aset, "Address-set 'allowed' not found in operational"
assert STATIC_IP in aset.get("entry", []), \
f"Static entry {STATIC_IP} missing from configuration"
with infamy.IsolatedMacVlan(host_data) as ns:
ns.addip(HOST_IP)
with test.step("Verify host is blocked by default"):
ns.must_not_reach(TARGET_IP, timeout=5)
with test.step("Add dynamic entry for host, verify it is allowed"):
target.call_action(f"{ALLOWED}/add", {"entry": HOST_IP})
ns.must_reach(TARGET_IP, timeout=10)
with test.step("Verify operational state distinguishes static/dynamic"):
def entries_settled():
current = current_entries(target, "allowed")
return current.get(STATIC_IP) is False and \
current.get(HOST_IP) is True
until(entries_settled, attempts=10)
with test.step("Verify dynamic entry survives configuration change"):
target.put_config_dicts({
"infix-firewall": {
"firewall": {
"logging": "unicast"
}
}
})
ns.must_reach(TARGET_IP, timeout=30)
current = current_entries(target, "allowed")
assert current.get(HOST_IP) is True, \
f"Dynamic entry {HOST_IP} lost in firewall reload"
with test.step("Verify static entry cannot be removed with action"):
must_fail(target.call_action, f"{ALLOWED}/remove",
{"entry": STATIC_IP})
with test.step("Remove dynamic entry, verify host is blocked"):
target.call_action(f"{ALLOWED}/remove", {"entry": HOST_IP})
ns.must_not_reach(TARGET_IP, timeout=5)
with test.step("Flush dynamic entries, static remain"):
target.call_action(f"{ALLOWED}/add", {"entry": HOST_IP})
target.call_action(f"{ALLOWED}/add", {"entry": EXTRA_IP})
target.call_action(f"{ALLOWED}/flush")
def only_static_left():
current = current_entries(target, "allowed")
return list(current.keys()) == [STATIC_IP]
until(only_static_left, attempts=10)
ns.must_not_reach(TARGET_IP, timeout=5)
with test.step("Verify entries in timeout set expire on their own"):
target.call_action(f"{GREYLIST}/add", {"entry": "10.0.0.1"})
aset = get_address_set(target, "greylist")
assert aset, "Address-set 'greylist' not found in operational"
current = {c["entry"]: c for c in aset.get("current", [])}
assert "10.0.0.1" in current, "Entry missing from timeout set"
assert current["10.0.0.1"].get("expires") is not None, \
"Entry in timeout set has no expiry"
must_fail(target.call_action, f"{GREYLIST}/remove",
{"entry": "10.0.0.1"})
def entry_expired():
return "10.0.0.1" not in current_entries(target, "greylist")
until(entry_expired, attempts=20)
test.succeed()
@@ -0,0 +1,23 @@
graph "1x2" {
layout = "neato";
overlap = false;
esep = "+30";
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="host | { <mgmt> mgmt | <data> data }",
pos="10,10!",
requires="controller"
];
target [
label="{ <mgmt> mgmt | <data> data } | target",
pos="40,10!",
requires="infix",
];
host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
host:data -- target:data [color=black]
}
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 1x2 Pages: 1 -->
<svg width="299pt" height="55pt"
viewBox="0.00 0.00 299.02 55.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 51)">
<title>1x2</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-51 295.02,-51 295.02,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-0.5 0,-46.5 100,-46.5 100,-0.5 0,-0.5"/>
<text text-anchor="middle" x="25" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-0.5 50,-46.5 "/>
<text text-anchor="middle" x="75" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="50,-23.5 100,-23.5 "/>
<text text-anchor="middle" x="75" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
</g>
<!-- target -->
<g id="node2" class="node">
<title>target</title>
<polygon fill="none" stroke="black" points="175.02,-0.5 175.02,-46.5 291.02,-46.5 291.02,-0.5 175.02,-0.5"/>
<text text-anchor="middle" x="200.02" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="175.02,-23.5 225.02,-23.5 "/>
<text text-anchor="middle" x="200.02" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="225.02,-0.5 225.02,-46.5 "/>
<text text-anchor="middle" x="258.02" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
</g>
<!-- host&#45;&#45;target -->
<g id="edge1" class="edge">
<title>host:mgmt&#45;&#45;target:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-35.5C100,-35.5 175.02,-35.5 175.02,-35.5"/>
</g>
<!-- host&#45;&#45;target -->
<g id="edge2" class="edge">
<title>host:data&#45;&#45;target:data</title>
<path fill="none" stroke="black" stroke-width="2" d="M100,-11.5C100,-11.5 175.02,-11.5 175.02,-11.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+3
View File
@@ -2,6 +2,9 @@
- name: Basic Firewall for End Devices - name: Basic Firewall for End Devices
case: basic/test.py case: basic/test.py
- name: Firewall Address-Sets with Dynamic Entries
case: address-set/test.py
- name: LAN-WAN Firewall with Masquerading - name: LAN-WAN Firewall with Masquerading
case: lan-wan/test.py case: lan-wan/test.py
+2 -2
View File
@@ -58,7 +58,7 @@ SPEC_Q :=
endif endif
test-spec: test-spec:
@esc_infix_name="$(echo $(INFIX_NAME) | sed 's/\//\\\//g')"; \ @esc_infix_name="$(echo $(IX_NAME) | sed 's/\//\\\//g')"; \
sed 's/{REPLACE}/$(subst ",,$(esc_infix_name)) $(INFIX_VERSION)/' \ sed 's/{REPLACE}/$(subst ",,$(esc_infix_name)) $(INFIX_VERSION)/' \
$(spec-dir)/Readme.adoc.in > $(spec-dir)/Readme.adoc $(spec-dir)/Readme.adoc.in > $(spec-dir)/Readme.adoc
$(SPEC_Q)$(spec-dir)/generate_spec.py -s $(test-dir)/case/all.yaml -r $(BR2_EXTERNAL_INFIX_PATH) $(SPEC_DEBUG) $(SPEC_Q)$(spec-dir)/generate_spec.py -s $(test-dir)/case/all.yaml -r $(BR2_EXTERNAL_INFIX_PATH) $(SPEC_DEBUG)
@@ -86,7 +86,7 @@ xpath-coverage-report:
$(xpath_coverage_report_md) \ $(xpath_coverage_report_md) \
$(xpath_coverage_report_css) \ $(xpath_coverage_report_css) \
$(xpath_coverage_logo) \ $(xpath_coverage_logo) \
"$(subst ",,$(INFIX_NAME)) $(INFIX_VERSION)" \ "$(subst ",,$(IX_NAME)) $(INFIX_VERSION)" \
$(xpath_coverage_report_pdf) $(xpath_coverage_report_pdf)
# Unit tests run with random (-r) hostname and container name to # Unit tests run with random (-r) hostname and container name to
+86
View File
@@ -0,0 +1,86 @@
#!/bin/sh
# Populate the shared Buildroot download cache (dl/) with the source
# artifacts needed by all defconfigs, then optionally publish it to a
# download mirror using rsync. The dl/ directory layout is exactly what
# BR2_PRIMARY_SITE expects (dl/<package>/<tarball>), the mirror root is
# a plain copy of dl/, see configs/snippets/mirror.conf
#
# Intended to run nightly from cron on the file server, on every branch
# that should remain buildable from the mirror:
#
# 0 3 * * * cd $HOME/src/infix && git pull --ff-only -q && \
# ./utils/mirror-sync.sh /srv/pub
#
set -u
usage()
{
cat <<-EOF >&2
Usage: $0 [-o DIR] [DEST]
Run 'make <defconfig> source' for every defconfig in configs/,
downloading all required source artifacts to the shared dl/
directory. All tarball hashes are verified by Buildroot.
If DEST is given, dl/ is then published there with rsync. Git
caches (dl/*/git) are skipped and nothing is ever deleted from
DEST -- old release branches still reference old tarballs.
Options:
-o DIR Directory for scratch build trees. Speeds up
repeated runs (default: temporary, removed on exit)
EOF
exit "$1"
}
outdir=
while getopts "ho:" opt; do
case $opt in
h) usage 0;;
o) outdir=$OPTARG;;
*) usage 1;;
esac
done
shift $((OPTIND - 1))
dest=${1:-}
cd "$(dirname "$0")/.." || exit 1
exec 9> "${TMPDIR:-/tmp}/infix-mirror-sync.lock"
if ! flock -n 9; then
echo "$0: another instance is already running, skipping." >&2
exit 0
fi
if [ -z "$outdir" ]; then
outdir=$(mktemp -d)
trap 'rm -rf "$outdir"' EXIT INT TERM
fi
rc=0
for cfg in configs/*_defconfig; do
cfg=$(basename "$cfg")
O="$outdir/${cfg%_defconfig}"
echo "=== $cfg"
if ! make O="$O" "$cfg" >/dev/null; then
echo "$0: failed configuring $cfg, skipping." >&2
rc=1
continue
fi
if ! make O="$O" source; then
echo "$0: failed downloading sources for $cfg." >&2
rc=1
fi
# Drop this defconfig's build tree; the downloads live in the
# shared dl/ dir, and keeping every tree would exhaust the disk.
rm -rf "$O"
done
if [ -n "$dest" ]; then
rsync -a --exclude='/*/git' --exclude='*.lock' dl/ "$dest" || rc=1
fi
exit $rc
+5 -5
View File
@@ -33,7 +33,7 @@ Arguments:
Description: Description:
When called from Buildroot (no options), uses environment variables: When called from Buildroot (no options), uses environment variables:
BINARIES_DIR, BUILD_DIR, BR2_EXTERNAL_INFIX_PATH, RELEASE, INFIX_ID BINARIES_DIR, BUILD_DIR, BR2_EXTERNAL_INFIX_PATH, RELEASE, IX_ID
When called with -b/-r options, enters standalone mode and combines artifacts When called with -b/-r options, enters standalone mode and combines artifacts
from separate boot and rootfs sources. Useful for CI or manual image creation. from separate boot and rootfs sources. Useful for CI or manual image creation.
@@ -484,7 +484,7 @@ if [ -n "$STANDALONE" ]; then
export BUILD_DIR="$BOOT_DIR/build" export BUILD_DIR="$BOOT_DIR/build"
export BR2_EXTERNAL_INFIX_PATH export BR2_EXTERNAL_INFIX_PATH
export RELEASE="${RELEASE:-""}" export RELEASE="${RELEASE:-""}"
export INFIX_ID="${INFIX_ID:-"infix"}" export IX_ID="${IX_ID:-"infix"}"
# Add host tools to PATH (for genimage, bmaptool, etc.) # Add host tools to PATH (for genimage, bmaptool, etc.)
for dir in "$BOOT_DIR" "$ROOT_DIR"; do for dir in "$BOOT_DIR" "$ROOT_DIR"; do
@@ -550,7 +550,7 @@ fi
# Set defaults for optional variables # Set defaults for optional variables
: "${RELEASE:=""}" : "${RELEASE:=""}"
: "${INFIX_ID:="infix"}" : "${IX_ID:="infix"}"
# Download bootloader if requested # Download bootloader if requested
if [ -n "$DOWNLOAD_BOOT" ]; then if [ -n "$DOWNLOAD_BOOT" ]; then
@@ -626,12 +626,12 @@ fi
# For boot-only mode, append "-boot" to the target name in the image filename # For boot-only mode, append "-boot" to the target name in the image filename
if [ -n "$BOOT_ONLY" ]; then if [ -n "$BOOT_ONLY" ]; then
sed "s|#VERSION#|${RELEASE}|" "$GENIMAGE_TEMPLATE" | \ sed "s|#VERSION#|${RELEASE}|" "$GENIMAGE_TEMPLATE" | \
sed "s|#INFIX_ID#|${INFIX_ID}|" | \ sed "s|#IX_ID#|${IX_ID}|" | \
sed "s|#TARGET#|${TARGET}|" | \ sed "s|#TARGET#|${TARGET}|" | \
sed "s|-${TARGET}\.img|-${TARGET}-boot.img|" > "$GENIMAGE_CFG" sed "s|-${TARGET}\.img|-${TARGET}-boot.img|" > "$GENIMAGE_CFG"
else else
sed "s|#VERSION#|${RELEASE}|" "$GENIMAGE_TEMPLATE" | \ sed "s|#VERSION#|${RELEASE}|" "$GENIMAGE_TEMPLATE" | \
sed "s|#INFIX_ID#|${INFIX_ID}|" | \ sed "s|#IX_ID#|${IX_ID}|" | \
sed "s|#TARGET#|${TARGET}|" > "$GENIMAGE_CFG" sed "s|#TARGET#|${TARGET}|" > "$GENIMAGE_CFG"
fi fi