diff --git a/.github/workflows/build-boot.yml b/.github/workflows/build-boot.yml index 3678df4b3..43ea5d18a 100644 --- a/.github/workflows/build-boot.yml +++ b/.github/workflows/build-boot.yml @@ -78,7 +78,7 @@ jobs: - name: Configure ${{ matrix.defconfig }}_defconfig run: | - make ${{ matrix.defconfig }}_defconfig + make ${{ matrix.defconfig }}_defconfig apply-mirror - name: Build ${{ matrix.defconfig }}_defconfig run: | diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 39a08bdad..d13fe1415 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -173,7 +173,7 @@ jobs: export BUILD_DIR=$PWD/build export BR2_EXTERNAL_INFIX_PATH=$PWD export RELEASE="" - export INFIX_ID="infix" + export IX_ID="infix" for target in $TARGETS; do export BINARIES_DIR=$PWD/output_${target}/images diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index b3b9fe4e5..ec7a5f9bb 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -54,7 +54,7 @@ jobs: run: | target=${{ matrix.target }}_defconfig echo "Building $target ..." - make $target + make $target apply-mirror make - name: Generate SBOM from Build @@ -70,6 +70,25 @@ jobs: run: | 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 run: | cd output/ @@ -98,3 +117,8 @@ jobs: with: name: artifact-disk-image-${{ matrix.target }} path: output/images/*.qcow2 + + - uses: actions/upload-artifact@v7 + with: + name: artifact-pkg-${{ matrix.target }} + path: output/images/*.pkg diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aa28c36c9..86bb4c23e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -129,7 +129,7 @@ jobs: - name: Configure ${{ env.TARGET }} run: | - make ${{ env.TARGET }}_defconfig + make ${{ env.TARGET }}_defconfig apply-mirror - name: Cleanup stale containers and ports run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7de5995f0..b08d247b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -95,7 +95,7 @@ jobs: makeLatest: ${{ steps.rel.outputs.latest }} discussionCategory: ${{ steps.rel.outputs.cat }} bodyFile: release.md - artifacts: "*.tar.gz,*.qcow2" + artifacts: "*.tar.gz,*.qcow2,*.pkg,*.img.xz" - name: Summary run: | diff --git a/Config.in b/Config.in index f28758076..a21701127 100644 --- a/Config.in +++ b/Config.in @@ -2,7 +2,7 @@ source "$BR2_EXTERNAL_INFIX_PATH/board/Config.in" menu "Branding" -config INFIX_VENDOR +config IX_VENDOR string "Vendor name" default "KernelKit" help @@ -15,7 +15,7 @@ config INFIX_VENDOR Used for VENDOR_NAME in /etc/os-release and GNS3 appliance files. -config INFIX_VENDOR_HOME +config IX_VENDOR_HOME string "Vendor URL" help 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 -config INFIX_NAME +config IX_NAME string "Operating system name" default "Infix" help Mandatory. Used for identifying the OS as NAME in /etc/os-release and product_name in GNS3 appliance files. -config INFIX_ID +config IX_ID string "Operating system identifier" default "infix" help @@ -43,9 +43,9 @@ config INFIX_ID Mandatory. Used for identifying the OS as ID in /etc/os-release and in the generated image name: ID-ARCH-VERSION.img -config INFIX_IMAGE_ID +config IX_IMAGE_ID string "Operating system image name" - default "${INFIX_ID}-${BR2_ARCH}" + default "${IX_ID}-${BR2_ARCH}" help A lower-case string (no spaces or other characters outside of 0–9, a–z, ".", "_" 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 the IMAGE_ID with a '-' separator. -config INFIX_COMPATIBLE +config IX_COMPATIBLE string "Operating system compatible string" - default "${INFIX_IMAGE_ID}" + default "${IX_IMAGE_ID}" help A lower-case string (no spaces or other characters outside of 0–9, a–z, ".", "_" and "-"), used for image identification at upgrade. E.g., the RAUC [system] compatible string. - Mandatory. Defaults to $INFIX_IMAGE_ID, which in turn is composed - of $INFIX_ID-$BR2_ARCH. + Mandatory. Defaults to $IX_IMAGE_ID, which in turn is composed + of $IX_ID-$BR2_ARCH. -config INFIX_TAGLINE +config IX_TAGLINE string "Operating system tagline" default "Infix OS — Immutable.Friendly.Secure" help @@ -74,25 +74,25 @@ config INFIX_TAGLINE This is also show at boot when the system init process starts. -config INFIX_DESC +config IX_DESC string "Operating system description" help Optional. Used for long description texts about the OS. E.g., 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" help Used for identifying the OS, e.g. as HOME_URL in /etc/os-release -config INFIX_DOC +config IX_DOC string "Operating system docs" help Optional. Main documentation URL, will be shown in /etc/os-release as DOCUMENTATION_URL. -config INFIX_SUPPORT +config IX_SUPPORT string "Operating system support" help 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 maintainer_email in .gns3a. -config INFIX_OEM_PATH +config IX_OEM_PATH string "Path to OEM br2-external" help 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 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 /etc/os-release) is always derived from `git describe`. The global @@ -118,7 +118,7 @@ config INFIX_OEM_PATH endmenu # For /etc/os-release, uses CondtionArchitechture= from systemd.unit(5) -config INFIX_ARCH +config IX_ARCH string default "arm" if BR2_arm default "arm64" if BR2_aarch64 diff --git a/board/aarch64/acer-connect-vero-w6m/genimage.cfg.in b/board/aarch64/acer-connect-vero-w6m/genimage.cfg.in index bf76088dc..accc6c463 100644 --- a/board/aarch64/acer-connect-vero-w6m/genimage.cfg.in +++ b/board/aarch64/acer-connect-vero-w6m/genimage.cfg.in @@ -22,7 +22,7 @@ image var.ext4 { } } -image #INFIX_ID##VERSION#-vero-w-#TARGET#.img { +image #IX_ID##VERSION#-vero-w-#TARGET#.img { hdimage { partition-table-type = "gpt" gpt-no-backup = true diff --git a/board/aarch64/bananapi-bpi-r3/genimage.cfg.in b/board/aarch64/bananapi-bpi-r3/genimage.cfg.in index f9fc6fccf..dbd8053e2 100644 --- a/board/aarch64/bananapi-bpi-r3/genimage.cfg.in +++ b/board/aarch64/bananapi-bpi-r3/genimage.cfg.in @@ -24,7 +24,7 @@ image var.ext4 { } } -image #INFIX_ID##VERSION#-bpi-r3-#TARGET#.img { +image #IX_ID##VERSION#-bpi-r3-#TARGET#.img { hdimage { partition-table-type = "gpt" diff --git a/board/aarch64/bananapi-bpi-r4/genimage.cfg.in b/board/aarch64/bananapi-bpi-r4/genimage.cfg.in index 51bda51da..d5c66484f 100644 --- a/board/aarch64/bananapi-bpi-r4/genimage.cfg.in +++ b/board/aarch64/bananapi-bpi-r4/genimage.cfg.in @@ -24,7 +24,7 @@ image var.ext4 { } } -image #INFIX_ID##VERSION#-bpi-r4-#TARGET#.img { +image #IX_ID##VERSION#-bpi-r4-#TARGET#.img { hdimage { partition-table-type = "gpt" gpt-no-backup = true diff --git a/board/aarch64/bananapi-bpi-r64/genimage.cfg.in b/board/aarch64/bananapi-bpi-r64/genimage.cfg.in index eda4fa136..3a2733de6 100644 --- a/board/aarch64/bananapi-bpi-r64/genimage.cfg.in +++ b/board/aarch64/bananapi-bpi-r64/genimage.cfg.in @@ -24,7 +24,7 @@ image var.ext4 { } } -image #INFIX_ID##VERSION#-bpi-r64-#TARGET#.img { +image #IX_ID##VERSION#-bpi-r64-#TARGET#.img { hdimage { partition-table-type = "hybrid" # MT7622 TF-A partition driver detects GPT by checking MBR[0] diff --git a/board/aarch64/friendlyarm-nanopi-r2s/genimage.cfg.in b/board/aarch64/friendlyarm-nanopi-r2s/genimage.cfg.in index 8c0031b4c..5ff86227a 100644 --- a/board/aarch64/friendlyarm-nanopi-r2s/genimage.cfg.in +++ b/board/aarch64/friendlyarm-nanopi-r2s/genimage.cfg.in @@ -26,7 +26,7 @@ image var.ext4 { } } -image #INFIX_ID##VERSION#-nanopi-r2s-sdcard.img { +image #IX_ID##VERSION#-nanopi-r2s-sdcard.img { hdimage { partition-table-type = "gpt" } diff --git a/board/aarch64/marvell-espressobin/genimage.cfg.in b/board/aarch64/marvell-espressobin/genimage.cfg.in index f1c89ff50..3924fa4d8 100644 --- a/board/aarch64/marvell-espressobin/genimage.cfg.in +++ b/board/aarch64/marvell-espressobin/genimage.cfg.in @@ -24,7 +24,7 @@ image var.ext4 { } } -image #INFIX_ID##VERSION#-espressobin-#TARGET#.img { +image #IX_ID##VERSION#-espressobin-#TARGET#.img { hdimage { partition-table-type = "gpt" } diff --git a/board/aarch64/raspberrypi-rpi64/genimage.cfg.in b/board/aarch64/raspberrypi-rpi64/genimage.cfg.in index 88d7a580d..086ff1773 100644 --- a/board/aarch64/raspberrypi-rpi64/genimage.cfg.in +++ b/board/aarch64/raspberrypi-rpi64/genimage.cfg.in @@ -36,7 +36,7 @@ image var.ext4 { } } -image #INFIX_ID##VERSION#-rpi64-sdcard.img { +image #IX_ID##VERSION#-rpi64-sdcard.img { hdimage { partition-table-type = "hybrid" } diff --git a/board/arm/microchip-sama7g54-ek/genimage.cfg.in b/board/arm/microchip-sama7g54-ek/genimage.cfg.in index 0a8c5e3e6..8bd499f26 100644 --- a/board/arm/microchip-sama7g54-ek/genimage.cfg.in +++ b/board/arm/microchip-sama7g54-ek/genimage.cfg.in @@ -39,7 +39,7 @@ image var.ext4 { } } -image #INFIX_ID##VERSION#-sama7g54-ek-#TARGET#.img { +image #IX_ID##VERSION#-sama7g54-ek-#TARGET#.img { hdimage { partition-table-type = "hybrid" } diff --git a/board/arm/raspberrypi-rpi2/genimage.cfg.in b/board/arm/raspberrypi-rpi2/genimage.cfg.in index 494a84ac1..d5d7795b4 100644 --- a/board/arm/raspberrypi-rpi2/genimage.cfg.in +++ b/board/arm/raspberrypi-rpi2/genimage.cfg.in @@ -36,7 +36,7 @@ image var.ext4 { } } -image #INFIX_ID##VERSION#-rpi2-sdcard.img { +image #IX_ID##VERSION#-rpi2-sdcard.img { hdimage { partition-table-type = "hybrid" } diff --git a/board/common/Config.in b/board/common/Config.in index 468a208a3..e9d0576b3 100644 --- a/board/common/Config.in +++ b/board/common/Config.in @@ -11,7 +11,7 @@ source "$BR2_EXTERNAL_INFIX_PATH/board/common/image/image-readme/Config.in" endmenu -config QEMU_SCRIPTS +config IX_QEMU_SCRIPTS bool "QEMU scripts" default y help @@ -19,20 +19,20 @@ config QEMU_SCRIPTS directory, which can be used to launch virtual Infix instances. -menuconfig TRUSTED_KEYS +menuconfig IX_TRUSTED_KEYS bool "Trusted keys for image" help Keys that will be accepted for this image -config TRUSTED_KEYS_DEVELOPMENT +config IX_TRUSTED_KEYS_DEVELOPMENT bool "Development key" - depends on TRUSTED_KEYS + depends on IX_TRUSTED_KEYS -config TRUSTED_KEYS_DEVELOPMENT_PATH +config IX_TRUSTED_KEYS_DEVELOPMENT_PATH string - depends on TRUSTED_KEYS_DEVELOPMENT + depends on IX_TRUSTED_KEYS_DEVELOPMENT 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" - depends on TRUSTED_KEYS + depends on IX_TRUSTED_KEYS diff --git a/board/common/common.mk b/board/common/common.mk index 2fcfe2d4a..e0586379d 100644 --- a/board/common/common.mk +++ b/board/common/common.mk @@ -1,14 +1,14 @@ include $(BR2_EXTERNAL_INFIX_PATH)/board/common/image/image.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 -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 @$(call IXMSG,"Installing signing cert for RAUC") 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;) endef diff --git a/board/common/image/image-ext4-rauc/Config.in b/board/common/image/image-ext4-rauc/Config.in index 53ad796c4..f28a66048 100644 --- a/board/common/image/image-ext4-rauc/Config.in +++ b/board/common/image/image-ext4-rauc/Config.in @@ -1,4 +1,4 @@ -config IMAGE_EXT4_RAUC +config IX_IMAGE_EXT4_RAUC bool "RAUC upgrade bundle (ext4)" depends on BR2_TARGET_ROOTFS_EXT2 select BR2_PACKAGE_HOST_RAUC @@ -7,17 +7,17 @@ config IMAGE_EXT4_RAUC image. Intended for development boards whose bootloader does not support squashfs. -config IMAGE_EXT4_RAUC_KEY +config IX_IMAGE_EXT4_RAUC_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" help Path to the private key, in PKCS#8 format, used to sign the RAUC bundle; or a PKCS#11 URI. -config IMAGE_EXT4_RAUC_CERT +config IX_IMAGE_EXT4_RAUC_CERT 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" help Path to the X509 certificate which will be associated with diff --git a/board/common/image/image-itb-aux/Config.in b/board/common/image/image-itb-aux/Config.in index 4cda6cd6e..b576f5541 100644 --- a/board/common/image/image-itb-aux/Config.in +++ b/board/common/image/image-itb-aux/Config.in @@ -1,6 +1,6 @@ -config IMAGE_ITB_AUX +config IX_IMAGE_ITB_AUX bool "aux partition" - depends on IMAGE_ITB_ROOTFS + depends on IX_IMAGE_ITB_ROOTFS select BR2_PACKAGE_HOST_UBOOT_TOOLS select BR2_PACKAGE_HOST_GENIMAGE help diff --git a/board/common/image/image-itb-dl-release/Config.in b/board/common/image/image-itb-dl-release/Config.in index 0b6769e80..9b79ceb37 100644 --- a/board/common/image/image-itb-dl-release/Config.in +++ b/board/common/image/image-itb-dl-release/Config.in @@ -1,4 +1,4 @@ -menuconfig IMAGE_ITB_DL_RELEASE +menuconfig IX_IMAGE_ITB_DL_RELEASE bool "Download existing release" depends on !BR2_TARGET_ROOTFS_SQUASHFS help @@ -7,9 +7,9 @@ menuconfig IMAGE_ITB_DL_RELEASE Infix image, to create a full disk image that can be provisioned to an SD-card or eMMC. -config IMAGE_ITB_DL_RELEASE_URL +config IX_IMAGE_ITB_DL_RELEASE_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" help URL to release tarball. diff --git a/board/common/image/image-itb-gns3a/Config.in b/board/common/image/image-itb-gns3a/Config.in index 5afe23923..cf9606045 100644 --- a/board/common/image/image-itb-gns3a/Config.in +++ b/board/common/image/image-itb-gns3a/Config.in @@ -1,23 +1,23 @@ -menuconfig IMAGE_ITB_GNS3A +menuconfig IX_IMAGE_ITB_GNS3A bool "GNS3 Appliance (ITB)" depends on BR2_x86_64 - select IMAGE_ITB_QCOW + select IX_IMAGE_ITB_QCOW help Create a GNS3 appliance description that, together with the disk image, can be imported into GNS3. -config IMAGE_ITB_GNS3A_RAM +config IX_IMAGE_ITB_GNS3A_RAM int "Reserved RAM (MiB)" - depends on IMAGE_ITB_GNS3A + depends on IX_IMAGE_ITB_GNS3A default "192" help Amount of host RAM reserved for an appliance instance. Minimum supported size is 192M. -config IMAGE_ITB_GNS3A_IFNUM +config IX_IMAGE_ITB_GNS3A_IFNUM int "Number of interfaces" - depends on IMAGE_ITB_GNS3A + depends on IX_IMAGE_ITB_GNS3A default "1" help Number of Ethernet interfaces to create for an appliance instance. diff --git a/board/common/image/image-itb-qcow/Config.in b/board/common/image/image-itb-qcow/Config.in index 8262344fd..02b1c97be 100644 --- a/board/common/image/image-itb-qcow/Config.in +++ b/board/common/image/image-itb-qcow/Config.in @@ -1,6 +1,6 @@ -menuconfig IMAGE_ITB_QCOW +menuconfig IX_IMAGE_ITB_QCOW 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 help Compose a full disk image with redundant Linux OS partitions, @@ -11,9 +11,9 @@ menuconfig IMAGE_ITB_QCOW - Creating a GNS3 appliance - Developing/debugging issues in the boot process in QEMU -menuconfig IMAGE_ITB_QCOW_SIZE +menuconfig IX_IMAGE_ITB_QCOW_SIZE string "Image size" - depends on IMAGE_ITB_QCOW + depends on IX_IMAGE_ITB_QCOW default "512M" help 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 prompt "Bootloader" - depends on IMAGE_ITB_QCOW - default IMAGE_ITB_QCOW_BOOT_EFI if BR2_x86_64 - default IMAGE_ITB_QCOW_BOOT_NONE + depends on IX_IMAGE_ITB_QCOW + default IX_IMAGE_ITB_QCOW_BOOT_EFI if BR2_x86_64 + default IX_IMAGE_ITB_QCOW_BOOT_NONE -config IMAGE_ITB_QCOW_BOOT_NONE +config IX_IMAGE_ITB_QCOW_BOOT_NONE bool "None" help 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" help Create a boot partition from a directory containing an EFI boot application, e.g. GRUB. -config IMAGE_ITB_QCOW_BOOT_BIN +config IX_IMAGE_ITB_QCOW_BOOT_BIN bool "Binary" help Create a boot partition from a raw image containing the boot @@ -49,18 +49,18 @@ config IMAGE_ITB_QCOW_BOOT_BIN endchoice -config IMAGE_ITB_QCOW_BOOT_DATA +config IX_IMAGE_ITB_QCOW_BOOT_DATA string "Bootloader data" - depends on IMAGE_ITB_QCOW - depends on IMAGE_ITB_QCOW_BOOT_EFI || IMAGE_ITB_QCOW_BOOT_BIN + depends on IX_IMAGE_ITB_QCOW + depends on IX_IMAGE_ITB_QCOW_BOOT_EFI || IX_IMAGE_ITB_QCOW_BOOT_BIN default "${BINARIES_DIR}/efi-part/EFI" if BR2_x86_64 help 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" - depends on IMAGE_ITB_QCOW - depends on IMAGE_ITB_QCOW_BOOT_EFI || IMAGE_ITB_QCOW_BOOT_BIN + depends on IX_IMAGE_ITB_QCOW + depends on IX_IMAGE_ITB_QCOW_BOOT_EFI || IX_IMAGE_ITB_QCOW_BOOT_BIN default 0x8000 help Offset at which the bootloader partition is placed. Remember diff --git a/board/common/image/image-itb-qcow/image-itb-qcow.mk b/board/common/image/image-itb-qcow/image-itb-qcow.mk index be44478a6..bcf170ae6 100644 --- a/board/common/image/image-itb-qcow/image-itb-qcow.mk +++ b/board/common/image/image-itb-qcow/image-itb-qcow.mk @@ -6,8 +6,8 @@ # We can source the rootfs+aux from a local build, or from a # downloaded release; so adjust our dependencies accordingly. -IMAGE_ITB_QCOW_SRC-$(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_ROOTFS) := image-itb-rootfs image-itb-aux +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_CONFIG_VARS := BOOT_DATA BOOT_OFFSET SIZE diff --git a/board/common/image/image-itb-rauc/Config.in b/board/common/image/image-itb-rauc/Config.in index 27f7017d1..2c830a8cd 100644 --- a/board/common/image/image-itb-rauc/Config.in +++ b/board/common/image/image-itb-rauc/Config.in @@ -1,23 +1,23 @@ -menuconfig IMAGE_ITB_RAUC +menuconfig IX_IMAGE_ITB_RAUC bool "RAUC upgrade bundle (ITB)" - select IMAGE_ITB_ROOTFS + select IX_IMAGE_ITB_ROOTFS select BR2_PACKAGE_HOST_RAUC help Create RAUC upgrade bundle, for targets using ITB images, that can be used to upgrade a running system to this version of Infix. -config IMAGE_ITB_RAUC_KEY +config IX_IMAGE_ITB_RAUC_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" help Path to the private key, in PKCS#8 format, used to sign the RAUC bundle; or a PKCS#11 URI. -config IMAGE_ITB_RAUC_CERT +config IX_IMAGE_ITB_RAUC_CERT 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" help Path to the X509 certificate which will be associated with diff --git a/board/common/image/image-itb-rootfs/Config.in b/board/common/image/image-itb-rootfs/Config.in index fbb04d8da..4b8b995c5 100644 --- a/board/common/image/image-itb-rootfs/Config.in +++ b/board/common/image/image-itb-rootfs/Config.in @@ -1,4 +1,4 @@ -menuconfig IMAGE_ITB_ROOTFS +menuconfig IX_IMAGE_ITB_ROOTFS bool "rootfs.itb+.itbh" select BR2_TARGET_ROOTFS_SQUASHFS select BR2_PACKAGE_HOST_UBOOT_TOOLS @@ -7,9 +7,9 @@ menuconfig IMAGE_ITB_ROOTFS and extract detached header (.itbh) for U-Boot based targets. -config IMAGE_ITB_ROOTFS_KEY +config IX_IMAGE_ITB_ROOTFS_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" help Path to the private RSA key, in PKCS#8 format, used to sign diff --git a/board/common/image/image-readme/Config.in b/board/common/image/image-readme/Config.in index 293d12bc8..65e2b42ac 100644 --- a/board/common/image/image-readme/Config.in +++ b/board/common/image/image-readme/Config.in @@ -1,4 +1,4 @@ -config IMAGE_README +config IX_IMAGE_README bool "Install README.md in images" help Install a README.md with useful information about getting diff --git a/board/common/image/image-readme/image-readme.mk b/board/common/image/image-readme/image-readme.mk index f567989b8..6b2cdb876 100644 --- a/board/common/image/image-readme/image-readme.mk +++ b/board/common/image/image-readme/image-readme.mk @@ -13,6 +13,6 @@ $(BINARIES_DIR)/README.md: $(IMAGE_README_DIR)/README.md @mkdir -p $(BINARIES_DIR) @cp $< $@ -ifeq ($(IMAGE_README),y) +ifeq ($(IX_IMAGE_README),y) TARGETS_ROOTFS += image-readme endif diff --git a/board/common/image/ix-image.mk b/board/common/image/ix-image.mk index 2da7cc2f5..99eeb6d30 100644 --- a/board/common/image/ix-image.mk +++ b/board/common/image/ix-image.mk @@ -12,15 +12,17 @@ $(1): $$($(2)_DEPENDENCIES) BINARIES_DIR=$$(BINARIES_DIR) \ BR2_EXTERNAL_INFIX_PATH=$$(BR2_EXTERNAL_INFIX_PATH) \ ARTIFACT=$$(INFIX_ARTIFACT) \ - COMPATIBLE=$$(INFIX_COMPATIBLE) \ + COMPATIBLE=$$(IX_COMPATIBLE) \ 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) -ifeq ($$($(2)),y) +ifeq ($$($(3)),y) TARGETS_ROOTFS += $(1) endif 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))) diff --git a/board/common/lib.sh b/board/common/lib.sh index 89f7e61a8..81893812f 100644 --- a/board/common/lib.sh +++ b/board/common/lib.sh @@ -16,7 +16,7 @@ die() # DISK_IMAGE_SIZE="512" # 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. # # shellcheck disable=SC1090 diff --git a/board/common/post-build.sh b/board/common/post-build.sh index 305f77ad9..eef3e9492 100755 --- a/board/common/post-build.sh +++ b/board/common/post-build.sh @@ -37,7 +37,7 @@ EOF } 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 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" rm -f "$TARGET_DIR/etc/os-release" { - echo "NAME=\"$INFIX_NAME\"" - echo "ID=$INFIX_ID" - echo "PRETTY_NAME=\"$INFIX_TAGLINE $INFIX_VERSION\"" + echo "NAME=\"$IX_NAME\"" + echo "ID=$IX_ID" + echo "PRETTY_NAME=\"$IX_TAGLINE $INFIX_VERSION\"" echo "ID_LIKE=\"${ID}\"" echo "DEFAULT_HOSTNAME=$BR2_TARGET_GENERIC_HOSTNAME" echo "VERSION=\"${INFIX_VERSION}\"" echo "VERSION_ID=${INFIX_VERSION}" echo "BUILD_ID=\"${INFIX_BUILD_ID}\"" - if [ -n "$INFIX_IMAGE_ID" ]; then - echo "IMAGE_ID=\"$INFIX_IMAGE_ID\"" + if [ -n "$IX_IMAGE_ID" ]; then + echo "IMAGE_ID=\"$IX_IMAGE_ID\"" fi if [ -n "$INFIX_RELEASE" ]; then echo "IMAGE_VERSION=\"$INFIX_RELEASE\"" fi - echo "ARCHITECTURE=\"${INFIX_ARCH}\"" - echo "HOME_URL=$INFIX_HOME" - if [ -n "$INFIX_VENDOR" ]; then - echo "VENDOR_NAME=\"$INFIX_VENDOR\"" + echo "ARCHITECTURE=\"${IX_ARCH}\"" + echo "HOME_URL=$IX_HOME" + if [ -n "$IX_VENDOR" ]; then + echo "VENDOR_NAME=\"$IX_VENDOR\"" fi - if [ -n "$INFIX_VENDOR_HOME" ]; then - echo "VENDOR_HOME=\"$INFIX_VENDOR_HOME\"" + if [ -n "$IX_VENDOR_HOME" ]; then + echo "VENDOR_HOME=\"$IX_VENDOR_HOME\"" fi - if [ -n "$INFIX_DOC" ]; then - echo "DOCUMENTATION_URL=\"$INFIX_DOC\"" + if [ -n "$IX_DOC" ]; then + echo "DOCUMENTATION_URL=\"$IX_DOC\"" fi - if [ -n "$INFIX_SUPPORT" ]; then - echo "SUPPORT_URL=\"$INFIX_SUPPORT\"" + if [ -n "$IX_SUPPORT" ]; then + echo "SUPPORT_URL=\"$IX_SUPPORT\"" fi - if [ -n "$INFIX_DESC" ]; then - echo "INFIX_DESC=\"$INFIX_DESC\"" + if [ -n "$IX_DESC" ]; then + echo "INFIX_DESC=\"$IX_DESC\"" fi } > "$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")" # In case of ambguities, this is what the image was built from diff --git a/board/common/qemu/Config.in.in b/board/common/qemu/Config.in.in index 0d49ca1b2..aed6a61dd 100644 --- a/board/common/qemu/Config.in.in +++ b/board/common/qemu/Config.in.in @@ -1,85 +1,85 @@ mainmenu "QEMU Virtualization" -config QEMU_ARCH_IS_32 +config IX_QEMU_ARCH_IS_32 bool -config QEMU_ARCH_IS_64 +config IX_QEMU_ARCH_IS_64 bool choice prompt "Target Architecture" default @ARCH@ -config QEMU_riscv64 - bool "risv64" - select QEMU_ARCH_IS_64 +config IX_QEMU_riscv64 + bool "riscv64" + select IX_QEMU_ARCH_IS_64 -config QEMU_x86_64 +config IX_QEMU_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)" - select QEMU_ARCH_IS_32 + select IX_QEMU_ARCH_IS_32 -config QEMU_aarch64 +config IX_QEMU_aarch64 bool "AArch64 (little endian)" - select QEMU_ARCH_IS_64 + select IX_QEMU_ARCH_IS_64 endchoice choice prompt "Loader" - default QEMU_LOADER_KERNEL + default IX_QEMU_LOADER_KERNEL -config QEMU_LOADER_KERNEL +config IX_QEMU_LOADER_KERNEL bool "Kernel" -config QEMU_LOADER_UBOOT +config IX_QEMU_LOADER_UBOOT bool "U-Boot" - depends on QEMU_aarch64 + depends on IX_QEMU_aarch64 -config QEMU_LOADER_OVMF +config IX_QEMU_LOADER_OVMF bool "OVMF (UEFI)" - depends on QEMU_x86_64 + depends on IX_QEMU_x86_64 endchoice choice prompt "Rootfs type" - default QEMU_ROOTFS_INITRD + default IX_QEMU_ROOTFS_INITRD -config QEMU_ROOTFS_MMC +config IX_QEMU_ROOTFS_MMC bool "MMC" - depends on QEMU_aarch64 + depends on IX_QEMU_aarch64 -config QEMU_ROOTFS_INITRD +config IX_QEMU_ROOTFS_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" endchoice choice prompt "Console" - default QEMU_CONSOLE_SERIAL if QEMU_arm - default QEMU_CONSOLE_VIRTIO + default IX_QEMU_CONSOLE_SERIAL if IX_QEMU_arm + default IX_QEMU_CONSOLE_VIRTIO -config QEMU_CONSOLE_VIRTIO +config IX_QEMU_CONSOLE_VIRTIO bool "Virtio (hvc0)" -config QEMU_CONSOLE_SERIAL +config IX_QEMU_CONSOLE_SERIAL bool "Serial (ttyS0/ttyAMA0)" - depends on !QEMU_LOADER_OVMF + depends on !IX_QEMU_LOADER_OVMF endchoice -config QEMU_MACHINE +config IX_QEMU_MACHINE string "Select emulated machine" - default "qemu-system-arm -M virt,accel=kvm:tcg -cpu max" if QEMU_arm - default "qemu-system-aarch64 -M virt,accel=kvm:tcg -cpu cortex-a53" if QEMU_aarch64 - default "qemu-system-x86_64 -M pc,accel=kvm:tcg -cpu max" if QEMU_x86_64 + 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 IX_QEMU_aarch64 + default "qemu-system-x86_64 -M pc,accel=kvm:tcg -cpu max" if IX_QEMU_x86_64 help You should not have to change this setting, although you may want to tweak it, or change the acceleration. @@ -88,7 +88,7 @@ config QEMU_MACHINE the defconfig you started with. Currently Infix supports arm (AArch32), aarch64 (ARM64), and x86_64 (AMD64). -config QEMU_MACHINE_RAM +config IX_QEMU_MACHINE_RAM string "RAM size (k/M/G)" default "448M" help @@ -96,62 +96,62 @@ config QEMU_MACHINE_RAM if you get kernel panic with: "System is deadlocked on memory", try increasing this one. -config QEMU_KERNEL +config IX_QEMU_KERNEL string - depends on QEMU_LOADER_KERNEL - default "../zImage" if QEMU_arm - default "../Image" if QEMU_aarch64 - default "../bzImage" if QEMU_x86_64 + depends on IX_QEMU_LOADER_KERNEL + default "../zImage" if IX_QEMU_arm + default "../Image" if IX_QEMU_aarch64 + default "../bzImage" if IX_QEMU_x86_64 -config QEMU_BIOS +config IX_QEMU_BIOS string - depends on !QEMU_LOADER_KERNEL - default "../u-boot.bin" if QEMU_LOADER_UBOOT - default "../OVMF.fd" if QEMU_LOADER_OVMF + depends on !IX_QEMU_LOADER_KERNEL + default "../u-boot.bin" if IX_QEMU_LOADER_UBOOT + default "../OVMF.fd" if IX_QEMU_LOADER_OVMF -config QEMU_ROOTFS +config IX_QEMU_ROOTFS string - default "@DISK_IMG@" if !QEMU_ROOTFS_INITRD - default "../rootfs.squashfs" if QEMU_ROOTFS_INITRD + default "@DISK_IMG@" if !IX_QEMU_ROOTFS_INITRD + default "../rootfs.squashfs" if IX_QEMU_ROOTFS_INITRD -config QEMU_DTB_EXTEND +config IX_QEMU_DTB_EXTEND bool - depends on QEMU_LOADER_UBOOT - default y if QEMU_aarch64 + depends on IX_QEMU_LOADER_UBOOT + 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" - depends on QEMU_ROOTFS_INITRD + depends on IX_QEMU_ROOTFS_INITRD default "cfg.ext4" -config QEMU_RW_VAR_OPT +config IX_QEMU_RW_VAR_OPT bool "Separate writable /var" -if QEMU_RW_VAR_OPT -config QEMU_RW_VAR_SIZE +if IX_QEMU_RW_VAR_OPT +config IX_QEMU_RW_VAR_SIZE string "Size of /var" default "256M" -config QEMU_RW_VAR +config IX_QEMU_RW_VAR string "Writable /var layer" default "var.ext4" endif endif -config QEMU_VPD +config IX_QEMU_VPD bool "Emulate a Vital Product Data (VPD) Memory" -config QEMU_HOST +config IX_QEMU_HOST string "Export host filesystem path" default "/tmp" -config QEMU_APPEND +config IX_QEMU_APPEND 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" @@ -159,72 +159,72 @@ comment "RTC" choice prompt "Mode" - default QEMU_RTC_UTC + default IX_QEMU_RTC_UTC -config QEMU_RTC_UTC +config IX_QEMU_RTC_UTC bool "UTC" -config QEMU_RTC_LOCAL +config IX_QEMU_RTC_LOCAL bool "Local time" -config QEMU_RTC_RANDOM +config IX_QEMU_RTC_RANDOM bool "Random" endchoice choice prompt "Clock" - default QEMU_CLOCK_HOST + default IX_QEMU_CLOCK_HOST -config QEMU_CLOCK_HOST +config IX_QEMU_CLOCK_HOST bool "Host clock" -config QEMU_CLOCK_RT +config IX_QEMU_CLOCK_RT bool "Independent (monotonic)" -config QEMU_CLOCK_VM +config IX_QEMU_CLOCK_VM bool "Virtual" endchoice -config QEMU_RTC +config IX_QEMU_RTC string - default "utc" if QEMU_RTC_UTC - default "localtime" if QEMU_RTC_LOCAL - default "random" if QEMU_RTC_RANDOM + default "utc" if IX_QEMU_RTC_UTC + default "localtime" if IX_QEMU_RTC_LOCAL + default "random" if IX_QEMU_RTC_RANDOM -config QEMU_CLOCK +config IX_QEMU_CLOCK string - default "host" if QEMU_CLOCK_HOST - default "rt" if QEMU_CLOCK_RT - default "vm" if QEMU_CLOCK_VM + default "host" if IX_QEMU_CLOCK_HOST + default "rt" if IX_QEMU_CLOCK_RT + default "vm" if IX_QEMU_CLOCK_VM comment "Networking" choice prompt "Network Mode" - default QEMU_NET_USER + default IX_QEMU_NET_USER -config QEMU_NET_NONE +config IX_QEMU_NET_NONE bool "None" -config QEMU_NET_BRIDGE +config IX_QEMU_NET_BRIDGE bool "Bridged" -config QEMU_NET_USER +config IX_QEMU_NET_USER bool "User" -config QEMU_NET_TAP +config IX_QEMU_NET_TAP bool "TAP" -config QEMU_NET_ROCKER +config IX_QEMU_NET_ROCKER bool "Rocker" endchoice -config QEMU_NET_MODEL +config IX_QEMU_NET_MODEL string "Interface model" - default "virtio-net-device" if QEMU_arm + default "virtio-net-device" if IX_QEMU_arm default "virtio-net-pci" help 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. -config QEMU_NET_BRIDGE_DEV +config IX_QEMU_NET_BRIDGE_DEV string "Bridge device" - depends on QEMU_NET_BRIDGE + depends on IX_QEMU_NET_BRIDGE default "virbr0" -config QEMU_NET_USER_OPTS +config IX_QEMU_NET_USER_OPTS string "User mode options" - depends on QEMU_NET_USER + depends on IX_QEMU_NET_USER help Extra -nic user, -config QEMU_NET_TAP_N +config IX_QEMU_NET_TAP_N int "Number of TAPs" - depends on QEMU_NET_TAP + depends on IX_QEMU_NET_TAP default 1 -config QEMU_NET_PORTS +config IX_QEMU_NET_PORTS int "Number of Rocker switch ports" - depends on QEMU_NET_ROCKER + depends on IX_QEMU_NET_ROCKER default 10 diff --git a/board/common/qemu/qemu.mk b/board/common/qemu/qemu.mk index 336507aa8..e05d36eb0 100644 --- a/board/common/qemu/qemu.mk +++ b/board/common/qemu/qemu.mk @@ -10,7 +10,7 @@ qemu-kconfig = \ BR2_CONFIG="$(BINARIES_DIR)/qemu/.config" \ $(BUILD_DIR)/buildroot-config/$(1) $(2) "$(BINARIES_DIR)/qemu/Config.in" -ifeq ($(QEMU_SCRIPTS),y) +ifeq ($(IX_QEMU_SCRIPTS),y) .PHONY: 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 @mkdir -p $(dir $@) @sed \ - -e "s:@ARCH@:QEMU_$(BR2_ARCH):" \ + -e "s:@ARCH@:IX_QEMU_$(BR2_ARCH):" \ -e "s:@DISK_IMG@:../$(INFIX_ARTIFACT).qcow2:" \ < $< >$@ diff --git a/board/common/qemu/run.sh b/board/common/qemu/run.sh index 69c3d71e5..acc3baa9e 100755 --- a/board/common/qemu/run.sh +++ b/board/common/qemu/run.sh @@ -40,7 +40,7 @@ usage() echo " ARGS1 Args before the '--' separator are for kernel space" echo " -- Separator" 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 "Example:" echo " $prognm -- finit.debug" @@ -62,45 +62,45 @@ load_qemucfg() # shellcheck disable=SC1090 . "./.config" - [ "$CONFIG_QEMU_MACHINE" ] || die "Missing QEMU_MACHINE" - [ "$CONFIG_QEMU_ROOTFS" ] || die "Missing QEMU_ROOTFS" + [ "$CONFIG_IX_QEMU_MACHINE" ] || die "Missing IX_QEMU_MACHINE" + [ "$CONFIG_IX_QEMU_ROOTFS" ] || die "Missing IX_QEMU_ROOTFS" - [ -n "$CONFIG_QEMU_KERNEL" ] && [ -n "$CONFIG_QEMU_BIOS" ] \ - && die "QEMU_KERNEL conflicts with QEMU_BIOS" + [ -n "$CONFIG_IX_QEMU_KERNEL" ] && [ -n "$CONFIG_IX_QEMU_BIOS" ] \ + && die "IX_QEMU_KERNEL conflicts with IX_QEMU_BIOS" - [ -z "$CONFIG_QEMU_KERNEL" ] && [ -z "$CONFIG_QEMU_BIOS" ] \ - && die "QEMU_KERNEL or QEMU_BIOS must be set" + [ -z "$CONFIG_IX_QEMU_KERNEL" ] && [ -z "$CONFIG_IX_QEMU_BIOS" ] \ + && die "IX_QEMU_KERNEL or IX_QEMU_BIOS must be set" } loader_args() { - if [ "$CONFIG_QEMU_BIOS" ]; then - echo -n "-bios $CONFIG_QEMU_BIOS " - elif [ "$CONFIG_QEMU_KERNEL" ]; then - echo -n "-kernel $CONFIG_QEMU_KERNEL " + if [ "$CONFIG_IX_QEMU_BIOS" ]; then + echo -n "-bios $CONFIG_IX_QEMU_BIOS " + elif [ "$CONFIG_IX_QEMU_KERNEL" ]; then + echo -n "-kernel $CONFIG_IX_QEMU_KERNEL " fi } append_args() { # 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 " - elif [ "$CONFIG_QEMU_CONSOLE_VIRTIO" ]; then + elif [ "$CONFIG_IX_QEMU_CONSOLE_VIRTIO" ]; then echo -n "console=hvc0 " - elif [ "$CONFIG_QEMU_x86_64" ]; then + elif [ "$CONFIG_IX_QEMU_x86_64" ]; then echo -n "console=ttyS0 " - elif [ "$CONFIG_QEMU_aarch64" ]; then + elif [ "$CONFIG_IX_QEMU_aarch64" ]; then echo -n "console=ttyAMA0 " else die "Unknown console" 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=$((($(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} " - elif [ "$CONFIG_QEMU_ROOTFS_VSCSI" = "y" ]; then + elif [ "$CONFIG_IX_QEMU_ROOTFS_VSCSI" = "y" ]; then echo -n "root=PARTLABEL=primary " fi @@ -110,20 +110,20 @@ append_args() echo -n "debug " fi - echo -n "${QEMU_APPEND} ${QEMU_EXTRA_APPEND} " + echo -n "${IX_QEMU_APPEND} ${QEMU_EXTRA_APPEND} " } rootfs_args() { - if [ "$CONFIG_QEMU_ROOTFS_INITRD" = "y" ]; then - echo -n "-initrd $CONFIG_QEMU_ROOTFS " - elif [ "$CONFIG_QEMU_ROOTFS_MMC" = "y" ]; then + if [ "$CONFIG_IX_QEMU_ROOTFS_INITRD" = "y" ]; then + echo -n "-initrd $CONFIG_IX_QEMU_ROOTFS " + elif [ "$CONFIG_IX_QEMU_ROOTFS_MMC" = "y" ]; then echo -n "-device sdhci-pci " echo -n "-device sd-card,drive=mmc " - echo -n "-drive id=mmc,file=$CONFIG_QEMU_ROOTFS,if=none,format=raw " - elif [ "$CONFIG_QEMU_ROOTFS_VSCSI" = "y" ]; then + echo -n "-drive id=mmc,file=$CONFIG_IX_QEMU_ROOTFS,if=none,format=raw " + elif [ "$CONFIG_IX_QEMU_ROOTFS_VSCSI" = "y" ]; then # 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 "-device virtio-blk-device,drive=rootfs " else @@ -140,9 +140,9 @@ serial_args() echo -n "-chardev stdio,id=console0,mux=on " 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 " - elif [ "$CONFIG_QEMU_CONSOLE_SERIAL" ]; then + elif [ "$CONFIG_IX_QEMU_CONSOLE_SERIAL" ]; then echo -n "-serial chardev:console0 " else die "Unknown console" @@ -175,7 +175,7 @@ usb_args() rw_args() { - [ "$CONFIG_QEMU_RW" ] || return + [ "$CONFIG_IX_QEMU_RW" ] || return 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 fi - if ! [ -f "$CONFIG_QEMU_RW" ]; then - dd if=/dev/zero of="$CONFIG_QEMU_RW" bs=16M count=1 >/dev/null 2>&1 - mkfs.ext4 -L cfg "$CONFIG_QEMU_RW" >/dev/null 2>&1 + if ! [ -f "$CONFIG_IX_QEMU_RW" ]; then + dd if=/dev/zero of="$CONFIG_IX_QEMU_RW" bs=16M count=1 >/dev/null 2>&1 + mkfs.ext4 -L cfg "$CONFIG_IX_QEMU_RW" >/dev/null 2>&1 fi # 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 "-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 " - if [ "$CONFIG_QEMU_RW_VAR_OPT" ]; then - if ! [ -f "$CONFIG_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 - mkfs.ext4 -L var "$CONFIG_QEMU_RW_VAR" >/dev/null 2>&1 + if [ "$CONFIG_IX_QEMU_RW_VAR_OPT" ]; then + if ! [ -f "$CONFIG_IX_QEMU_RW_VAR" ]; then + 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_IX_QEMU_RW_VAR" >/dev/null 2>&1 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 " fi else 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 ! [ -f "$CONFIG_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 - mkfs.ext4 -L var "$CONFIG_QEMU_RW_VAR" >/dev/null 2>&1 + if [ "$CONFIG_IX_QEMU_RW_VAR_OPT" ]; then + if ! [ -f "$CONFIG_IX_QEMU_RW_VAR" ]; then + 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_IX_QEMU_RW_VAR" >/dev/null 2>&1 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 } 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() @@ -230,7 +230,7 @@ net_dev_args() name="e$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" } @@ -252,29 +252,29 @@ net_args() :> "$mactab" echo -n "-fw_cfg name=opt/mactab,file=$mactab " - if [ "$CONFIG_QEMU_NET_BRIDGE" = "y" ]; then - echo -n "-netdev bridge,id=e1,br=$CONFIG_QEMU_NET_BRIDGE_DEV " + if [ "$CONFIG_IX_QEMU_NET_BRIDGE" = "y" ]; then + echo -n "-netdev bridge,id=e1,br=$CONFIG_IX_QEMU_NET_BRIDGE_DEV " net_dev_args 1 - elif [ "$CONFIG_QEMU_NET_TAP" = "y" ]; then - for i in $(seq 1 "$CONFIG_QEMU_NET_TAP_N"); do + elif [ "$CONFIG_IX_QEMU_NET_TAP" = "y" ]; then + for i in $(seq 1 "$CONFIG_IX_QEMU_NET_TAP_N"); do echo -n "-netdev tap,id=e$i,ifname=qtap$i " net_dev_args "$i" done - elif [ "$CONFIG_QEMU_NET_ROCKER" = "y" ]; then + elif [ "$CONFIG_IX_QEMU_NET_ROCKER" = "y" ]; then sw=sw0 # Only single switch support atm. echo -n "-device '{\"driver\":\"rocker\", \"name\":\"${sw}\", " echo -n "\"fp_start_macaddr\":\"02:00:00:00:00:01\", " 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 ", " echo -n "\"${sw}p${i}\"" done 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" done - elif [ "$CONFIG_QEMU_NET_USER" = "y" ]; then - [ "$CONFIG_QEMU_NET_USER_OPTS" ] && useropts=",$CONFIG_QEMU_NET_USER_OPTS" + elif [ "$CONFIG_IX_QEMU_NET_USER" = "y" ]; then + [ "$CONFIG_IX_QEMU_NET_USER_OPTS" ] && useropts=",$CONFIG_IX_QEMU_NET_USER_OPTS" echo -n "-netdev user,id=e1${useropts} " net_dev_args 1 else @@ -285,7 +285,7 @@ net_args() # Vital Product data vpd_args() { - [ "$CONFIG_QEMU_VPD" = "y" ] || return + [ "$CONFIG_IX_QEMU_VPD" = "y" ] || return vpd_file="${qdir}/vpd" @@ -324,8 +324,8 @@ random_date() rtc_args() { - rtc="${CONFIG_QEMU_RTC:-utc}" - clock="${CONFIG_QEMU_CLOCK:-host}" + rtc="${CONFIG_IX_QEMU_RTC:-utc}" + clock="${CONFIG_IX_QEMU_CLOCK:-host}" if [ "$rtc" = "random" ]; then rtc=$(random_date) fi @@ -358,28 +358,28 @@ extract_squashfs() run_qemu() { # Auto-extract rootfs.squashfs from rootfs.itb if needed for initrd mode - if [ "$CONFIG_QEMU_ROOTFS_INITRD" = "y" ] && [ ! -f "$CONFIG_QEMU_ROOTFS" ]; then - itb="${CONFIG_QEMU_ROOTFS%.squashfs}.itb" + if [ "$CONFIG_IX_QEMU_ROOTFS_INITRD" = "y" ] && [ ! -f "$CONFIG_IX_QEMU_ROOTFS" ]; then + itb="${CONFIG_IX_QEMU_ROOTFS%.squashfs}.itb" if [ -f "$itb" ]; then - extract_squashfs "$itb" "$CONFIG_QEMU_ROOTFS" + extract_squashfs "$itb" "$CONFIG_IX_QEMU_ROOTFS" 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 - if [ "$CONFIG_QEMU_ROOTFS_VSCSI" = "y" ]; then + if [ "$CONFIG_IX_QEMU_ROOTFS_VSCSI" = "y" ]; then if ! qemu-img check "qemu.qcow2"; then rm -f "qemu.qcow2" fi if [ ! -f "qemu.qcow2" ]; then 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 fi fi read -r qemu < "$run" - if [ "$CONFIG_QEMU_KERNEL" ]; then + if [ "$CONFIG_IX_QEMU_KERNEL" ]; then echo "$qemu -append \"$(append_args)\" $*" >> "$run" else echo "$qemu $*" >> "$run" @@ -419,9 +419,9 @@ EOF 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 # DT based on the system configuration. @@ -445,12 +445,12 @@ dtb_args() generate_dot() { - [ "$CONFIG_QEMU_NET_TAP" = "y" ] || return + [ "$CONFIG_IX_QEMU_NET_TAP" = "y" ] || return hostports=" qtap1" targetports=" e1" 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 " targetports="$targetports | e$tap " edges="$edges host:qtap$tap -- target:e$tap;" @@ -485,7 +485,7 @@ EOF 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" exec kconfig-mconf Config.in } diff --git a/board/common/rootfs/etc/sysctl.d/ipv4.conf b/board/common/rootfs/etc/sysctl.d/ipv4.conf index efc47f7a1..664a6a86c 100644 --- a/board/common/rootfs/etc/sysctl.d/ipv4.conf +++ b/board/common/rootfs/etc/sysctl.d/ipv4.conf @@ -24,6 +24,10 @@ net.ipv4.conf.all.arp_ignore=1 net.ipv4.ip_forward=1 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.default.forwarding=0 diff --git a/board/common/rootfs/etc/sysctl.d/ipv6.conf b/board/common/rootfs/etc/sysctl.d/ipv6.conf index fa683484e..d0f545285 100644 --- a/board/common/rootfs/etc/sysctl.d/ipv6.conf +++ b/board/common/rootfs/etc/sysctl.d/ipv6.conf @@ -7,6 +7,10 @@ net.ipv6.conf.all.ignore_routes_with_linkdown=1 net.ipv6.conf.all.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 net.ipv6.conf.all.accept_ra=2 net.ipv6.conf.default.accept_ra=2 diff --git a/board/common/uboot/uboot.mk b/board/common/uboot/uboot.mk index cd8f4b2a8..6159a3712 100644 --- a/board/common/uboot/uboot.mk +++ b/board/common/uboot/uboot.mk @@ -25,7 +25,7 @@ define UBOOT_PRE_BUILD_INSTALL_KEY $(HOST_DIR)/bin/dtc -a 1024 <(echo '/dts-v1/; / { signature {}; };') \ >$(@D)/infix-key.dtb $(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)) $(HOST_DIR)/bin/dtc -I dtb -O dts \ <$(@D)/infix-key.dtb \ @@ -46,3 +46,12 @@ define UBOOT_PRE_BUILD_INSTALL_ENV $(@D)/arch/$(UBOOT_ARCH)/dts/ endef 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 diff --git a/configs/aarch64_defconfig b/configs/aarch64_defconfig index 6176016b2..e7c3f39cd 100644 --- a/configs/aarch64_defconfig +++ b/configs/aarch64_defconfig @@ -143,11 +143,11 @@ BR2_PACKAGE_MARVELL_CN9130_CRB=y BR2_PACKAGE_MARVELL_ESPRESSOBIN=y BR2_PACKAGE_RASPBERRYPI_RPI64=y BR2_PACKAGE_STYX_DCP_SC_28P=y -INFIX_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." -INFIX_HOME="https://github.com/kernelkit/infix/" -INFIX_DOC="https://www.kernelkit.org/infix/" -INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" +IX_VENDOR_HOME="https://www.kernelkit.org" +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." +IX_HOME="https://github.com/kernelkit/infix/" +IX_DOC="https://www.kernelkit.org/infix/" +IX_SUPPORT="mailto:kernelkit@googlegroups.com" BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_FEATURE_WIFI=y BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y @@ -191,9 +191,9 @@ BR2_PACKAGE_WEBUI=y BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y BR2_PACKAGE_HOST_PYTHON_YANGDOC=y BR2_PACKAGE_PCIUTILS=y -IMAGE_ITB_AUX=y -IMAGE_ITB_QCOW=y -IMAGE_ITB_RAUC=y -IMAGE_README=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_IMAGE_ITB_AUX=y +IX_IMAGE_ITB_QCOW=y +IX_IMAGE_ITB_RAUC=y +IX_IMAGE_README=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/aarch64_minimal_defconfig b/configs/aarch64_minimal_defconfig index b032f8a18..2d19c91b8 100644 --- a/configs/aarch64_minimal_defconfig +++ b/configs/aarch64_minimal_defconfig @@ -121,11 +121,11 @@ BR2_PACKAGE_MARVELL_CN9130_CRB=y BR2_PACKAGE_MARVELL_ESPRESSOBIN=y BR2_PACKAGE_RASPBERRYPI_RPI64=y BR2_PACKAGE_STYX_DCP_SC_28P=y -INFIX_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." -INFIX_HOME="https://github.com/kernelkit/infix/" -INFIX_DOC="https://www.kernelkit.org/infix/" -INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" +IX_VENDOR_HOME="https://www.kernelkit.org" +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." +IX_HOME="https://github.com/kernelkit/infix/" +IX_DOC="https://www.kernelkit.org/infix/" +IX_SUPPORT="mailto:kernelkit@googlegroups.com" BR2_PACKAGE_CONFD=y BR2_PACKAGE_NETD=y BR2_PACKAGE_CONFD_TEST_MODE=y @@ -151,9 +151,9 @@ BR2_PACKAGE_MDNS_ALIAS=y BR2_PACKAGE_ONIEPROM=y BR2_PACKAGE_ROUSETTE=y BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y -IMAGE_ITB_AUX=y -IMAGE_ITB_QCOW=y -IMAGE_ITB_RAUC=y -IMAGE_README=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_IMAGE_ITB_AUX=y +IX_IMAGE_ITB_QCOW=y +IX_IMAGE_ITB_RAUC=y +IX_IMAGE_README=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/aarch64_qemu_boot_defconfig b/configs/aarch64_qemu_boot_defconfig index e15251810..c439dec03 100644 --- a/configs/aarch64_qemu_boot_defconfig +++ b/configs/aarch64_qemu_boot_defconfig @@ -25,5 +25,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/arm_defconfig b/configs/arm_defconfig index 7bd3beda7..127dabff3 100644 --- a/configs/arm_defconfig +++ b/configs/arm_defconfig @@ -129,6 +129,7 @@ BR2_PACKAGE_HOST_DOSFSTOOLS=y BR2_PACKAGE_HOST_E2FSPROGS=y BR2_PACKAGE_HOST_ENVIRONMENT_SETUP=y BR2_PACKAGE_HOST_GENEXT2FS=y +BR2_PACKAGE_HOST_GO_BIN=y BR2_PACKAGE_HOST_KMOD_XZ=y BR2_PACKAGE_HOST_MTOOLS=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_MICROCHIP_SAMA7G54_EK=y BR2_PACKAGE_RASPBERRYPI_RPI2=y -INFIX_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." -INFIX_HOME="https://github.com/kernelkit/infix/" -INFIX_DOC="https://www.kernelkit.org/infix/" -INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" +IX_VENDOR_HOME="https://www.kernelkit.org" +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." +IX_HOME="https://github.com/kernelkit/infix/" +IX_DOC="https://www.kernelkit.org/infix/" +IX_SUPPORT="mailto:kernelkit@googlegroups.com" BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y BR2_PACKAGE_FEATURE_WIFI_REALTEK=y @@ -173,9 +174,9 @@ BR2_PACKAGE_ROUSETTE=y BR2_PACKAGE_WEBUI=y BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y BR2_PACKAGE_HOST_PYTHON_YANGDOC=y -IMAGE_ITB_AUX=y -IMAGE_ITB_QCOW=y -IMAGE_ITB_RAUC=y -IMAGE_README=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_IMAGE_ITB_AUX=y +IX_IMAGE_ITB_QCOW=y +IX_IMAGE_ITB_RAUC=y +IX_IMAGE_README=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/arm_minimal_defconfig b/configs/arm_minimal_defconfig index 09ae64c84..ca4bf2a6d 100644 --- a/configs/arm_minimal_defconfig +++ b/configs/arm_minimal_defconfig @@ -119,11 +119,11 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y BR2_PACKAGE_MICROCHIP_SAMA7G54_EK=y BR2_PACKAGE_RASPBERRYPI_RPI2=y -INFIX_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." -INFIX_HOME="https://github.com/kernelkit/infix/" -INFIX_DOC="https://www.kernelkit.org/infix/" -INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" +IX_VENDOR_HOME="https://www.kernelkit.org" +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." +IX_HOME="https://github.com/kernelkit/infix/" +IX_DOC="https://www.kernelkit.org/infix/" +IX_SUPPORT="mailto:kernelkit@googlegroups.com" BR2_PACKAGE_CONFD=y BR2_PACKAGE_NETD=y BR2_PACKAGE_CONFD_TEST_MODE=y @@ -149,9 +149,9 @@ BR2_PACKAGE_MDNS_ALIAS=y BR2_PACKAGE_ONIEPROM=y BR2_PACKAGE_ROUSETTE=y BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y -IMAGE_ITB_AUX=y -IMAGE_ITB_QCOW=y -IMAGE_ITB_RAUC=y -IMAGE_README=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_IMAGE_ITB_AUX=y +IX_IMAGE_ITB_QCOW=y +IX_IMAGE_ITB_RAUC=y +IX_IMAGE_README=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/bpi_r3_emmc_boot_defconfig b/configs/bpi_r3_emmc_boot_defconfig index 387ed369e..2063555d7 100644 --- a/configs/bpi_r3_emmc_boot_defconfig +++ b/configs/bpi_r3_emmc_boot_defconfig @@ -40,5 +40,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/bpi_r3_sd_boot_defconfig b/configs/bpi_r3_sd_boot_defconfig index 2e3b28c6f..fb3bdf587 100644 --- a/configs/bpi_r3_sd_boot_defconfig +++ b/configs/bpi_r3_sd_boot_defconfig @@ -38,5 +38,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/bpi_r4_emmc_boot_defconfig b/configs/bpi_r4_emmc_boot_defconfig index 2ae3342aa..46a3d04f0 100644 --- a/configs/bpi_r4_emmc_boot_defconfig +++ b/configs/bpi_r4_emmc_boot_defconfig @@ -38,5 +38,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/bpi_r4_sd_boot_defconfig b/configs/bpi_r4_sd_boot_defconfig index 9f2a63e96..625045abd 100644 --- a/configs/bpi_r4_sd_boot_defconfig +++ b/configs/bpi_r4_sd_boot_defconfig @@ -38,5 +38,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/bpi_r64_emmc_boot_defconfig b/configs/bpi_r64_emmc_boot_defconfig index 425971894..f3e965b72 100644 --- a/configs/bpi_r64_emmc_boot_defconfig +++ b/configs/bpi_r64_emmc_boot_defconfig @@ -38,5 +38,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/bpi_r64_sd_boot_defconfig b/configs/bpi_r64_sd_boot_defconfig index 56f5f2acb..e78c2cfe7 100644 --- a/configs/bpi_r64_sd_boot_defconfig +++ b/configs/bpi_r64_sd_boot_defconfig @@ -38,5 +38,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/cn9130_crb_boot_defconfig b/configs/cn9130_crb_boot_defconfig index bc880db5e..13e1856bd 100644 --- a/configs/cn9130_crb_boot_defconfig +++ b/configs/cn9130_crb_boot_defconfig @@ -38,5 +38,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/nanopi_r2s_boot_defconfig b/configs/nanopi_r2s_boot_defconfig index 070d3dec4..65dc9e044 100644 --- a/configs/nanopi_r2s_boot_defconfig +++ b/configs/nanopi_r2s_boot_defconfig @@ -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_FDT_ADD_PUBKEY=y BR2_PACKAGE_BOOTLOADER_SPLASHSCREEN=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/riscv64_defconfig b/configs/riscv64_defconfig index b687676fc..5c0ad566f 100644 --- a/configs/riscv64_defconfig +++ b/configs/riscv64_defconfig @@ -164,14 +164,15 @@ BR2_TARGET_UBOOT_SPL=y 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_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_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y -INFIX_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." -INFIX_HOME="https://github.com/kernelkit/infix/" -INFIX_DOC="https://www.kernelkit.org/infix/" -INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" +IX_VENDOR_HOME="https://www.kernelkit.org" +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." +IX_HOME="https://github.com/kernelkit/infix/" +IX_DOC="https://www.kernelkit.org/infix/" +IX_SUPPORT="mailto:kernelkit@googlegroups.com" BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_FEATURE_WIFI=y BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y @@ -210,9 +211,9 @@ BR2_PACKAGE_WEBUI=y BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y BR2_PACKAGE_HOST_PYTHON_YANGDOC=y BR2_PACKAGE_PCIUTILS=y -IMAGE_ITB_AUX=y -IMAGE_ITB_QCOW=y -IMAGE_ITB_RAUC=y -IMAGE_README=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_IMAGE_ITB_AUX=y +IX_IMAGE_ITB_QCOW=y +IX_IMAGE_ITB_RAUC=y +IX_IMAGE_README=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/rpi64_boot_defconfig b/configs/rpi64_boot_defconfig index ba606ef7e..477f49f1d 100644 --- a/configs/rpi64_boot_defconfig +++ b/configs/rpi64_boot_defconfig @@ -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_FDT_ADD_PUBKEY=y BR2_PACKAGE_BOOTLOADER_SPLASHSCREEN=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/sama7g54_ek_emmc_boot_defconfig b/configs/sama7g54_ek_emmc_boot_defconfig index 5c76540cc..1b64c2051 100644 --- a/configs/sama7g54_ek_emmc_boot_defconfig +++ b/configs/sama7g54_ek_emmc_boot_defconfig @@ -34,5 +34,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/sama7g54_ek_sd_boot_defconfig b/configs/sama7g54_ek_sd_boot_defconfig index dc2f94560..9d8029418 100644 --- a/configs/sama7g54_ek_sd_boot_defconfig +++ b/configs/sama7g54_ek_sd_boot_defconfig @@ -34,5 +34,5 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/snippets/ext4.conf b/configs/snippets/ext4.conf index 20cfc728d..c2c42e67d 100644 --- a/configs/snippets/ext4.conf +++ b/configs/snippets/ext4.conf @@ -1,4 +1,4 @@ BR2_TARGET_ROOTFS_EXT2=y BR2_TARGET_ROOTFS_EXT2_4=y BR2_TARGET_ROOTFS_EXT2_SIZE="512M" -IMAGE_EXT4_RAUC=y +IX_IMAGE_EXT4_RAUC=y diff --git a/configs/snippets/mirror.conf b/configs/snippets/mirror.conf new file mode 100644 index 000000000..d359c022a --- /dev/null +++ b/configs/snippets/mirror.conf @@ -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" diff --git a/configs/x86_64_defconfig b/configs/x86_64_defconfig index 91bb7ce75..b23522910 100644 --- a/configs/x86_64_defconfig +++ b/configs/x86_64_defconfig @@ -139,11 +139,11 @@ BR2_PACKAGE_HOST_MTOOLS=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y -INFIX_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." -INFIX_HOME="https://github.com/kernelkit/infix/" -INFIX_DOC="https://www.kernelkit.org/infix/" -INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" +IX_VENDOR_HOME="https://www.kernelkit.org" +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." +IX_HOME="https://github.com/kernelkit/infix/" +IX_DOC="https://www.kernelkit.org/infix/" +IX_SUPPORT="mailto:kernelkit@googlegroups.com" BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_FEATURE_WIFI=y BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y @@ -185,9 +185,9 @@ BR2_PACKAGE_WEBUI=y BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y BR2_PACKAGE_HOST_PYTHON_YANGDOC=y BR2_PACKAGE_PCIUTILS=y -IMAGE_ITB_AUX=y -IMAGE_ITB_QCOW=y -IMAGE_ITB_RAUC=y -IMAGE_README=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_IMAGE_ITB_AUX=y +IX_IMAGE_ITB_QCOW=y +IX_IMAGE_ITB_RAUC=y +IX_IMAGE_README=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/configs/x86_64_minimal_defconfig b/configs/x86_64_minimal_defconfig index 747fd096e..533603a0f 100644 --- a/configs/x86_64_minimal_defconfig +++ b/configs/x86_64_minimal_defconfig @@ -118,11 +118,11 @@ BR2_PACKAGE_HOST_MTOOLS=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_HOST_UBOOT_TOOLS_FDT_ADD_PUBKEY=y -INFIX_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." -INFIX_HOME="https://github.com/kernelkit/infix/" -INFIX_DOC="https://www.kernelkit.org/infix/" -INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" +IX_VENDOR_HOME="https://www.kernelkit.org" +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." +IX_HOME="https://github.com/kernelkit/infix/" +IX_DOC="https://www.kernelkit.org/infix/" +IX_SUPPORT="mailto:kernelkit@googlegroups.com" BR2_PACKAGE_CONFD=y BR2_PACKAGE_NETD=y BR2_PACKAGE_CONFD_TEST_MODE=y @@ -148,9 +148,9 @@ BR2_PACKAGE_MDNS_ALIAS=y BR2_PACKAGE_ONIEPROM=y BR2_PACKAGE_ROUSETTE=y BR2_PACKAGE_RAUC_INSTALLATION_STATUS=y -IMAGE_ITB_AUX=y -IMAGE_ITB_QCOW=y -IMAGE_ITB_RAUC=y -IMAGE_README=y -TRUSTED_KEYS=y -TRUSTED_KEYS_DEVELOPMENT=y +IX_IMAGE_ITB_AUX=y +IX_IMAGE_ITB_QCOW=y +IX_IMAGE_ITB_RAUC=y +IX_IMAGE_README=y +IX_TRUSTED_KEYS=y +IX_TRUSTED_KEYS_DEVELOPMENT=y diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 1611a361c..2fa449f67 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -3,6 +3,21 @@ Change Log 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 ------------------------- @@ -2206,6 +2221,7 @@ Supported YANG models in addition to those used by sysrepo and netopeer: [buildroot]: https://buildroot.org/ [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.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 diff --git a/doc/branding.md b/doc/branding.md index 7056c95fa..4d9d468ec 100644 --- a/doc/branding.md +++ b/doc/branding.md @@ -28,7 +28,7 @@ Verify the result after a build by inspecting: > [!IMPORTANT] > 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 > file `/etc/os-release` as `BUILD_ID`, also in the file `/etc/version`. > 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 directory_. By default, the top directory refers to the root of the 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. #### `INFIX_RELEASE` diff --git a/doc/developers-guide.md b/doc/developers-guide.md index b1cdd2e4b..8598bbe5a 100644 --- a/doc/developers-guide.md +++ b/doc/developers-guide.md @@ -190,6 +190,9 @@ To apply a single snippet to the current output directory: make apply-ext4 # build an ext4 rootfs (needed for boards # whose bootloader lacks squashfs support, # 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 already run a `make _defconfig`). The snippet is merged using diff --git a/doc/firewall.md b/doc/firewall.md index 1f33052ab..692cf4c5c 100644 --- a/doc/firewall.md +++ b/doc/firewall.md @@ -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 > 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. + +
admin@example:/> configure
+admin@example:/config/> edit firewall address-set allowed
+admin@example:/config/firewall/…/allowed/> set description "End devices granted access"
+admin@example:/config/firewall/…/allowed/> set entry 192.168.1.40
+admin@example:/config/firewall/…/allowed/> end
+admin@example:/config/firewall/> edit zone trusted
+admin@example:/config/firewall/…/trusted/> set action accept
+admin@example:/config/firewall/…/trusted/> set address-set allowed
+admin@example:/config/firewall/…/trusted/> leave
+
+ +### 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: + +
admin@example:/> firewall address-set allowed add 192.168.1.42
+admin@example:/> show firewall address-set allowed
+name                : allowed
+family              : ipv4
+timeout             : none
+
+ENTRY            TYPE     EXPIRES
+192.168.1.40     static
+192.168.1.42     dynamic
+admin@example:/> firewall address-set allowed remove 192.168.1.42
+
+ +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. + +
admin@example:/config/firewall/> edit address-set banned
+admin@example:/config/firewall/…/banned/> set timeout 3600
+admin@example:/config/firewall/…/banned/> leave
+admin@example:/> firewall address-set banned add 203.0.113.99
+
+ +The remaining lifetime of each entry is shown in the `EXPIRES` column of +show firewall address-set. + +> [!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 ### End Device Protection diff --git a/infix.mk b/infix.mk index 31fc52f1e..887114382 100644 --- a/infix.mk +++ b/infix.mk @@ -1,6 +1,6 @@ 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)) # 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). export INFIX_BUILD_ID ?= $(shell git -C $(INFIX_TOPDIR) describe --dirty --always --tags --exclude 'latest*') 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 diff --git a/package/netbrowse/netbrowse.hash b/package/netbrowse/netbrowse.hash new file mode 100644 index 000000000..7f22fa7de --- /dev/null +++ b/package/netbrowse/netbrowse.hash @@ -0,0 +1 @@ +sha256 49c4d5edbd3ee0a0f6b15ef097b79539530a574ceed8a4dffec1d02455933029 LICENSE diff --git a/package/webui/webui.hash b/package/webui/webui.hash new file mode 100644 index 000000000..80277546d --- /dev/null +++ b/package/webui/webui.hash @@ -0,0 +1 @@ +sha256 8cdb4def6e91fde66ed0880a6e6ecc364aa96012e53b3cfb990b53b9dab976d4 LICENSE diff --git a/src/confd/bin/firewall b/src/confd/bin/firewall index 055b9096f..28a9834d3 100755 --- a/src/confd/bin/firewall +++ b/src/confd/bin/firewall @@ -103,6 +103,20 @@ panic_off() 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() { if is_panic_enabled; then @@ -294,6 +308,7 @@ OPTIONS: COMMANDS: reload Reload firewall configuration panic OPERATION Emergency panic mode: + ipset CMD SET ENTRY Runtime ipset operation: SET ENTRY show Show comprehensive firewall status and configuration help Show this help message @@ -386,6 +401,21 @@ main() ;; 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_status ;; diff --git a/src/confd/src/firewall.c b/src/confd/src/firewall.c index ba6969e84..be6a18311 100644 --- a/src/confd/src/firewall.c +++ b/src/confd/src/firewall.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -24,6 +25,11 @@ #define FIREWALLD_ZONES_DIR FIREWALLD_DIR_NEXT "/zones" #define FIREWALLD_SERVICES_DIR FIREWALLD_DIR_NEXT "/services" #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 { const char *yang; @@ -64,6 +70,148 @@ static const char *policy_action_to_target(const char *action) 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) { 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") fprintf(fp, " \n", lyd_get_value(node)); + LYX_LIST_FOR_EACH(lyd_child(cfg), node, "address-set") + fprintf(fp, " \n", lyd_get_value(node)); + LYX_LIST_FOR_EACH(lyd_child(cfg), node, "service") fprintf(fp, " \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); } +/* + * 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, " %s\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, "\n"); + fprintf(fp, " %s\n", name); + + if (desc) + fprintf(fp, " %s\n", desc); + + fprintf(fp, " \n"); + + return close_file(fp); +} + static int generate_service(struct lyd_node *cfg, const char *name) { 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)) { /* Firewall is disabled */ finit_disable("firewalld"); + rmrf(ADDRSET_RUNDIR); 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 */ rmrf(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) || fmkpath(0755, FIREWALLD_ZONES_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"); err = SR_ERR_SYS; 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") 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 */ clist = lydx_get_descendant(tree, "firewall", "policy", NULL); 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; } +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, 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) @@ -723,6 +1089,9 @@ int firewall_rpc_init(struct confd *confd) int rc; 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; fail: diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index e740c6cd4..2ad8291c7 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -38,7 +38,7 @@ MODULES=( "infix-dhcp-client@2025-11-09.yang" "infix-dhcpv6-client@2025-11-09.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-icmp-types@2025-04-26.yang" "infix-meta@2025-12-10.yang" diff --git a/src/confd/yang/confd/infix-firewall.yang b/src/confd/yang/confd/infix-firewall.yang index ca2119db1..4624abb0b 100644 --- a/src/confd/yang/confd/infix-firewall.yang +++ b/src/confd/yang/confd/infix-firewall.yang @@ -27,6 +27,13 @@ module infix-firewall { contact "kernelkit@googlegroups.com"; 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 { description "Initial revision."; reference "internal"; @@ -95,6 +102,14 @@ module infix-firewall { 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 { description "Network protocols supported for services and port definitions."; @@ -220,6 +235,21 @@ module infix-firewall { 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 { 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 { description "Current state of emergency lockdown mode."; config false; diff --git a/src/confd/yang/confd/infix-firewall@2025-04-26.yang b/src/confd/yang/confd/infix-firewall@2026-07-02.yang similarity index 100% rename from src/confd/yang/confd/infix-firewall@2025-04-26.yang rename to src/confd/yang/confd/infix-firewall@2026-07-02.yang diff --git a/src/klish-plugin-infix/src/infix.c b/src/klish-plugin-infix/src/infix.c index 01fc8f9cc..01bd48306 100644 --- a/src/klish-plugin-infix/src/infix.c +++ b/src/klish-plugin-infix/src/infix.c @@ -297,6 +297,64 @@ int infix_firewall_services(kcontext_t *ctx) "| 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) { 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_policies", infix_firewall_policies)); 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("shell", infix_shell)); kplugin_add_syms(plugin, ksym_new("ssh_connect", infix_ssh_connect)); diff --git a/src/klish-plugin-infix/xml/infix.xml b/src/klish-plugin-infix/xml/infix.xml index bfb3b9b3f..99f6f8893 100644 --- a/src/klish-plugin-infix/xml/infix.xml +++ b/src/klish-plugin-infix/xml/infix.xml @@ -134,6 +134,13 @@ + + + + + + + @@ -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-address-set "$KLISH_PARAM_name" |pager + + copy operational -x /infix-firewall:firewall | /usr/libexec/statd/cli-pretty show-firewall |pager @@ -826,6 +841,22 @@ echo "Public: $pub" + + + + + + + + + + + + + + + + diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index b4c5b34e4..9a58a7ee7 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -4149,6 +4149,7 @@ def show_firewall(json): # Create tables zone_table = firewall_zone_table(json) policy_table = firewall_policy_table(json) + address_set_table = firewall_address_set_table(json) # Add zone table if zone_table: @@ -4156,6 +4157,11 @@ def show_firewall(json): canvas.add_table(zone_table) canvas.add_spacing() + if address_set_table: + canvas.add_title("Address Sets") + canvas.add_table(address_set_table) + canvas.add_spacing() + # Add policy table if policy_table: canvas.add_title("Policies") @@ -4447,13 +4453,13 @@ def firewall_matrix(fw, width=None): zones = fw.get('zone', []) 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 = [] for z in zones: interfaces = z.get('interface', []) networks = z.get('network', []) - # Include if zone has interfaces OR networks (non-empty lists) - if interfaces or networks: + address_sets = z.get('address-set', []) + if interfaces or networks or address_sets: zone_names.append(z['name']) # Always add the implicit HOST zone @@ -4558,6 +4564,7 @@ def firewall_zone_table(json): Column('NAME', flexible=True), Column('TYPE'), Column('DATA', flexible=True), + Column('ADDR SET'), Column('ALLOWED HOST SERVICES', flexible=True) ]) @@ -4566,6 +4573,7 @@ def firewall_zone_table(json): action = zone.get('action', 'reject') interface_list = zone.get('interface', []) network_list = zone.get('network', []) + address_set_list = zone.get('address-set', []) port_forwards = zone.get('port-forward', []) services = zone.get('service', []) @@ -4586,7 +4594,7 @@ def firewall_zone_table(json): if interface_list: interfaces = compress_interface_list(interface_list) config_lines.append(("iif", interfaces)) - else: + elif not network_list and not address_set_list and not port_forwards: config_lines.append(("iif", "(none)")) # Networks @@ -4599,14 +4607,21 @@ def firewall_zone_table(json): pf_display = format_port_forwards(port_forwards) config_lines.append(("fwd", pf_display)) - # Add first line with zone name and services - if config_lines: - first_type, first_data = config_lines[0] - zone_table.row(locked, name, first_type, first_data, services_display) + addr_rows = address_set_list[:] if address_set_list else ["(none)"] + row_count = max(len(config_lines), len(addr_rows)) + if not config_lines: + config_lines = [("", "")] - # Add additional configuration lines as separate rows - for config_type, config_data in config_lines[1:]: - zone_table.row('', '', config_type, config_data, '') + for i in range(row_count): + if i < len(config_lines): + 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 @@ -4645,6 +4660,9 @@ def show_firewall_zone(json, zone_name=None): networks = zone.get('network', []) if not networks: networks = "" + address_sets = zone.get('address-set', []) + if not address_sets: + address_sets = "" services = zone.get('service', []) action = zone.get('action', 'reject') @@ -4660,6 +4678,7 @@ def show_firewall_zone(json, zone_name=None): print(f"{'action':<20}: {action}") print(f"{'interface':<20}: {compress_interface_list(interfaces)}") print(f"{'networks':<20}: {', '.join(networks)}") + print(f"{'address-sets':<20}: {', '.join(address_sets)}") print(f"{'services (to HOST)':<20}: {services_display}") # Show port forwards if any @@ -4928,6 +4947,75 @@ def show_firewall_service(json, name=None): 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): """Show OSPF general instance information""" routing = json_data.get('ietf-routing:routing', {}) @@ -6018,6 +6106,8 @@ def main(): .add_argument('name', nargs='?', help='Policy name') subparsers.add_parser('show-firewall-service', help='Show firewall services') \ .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') \ .add_argument('limit', nargs='?', help='Last N lines, default: all') @@ -6095,6 +6185,8 @@ def main(): show_firewall_policy(json_data, args.name) elif args.command == "show-firewall-service": 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": show_firewall_logs(args.limit) elif args.command == "show-nacm": diff --git a/src/statd/python/yanger/infix_firewall.py b/src/statd/python/yanger/infix_firewall.py index b06746ae4..cb5b4fc3a 100644 --- a/src/statd/python/yanger/infix_firewall.py +++ b/src/statd/python/yanger/infix_firewall.py @@ -7,8 +7,37 @@ --object-path /org/fedoraproject/FirewallD1 """ import dbus +import ipaddress import re 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"): @@ -52,13 +81,15 @@ def get_zone_data(fw, name): elif not short: short = "" + networks, ipsets = split_sources(settings.get('sources', [])) zone = { "name": name, "short": short, "immutable": immutable, "description": settings.get('description', ''), "interface": list(settings.get('interfaces', [])), - "network": list(settings.get('sources', [])), + "network": networks, + "address-set": ipsets, "action": action.get(target, "accept"), "service": list(settings.get('services', [])) } @@ -132,8 +163,10 @@ def get_zones(fw): for name, zone_info in active_zones.items(): zone_data = get_zone_data(fwz, name) if zone_data: + networks, ipsets = split_sources(zone_info.get('sources', [])) 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) except Exception as e: @@ -281,6 +314,100 @@ def get_policies(fw): 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): try: settings = fw.getServiceSettings2(name) @@ -360,4 +487,8 @@ def operational(): if 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 diff --git a/src/webui/internal/handlers/configure_firewall.go b/src/webui/internal/handlers/configure_firewall.go index 1f477dfec..a69e09c81 100644 --- a/src/webui/internal/handlers/configure_firewall.go +++ b/src/webui/internal/handlers/configure_firewall.go @@ -26,11 +26,6 @@ type cfgFwWrapper struct { 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 ──────────────────────────────────────────────────── type cfgZoneRow struct { @@ -38,8 +33,10 @@ type cfgZoneRow struct { IfaceCount int IfaceSet map[string]bool ServiceSet map[string]bool + AddrSetSet map[string]bool ServicesTxt string // fallback when ServiceOptions unavailable NetworksTxt string // comma-separated, shown read-only when zone uses networks + AddrSetsTxt string // comma-separated address-set sources } type cfgPolicyRow struct { @@ -56,9 +53,43 @@ type cfgServiceRow struct { PortsDisplay string // "tcp:80,443; udp:53" — at-a-glance } -// cfgFwSvcWrapper is used when reading a single service by path. -type cfgFwSvcWrapper struct { - Service []fwServiceJSON `json:"infix-firewall:service"` +type cfgAddrSetRow struct { + addressSetJSON + 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 ──────────────────────────────────────────────────────────── @@ -74,12 +105,15 @@ type cfgFirewallPageData struct { ZoneNames []string // for policy ingress/egress multi-select Policies []cfgPolicyRow Services []cfgServiceRow + AddressSets []cfgAddrSetRow + AddressSetNames []string // for zone source multi-select ProtoOptions []schema.IdentityOption Desc map[string]string LoggingOptions []schema.IdentityOption ActionOptions []schema.IdentityOption PolicyActionOptions []schema.IdentityOption ServiceOptions []schema.IdentityOption + FamilyOptions []schema.IdentityOption AllInterfaces []string Error string } @@ -107,6 +141,7 @@ func (h *ConfigureFirewallHandler) Overview(w http.ResponseWriter, r *http.Reque zPath := fwPath + "/zone" pPath := fwPath + "/policy" sPath := fwPath + "/service" + aPath := fwPath + "/address-set" data.Desc = map[string]string{ "enabled": schema.DescriptionOf(mgr, fwPath+"/enabled"), "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-upper": schema.DescriptionOf(mgr, sPath+"/port/upper"), "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.ActionOptions = schema.OptionsFor(mgr, zPath+"/action") data.PolicyActionOptions = schema.OptionsFor(mgr, pPath+"/action") data.ServiceOptions = schema.OptionsFor(mgr, zPath+"/service") data.ProtoOptions = schema.OptionsFor(mgr, sPath+"/port/proto") + data.FamilyOptions = schema.OptionsFor(mgr, aPath+"/family") } 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.Logging = fw.Logging 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{ zoneJSON: z, IfaceCount: len(z.Interface), - IfaceSet: ifaceSet, - ServiceSet: svcSet, + IfaceSet: toSet(z.Interface), + ServiceSet: toSet(z.Service), + AddrSetSet: toSet(z.AddressSet), ServicesTxt: strings.Join(z.Service, "\n"), NetworksTxt: strings.Join(z.Network, ", "), + AddrSetsTxt: strings.Join(z.AddressSet, ", "), }) 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 { masq := "—" if p.Masquerade { 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{ policyJSON: p, IngressDisplay: strings.Join(p.Ingress, ", "), EgressDisplay: strings.Join(p.Egress, ", "), MasqDisplay: masq, - IngressSet: ingressSet, - EgressSet: egressSet, + IngressSet: toSet(p.Ingress), + EgressSet: toSet(p.Egress), }) } 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") } -// 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, // port-forward). Note: port-forward entries are lost on save (Phase 3 limitation). // POST /configure/firewall/zones/{name} @@ -314,15 +357,15 @@ func (h *ConfigureFirewallHandler) SaveZone(w http.ResponseWriter, r *http.Reque } 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 { log.Printf("configure firewall zone save %q: GET: %v", name, err) renderSaveError(w, err) return } cur := zoneJSON{Name: name} - if len(wrap.Zone) > 0 { - cur = wrap.Zone[0] + if len(wrap.Firewall.Zone) > 0 { + cur = wrap.Firewall.Zone[0] } cur.Action = r.FormValue("action") @@ -340,20 +383,13 @@ func (h *ConfigureFirewallHandler) SaveZone(w http.ResponseWriter, r *http.Reque svcs = []string{} } cur.Service = svcs - - zone := map[string]any{ - "name": cur.Name, - "action": cur.Action, - "interface": cur.Interface, - "service": cur.Service, + sets := r.Form["address-sets"] + if sets == nil { + sets = []string{} } - 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}} + cur.AddressSet = sets + + body := map[string]any{"infix-firewall:zone": []map[string]any{zoneConfigBody(cur)}} 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) renderSaveError(w, err) @@ -362,52 +398,38 @@ func (h *ConfigureFirewallHandler) SaveZone(w http.ResponseWriter, r *http.Reque renderSavedRedirect(w, "Zone saved", "/configure/firewall") } -// ResetZoneLeafList clears a leaf-list (interface or service) on a zone by -// re-PUTting the zone container without that field. RFC 8040 leaf-list -// DELETE requires per-entry key predicates, so a bulk clear has to go -// through the parent. +// ResetZoneLeafList clears a leaf-list (interface or service) on a zone. +// RFC 8040 leaf-list DELETE requires per-entry key predicates, so each +// instance is deleted individually. Deliberately avoids rebuilding the +// 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) { 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 { log.Printf("configure firewall zone reset %s/%s: GET: %v", name, leaf, err) renderSaveError(w, err) return } - cur := zoneJSON{Name: name} - if len(wrap.Zone) > 0 { - cur = wrap.Zone[0] - } - switch leaf { - case "interface": - cur.Interface = nil - case "service": - cur.Service = nil + var values []string + if len(wrap.Firewall.Zone) > 0 { + switch leaf { + case "interface": + values = wrap.Firewall.Zone[0].Interface + case "service": + values = wrap.Firewall.Zone[0].Service + } } - zone := map[string]any{ - "name": cur.Name, - "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.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 + for _, val := range values { + path := fmt.Sprintf("%s/zone=%s/%s=%s", fwConfigPath, + 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 + } } - renderSaved(w, "Reset to default") + renderSavedRedirect(w, "Reset to default", "/configure/firewall") } // 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") } +// ─── 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 ───────────────────────────────────────────────────────────────── // fetchInterfaceNames returns configured interface names from candidate (fallback running). diff --git a/src/webui/internal/handlers/configure_firewall_test.go b/src/webui/internal/handlers/configure_firewall_test.go new file mode 100644 index 000000000..4d4b44dbe --- /dev/null +++ b/src/webui/internal/handlers/configure_firewall_test.go @@ -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) + } +} diff --git a/src/webui/internal/handlers/configure_interfaces.go b/src/webui/internal/handlers/configure_interfaces.go index f20734277..0f8da6451 100644 --- a/src/webui/internal/handlers/configure_interfaces.go +++ b/src/webui/internal/handlers/configure_interfaces.go @@ -1369,15 +1369,22 @@ func (h *ConfigureInterfacesHandler) ResetEthernetAdvertised(w http.ResponseWrit name := r.PathValue("name") base := ifacePath(name) + "/ieee802-ethernet-interface:ethernet" + // Keyed GETs nest the requested node under its full parent path. var resp struct { - AN struct { - Enable *bool `json:"enable"` - } `json:"ieee802-ethernet-interface:auto-negotiation"` + Interfaces struct { + Interface []struct { + 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 if err := h.RC.Get(r.Context(), base+"/auto-negotiation", &resp); err == nil { - if resp.AN.Enable != nil { - enable = *resp.AN.Enable + if ifs := resp.Interfaces.Interface; len(ifs) > 0 && ifs[0].Ethernet.AN.Enable != nil { + enable = *ifs[0].Ethernet.AN.Enable } } else if !restconf.IsNotFound(err) { log.Printf("configure interfaces %s reset advertised get: %v", name, err) diff --git a/src/webui/internal/handlers/containers.go b/src/webui/internal/handlers/containers.go index 14d55216e..e46cc980b 100644 --- a/src/webui/internal/handlers/containers.go +++ b/src/webui/internal/handlers/containers.go @@ -46,11 +46,6 @@ type containerListWrapper struct { } `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. type ContainerEntry struct { Name string @@ -106,13 +101,16 @@ func (h *ContainersHandler) Overview(w http.ResponseWriter, r *http.Request) { defer wg.Done() path := fmt.Sprintf("/data/infix-containers:containers/container=%s/resource-usage", 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 { log.Printf("restconf resource-usage %s: %v", name, err) return } + if len(w.Containers.Container) == 0 { + return + } mu.Lock() - usages[idx] = w.ResourceUsage + usages[idx] = w.Containers.Container[0].ResourceUsage mu.Unlock() }(i, c.Name) } diff --git a/src/webui/internal/handlers/firewall.go b/src/webui/internal/handlers/firewall.go index 9d2737ace..c4abc17c4 100644 --- a/src/webui/internal/handlers/firewall.go +++ b/src/webui/internal/handlers/firewall.go @@ -19,13 +19,32 @@ type firewallWrapper struct { } type firewallJSON struct { - Enabled *yangBool `json:"enabled"` // YANG default: true; nil means enabled - Default string `json:"default"` - Logging string `json:"logging"` - Lockdown yangBool `json:"lockdown"` - Zone []zoneJSON `json:"zone"` - Policy []policyJSON `json:"policy"` - Service []fwServiceJSON `json:"service"` + Enabled *yangBool `json:"enabled"` // YANG default: true; nil means enabled + Default string `json:"default"` + Logging string `json:"logging"` + Lockdown yangBool `json:"lockdown"` + Zone []zoneJSON `json:"zone"` + Policy []policyJSON `json:"policy"` + 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). @@ -50,6 +69,7 @@ type zoneJSON struct { Description string `json:"description"` Interface []string `json:"interface"` Network []string `json:"network"` + AddressSet []string `json:"address-set"` Service []string `json:"service"` PortForward []portForwardJSON `json:"port-forward"` Immutable bool `json:"immutable"` @@ -92,6 +112,7 @@ type firewallData struct { Matrix []matrixRow Zones []zoneEntry Policies []policyEntry + AddressSets []addressSetEntry Error string } @@ -110,11 +131,25 @@ type matrixCell struct { } type zoneEntry struct { - Name string - Action string - Interfaces string - Networks string - Services string // services allowed to HOST from this zone + Name string + Action string + Interfaces string + Networks string + 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 { @@ -169,14 +204,39 @@ func (h *FirewallHandler) Overview(w http.ResponseWriter, r *http.Request) { for _, z := range f.Zone { data.Zones = append(data.Zones, zoneEntry{ - Name: z.Name, - Action: z.Action, - Interfaces: strings.Join(z.Interface, ", "), - Networks: strings.Join(z.Network, ", "), - Services: strings.Join(z.Service, ", "), + Name: z.Name, + Action: z.Action, + Interfaces: strings.Join(z.Interface, ", "), + Networks: strings.Join(z.Network, ", "), + 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 { data.Policies = append(data.Policies, policyEntry{ Name: p.Name, diff --git a/src/webui/internal/handlers/vpn.go b/src/webui/internal/handlers/vpn.go index a00cac4e2..e0ecc24bf 100644 --- a/src/webui/internal/handlers/vpn.go +++ b/src/webui/internal/handlers/vpn.go @@ -21,8 +21,13 @@ type wgConfigJSON struct { } // wgIfaceConfigWrapper is used to fetch per-interface WireGuard config. +// Keyed GETs nest the requested node under its full parent path. 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. @@ -128,8 +133,9 @@ func buildWGTunnel(ctx context.Context, rc *restconf.Client, iface ifaceJSON) WG // Fetch ListenPort from config endpoint (separate from oper-state). var cfgWrap wgIfaceConfigWrapper 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 { - tunnel.ListenPort = cfgWrap.WireGuard.ListenPort + if err := rc.Get(ctx, path, &cfgWrap); err == nil && + 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. diff --git a/src/webui/internal/restconf/client.go b/src/webui/internal/restconf/client.go index 0f6470908..50cc14aac 100644 --- a/src/webui/internal/restconf/client.go +++ b/src/webui/internal/restconf/client.go @@ -9,8 +9,10 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" "net/url" + "reflect" "strings" "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. // 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 { resp, err := c.doRequest(ctx, http.MethodGet, path) if err != nil { @@ -88,7 +96,20 @@ func (c *Client) Get(ctx context.Context, path string, target any) error { if resp.StatusCode != http.StatusOK { 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. diff --git a/src/webui/internal/server/server.go b/src/webui/internal/server/server.go index c8f20ef32..b02bc8a5c 100644 --- a/src/webui/internal/server/server.go +++ b/src/webui/internal/server/server.go @@ -424,6 +424,9 @@ func New( mux.HandleFunc("POST /configure/firewall/services", cfgFw.AddService) mux.HandleFunc("POST /configure/firewall/services/{name}", cfgFw.SaveService) 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("POST /configure/hardware", cfgHw.CreateHardware) mux.HandleFunc("POST /configure/hardware/usb/{name}", cfgHw.SaveUSBPort) diff --git a/src/webui/templates/pages/configure-firewall.html b/src/webui/templates/pages/configure-firewall.html index 011798bb5..234cf73e3 100644 --- a/src/webui/templates/pages/configure-firewall.html +++ b/src/webui/templates/pages/configure-firewall.html @@ -112,6 +112,7 @@ Action{{template "field-info" (index $d "zone-action")}} Description{{template "field-info" (index $d "zone-description")}} Interfaces + Address sets{{template "field-info" (index $d "zone-address-set")}} @@ -128,6 +129,7 @@ {{if .Action}}{{.Action}}{{else}}reject{{end}} {{if .Description}}{{.Description}}{{else}}{{end}} {{.IfaceCount}} + {{if .AddressSet}}{{.AddrSetsTxt}}{{else}}{{end}} {{if not .Immutable}} + + + {{end}} @@ -731,4 +906,3 @@
Port Forwarding
{{end}}{{/* end {{else}} — schema ready */}} {{end}} - diff --git a/src/webui/templates/pages/firewall.html b/src/webui/templates/pages/firewall.html index 41701d20c..2ae2f87db 100644 --- a/src/webui/templates/pages/firewall.html +++ b/src/webui/templates/pages/firewall.html @@ -90,6 +90,7 @@ Action Interfaces Networks + Address Sets Host Services @@ -100,6 +101,7 @@ {{.Action}} {{.Interfaces}} {{if .Networks}}{{.Networks}}{{else}}{{end}} + {{if .AddressSets}}{{.AddressSets}}{{else}}{{end}} {{if .Services}}{{.Services}}{{else}}{{end}} {{end}} @@ -109,6 +111,40 @@ {{end}} + {{if .AddressSets}} +
+
Address Sets
+
+ + + + + + + + + + + {{range .AddressSets}} + + + + + + + {{end}} + +
NameFamilyTimeoutEntries
{{.Name}}{{.Family}}{{if .Timeout}}{{.Timeout}}{{else}}{{end}} + {{if .Entries}} + {{range $i, $e := .Entries}}{{if $i}}, {{end}}{{.Entry}}{{if .Dynamic}} dyn{{if .Expires}} {{.Expires}}{{end}}{{end}}{{end}} + {{else}} + (empty) + {{end}} +
+
+
+ {{end}} + {{if .Policies}}
Policies
diff --git a/test/case/cli_pretty/all.yaml b/test/case/cli_pretty/all.yaml index 64de1bf68..b56be0ee8 100644 --- a/test/case/cli_pretty/all.yaml +++ b/test/case/cli_pretty/all.yaml @@ -37,3 +37,6 @@ - "json/bloated.json" - "show-interfaces" - "-n br0" + +- case: firewall_overview.sh + name: "firewall-overview" diff --git a/test/case/cli_pretty/firewall_overview.sh b/test/case/cli_pretty/firewall_overview.sh new file mode 100755 index 000000000..ad12a345e --- /dev/null +++ b/test/case/cli_pretty/firewall_overview.sh @@ -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 diff --git a/test/case/cli_pretty/json/firewall-overview.json b/test/case/cli_pretty/json/firewall-overview.json new file mode 100644 index 000000000..6ce939217 --- /dev/null +++ b/test/case/cli_pretty/json/firewall-overview.json @@ -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"] + } + ] + } +} diff --git a/test/case/firewall/address-set/Readme.adoc b/test/case/firewall/address-set/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/firewall/address-set/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/firewall/address-set/test.adoc b/test/case/firewall/address-set/test.adoc new file mode 100644 index 000000000..472014d4b --- /dev/null +++ b/test/case/firewall/address-set/test.adoc @@ -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 + + diff --git a/test/case/firewall/address-set/test.py b/test/case/firewall/address-set/test.py new file mode 100755 index 000000000..7ed809d60 --- /dev/null +++ b/test/case/firewall/address-set/test.py @@ -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() diff --git a/test/case/firewall/address-set/topology.dot b/test/case/firewall/address-set/topology.dot new file mode 100644 index 000000000..84948360a --- /dev/null +++ b/test/case/firewall/address-set/topology.dot @@ -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 | data }", + pos="10,10!", + requires="controller" + ]; + + target [ + label="{ mgmt | data } | target", + pos="40,10!", + requires="infix", + ]; + + host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"] + host:data -- target:data [color=black] +} diff --git a/test/case/firewall/address-set/topology.svg b/test/case/firewall/address-set/topology.svg new file mode 100644 index 000000000..35a4d0caf --- /dev/null +++ b/test/case/firewall/address-set/topology.svg @@ -0,0 +1,42 @@ + + + + + + +1x2 + + + +host + +host + +mgmt + +data + + + +target + +mgmt + +data + +target + + + +host:mgmt--target:mgmt + + + + +host:data--target:data + + + + diff --git a/test/case/firewall/all.yaml b/test/case/firewall/all.yaml index 2778a31be..008c31c25 100644 --- a/test/case/firewall/all.yaml +++ b/test/case/firewall/all.yaml @@ -2,6 +2,9 @@ - name: Basic Firewall for End Devices case: basic/test.py +- name: Firewall Address-Sets with Dynamic Entries + case: address-set/test.py + - name: LAN-WAN Firewall with Masquerading case: lan-wan/test.py diff --git a/test/test.mk b/test/test.mk index 36683090e..ac83de782 100644 --- a/test/test.mk +++ b/test/test.mk @@ -58,7 +58,7 @@ SPEC_Q := endif 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)/' \ $(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) @@ -86,7 +86,7 @@ xpath-coverage-report: $(xpath_coverage_report_md) \ $(xpath_coverage_report_css) \ $(xpath_coverage_logo) \ - "$(subst ",,$(INFIX_NAME)) $(INFIX_VERSION)" \ + "$(subst ",,$(IX_NAME)) $(INFIX_VERSION)" \ $(xpath_coverage_report_pdf) # Unit tests run with random (-r) hostname and container name to diff --git a/utils/mirror-sync.sh b/utils/mirror-sync.sh new file mode 100755 index 000000000..5c4a4d5b7 --- /dev/null +++ b/utils/mirror-sync.sh @@ -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//), 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 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 diff --git a/utils/mkimage.sh b/utils/mkimage.sh index a78f9fcb0..5ef7153c5 100755 --- a/utils/mkimage.sh +++ b/utils/mkimage.sh @@ -33,7 +33,7 @@ Arguments: Description: 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 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 BR2_EXTERNAL_INFIX_PATH export RELEASE="${RELEASE:-""}" - export INFIX_ID="${INFIX_ID:-"infix"}" + export IX_ID="${IX_ID:-"infix"}" # Add host tools to PATH (for genimage, bmaptool, etc.) for dir in "$BOOT_DIR" "$ROOT_DIR"; do @@ -550,7 +550,7 @@ fi # Set defaults for optional variables : "${RELEASE:=""}" -: "${INFIX_ID:="infix"}" +: "${IX_ID:="infix"}" # Download bootloader if requested if [ -n "$DOWNLOAD_BOOT" ]; then @@ -626,12 +626,12 @@ fi # For boot-only mode, append "-boot" to the target name in the image filename if [ -n "$BOOT_ONLY" ]; then 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}\.img|-${TARGET}-boot.img|" > "$GENIMAGE_CFG" else sed "s|#VERSION#|${RELEASE}|" "$GENIMAGE_TEMPLATE" | \ - sed "s|#INFIX_ID#|${INFIX_ID}|" | \ + sed "s|#IX_ID#|${IX_ID}|" | \ sed "s|#TARGET#|${TARGET}|" > "$GENIMAGE_CFG" fi