diff --git a/README.md b/README.md index 03aaa26bc..b5298608f 100644 --- a/README.md +++ b/README.md @@ -85,12 +85,12 @@ interfaces { admin@infix-12-34-56:/config/interface/eth0/> leave admin@infix-12-34-56:/> show interfaces INTERFACE PROTOCOL STATE DATA -eth0 ethernet UP 52:54:00:12:34:56 - ipv4 192.168.2.200/24 (static) - ipv6 fe80::5054:ff:fe12:3456/64 (link-layer) lo ethernet UP 00:00:00:00:00:00 ipv4 127.0.0.1/8 (static) ipv6 ::1/128 (static) +eth0 ethernet UP 52:54:00:12:34:56 + ipv4 192.168.2.200/24 (static) + ipv6 fe80::5054:ff:fe12:3456/64 (link-layer) admin@infix-12-34-56:/> copy running startup @@ -114,6 +114,7 @@ containers for any custom functionality you need. - **x86_64** - Run in VMs or on mini PCs for development and testing - **Marvell CN9130 CRB, EspressoBIN** - High-performance ARM64 platforms - **Microchip SparX-5i** - Enterprise switching capabilities +- **Microchip SAMA7G54-EK** - ARM Cortex-A7 - **NXP i.MX8MP EVK** - Highly capable ARM64 SoC - **StarFive VisionFive2** - RISC-V architecture support diff --git a/board/aarch64/linux_defconfig b/board/aarch64/linux_defconfig index b37b59593..b7c4fe051 100644 --- a/board/aarch64/linux_defconfig +++ b/board/aarch64/linux_defconfig @@ -502,6 +502,7 @@ CONFIG_USB_OTG=y CONFIG_USB_XHCI_HCD=m CONFIG_USB_XHCI_MVEBU=m CONFIG_USB_EHCI_HCD=m +CONFIG_USB_EHCI_ROOT_HUB_TT=y CONFIG_USB_EHCI_HCD_PLATFORM=m CONFIG_USB_OHCI_HCD=m CONFIG_USB_OHCI_HCD_PLATFORM=m diff --git a/board/aarch64/raspberrypi-rpi64/Config.in b/board/aarch64/raspberrypi-rpi64/Config.in index cf2a63423..e2f3242b2 100644 --- a/board/aarch64/raspberrypi-rpi64/Config.in +++ b/board/aarch64/raspberrypi-rpi64/Config.in @@ -2,9 +2,6 @@ config BR2_PACKAGE_RASPBERRYPI_RPI64 bool "Raspberry Pi 64-bit (RPi3 and later)" depends on BR2_aarch64 select SDCARD_AUX - select BR2_PACKAGE_FEATURE_WIFI - select BR2_PACKAGE_BRCMFMAC_SDIO_FIRMWARE_RPI - select BR2_PACKAGE_BRCMFMAC_SDIO_FIRMWARE_RPI_WIFI select BR2_PACKAGE_LINUX_FIRMWARE_RTL_8169 help Raspberry Pi 64-bit adds support for the Raspberry Pi family of diff --git a/board/arm/README.md b/board/arm/README.md index 2385ba84c..6058d7ee3 100644 --- a/board/arm/README.md +++ b/board/arm/README.md @@ -4,4 +4,5 @@ Arm 32-bit Board Specific Documentation ---------------------------- +- [Microchip SAMA7G54-EK (32-bit)](microchip-sama7g54-ek/) - [Raspberry Pi 2 Model B (32-bit)](raspberrypi-rpi2/) diff --git a/board/arm/linux_defconfig b/board/arm/linux_defconfig index c54ed16fe..3c0639ad7 100644 --- a/board/arm/linux_defconfig +++ b/board/arm/linux_defconfig @@ -1,20 +1,19 @@ CONFIG_SYSVIPC=y CONFIG_POSIX_MQUEUE=y -CONFIG_AUDIT=y CONFIG_NO_HZ_IDLE=y CONFIG_HIGH_RES_TIMERS=y CONFIG_BPF_SYSCALL=y CONFIG_BPF_JIT=y -CONFIG_PREEMPT=y CONFIG_BSD_PROCESS_ACCT=y CONFIG_BSD_PROCESS_ACCT_V3=y CONFIG_TASKSTATS=y CONFIG_TASK_DELAY_ACCT=y CONFIG_TASK_XACCT=y CONFIG_TASK_IO_ACCOUNTING=y +# CONFIG_CPU_ISOLATION is not set CONFIG_IKCONFIG=y CONFIG_IKCONFIG_PROC=y -CONFIG_LOG_BUF_SHIFT=18 +CONFIG_LOG_BUF_SHIFT=16 CONFIG_MEMCG=y CONFIG_BLK_CGROUP=y CONFIG_CFS_BANDWIDTH=y @@ -31,19 +30,15 @@ CONFIG_SCHED_AUTOGROUP=y CONFIG_BLK_DEV_INITRD=y CONFIG_KALLSYMS_ALL=y CONFIG_PROFILING=y -CONFIG_ARCH_MULTI_V6=y -CONFIG_ARCH_VIRT=y -CONFIG_ARCH_BCM=y -CONFIG_ARCH_BCM2835=y +# CONFIG_ARM_ERRATA_643719 is not set CONFIG_SMP=y CONFIG_CPU_FREQ=y -CONFIG_CPU_FREQ_STAT=y -CONFIG_CPU_FREQ_DEFAULT_GOV_CONSERVATIVE=y +CONFIG_CPU_FREQ_DEFAULT_GOV_PERFORMANCE=y CONFIG_CPU_FREQ_GOV_POWERSAVE=y CONFIG_CPU_FREQ_GOV_USERSPACE=y CONFIG_CPU_FREQ_GOV_ONDEMAND=y +CONFIG_CPU_FREQ_GOV_CONSERVATIVE=y CONFIG_CPUFREQ_DT=y -CONFIG_ARM_RASPBERRYPI_CPUFREQ=y CONFIG_VFP=y CONFIG_NEON=y CONFIG_KERNEL_MODE_NEON=y @@ -55,7 +50,11 @@ CONFIG_MODULES=y CONFIG_MODULE_UNLOAD=y CONFIG_PARTITION_ADVANCED=y # CONFIG_CORE_DUMP_DEFAULT_ELF_HEADERS is not set +CONFIG_ZSWAP=y +CONFIG_ZSWAP_DEFAULT_ON=y +CONFIG_ZSWAP_SHRINKER_DEFAULT_ON=y # CONFIG_COMPAT_BRK is not set +# CONFIG_BALLOON_COMPACTION is not set CONFIG_KSM=y CONFIG_CMA=y CONFIG_NET=y @@ -259,7 +258,6 @@ CONFIG_UEVENT_HELPER_PATH="/sbin/hotplug" CONFIG_DEVTMPFS=y CONFIG_DEVTMPFS_MOUNT=y # CONFIG_STANDALONE is not set -CONFIG_RASPBERRYPI_FIRMWARE=y CONFIG_FW_CFG_SYSFS=y CONFIG_FW_CFG_SYSFS_CMDLINE=y CONFIG_OF_OVERLAY=y @@ -301,63 +299,29 @@ CONFIG_VETH=m CONFIG_VIRTIO_NET=y CONFIG_NLMON=y CONFIG_NET_VRF=y -CONFIG_BCMGENET=y -CONFIG_SMSC911X=y -CONFIG_USB_LAN78XX=y CONFIG_USB_USBNET=y -CONFIG_USB_NET_SMSC95XX=y -CONFIG_INPUT_MOUSEDEV=m CONFIG_INPUT_EVDEV=y -CONFIG_INPUT_TOUCHSCREEN=y -CONFIG_TOUCHSCREEN_EDT_FT5X06=m # CONFIG_LEGACY_PTYS is not set -CONFIG_SERIAL_8250=y -CONFIG_SERIAL_8250_CONSOLE=y -CONFIG_SERIAL_8250_EXTENDED=y -CONFIG_SERIAL_8250_SHARE_IRQ=y -CONFIG_SERIAL_8250_BCM2835AUX=y -CONFIG_SERIAL_AMBA_PL011=y -CONFIG_SERIAL_AMBA_PL011_CONSOLE=y CONFIG_SERIAL_DEV_BUS=y CONFIG_VIRTIO_CONSOLE=y CONFIG_I2C_CHARDEV=y -CONFIG_I2C_BCM2835=m CONFIG_SPI=y -CONFIG_SPI_BCM2835=y -CONFIG_SPI_BCM2835AUX=y -CONFIG_SENSORS_RASPBERRYPI_HWMON=m CONFIG_THERMAL=y -CONFIG_BCM2711_THERMAL=y -CONFIG_BCM2835_THERMAL=m CONFIG_WATCHDOG=y CONFIG_I6300ESB_WDT=y -CONFIG_BCM2835_WDT=y CONFIG_MFD_SYSCON=y CONFIG_REGULATOR=y CONFIG_REGULATOR_FIXED_VOLTAGE=y -CONFIG_REGULATOR_GPIO=y CONFIG_MEDIA_SUPPORT=y -CONFIG_DRM=y -CONFIG_DRM_LOAD_EDID_FIRMWARE=y -CONFIG_DRM_SIMPLEDRM=y -CONFIG_DRM_PANEL_SIMPLE=m -CONFIG_DRM_TOSHIBA_TC358762=m -CONFIG_DRM_V3D=m -CONFIG_DRM_VC4=m -CONFIG_DRM_VC4_HDMI_CEC=y -CONFIG_FB=y -CONFIG_BACKLIGHT_CLASS_DEVICE=y -CONFIG_SOUND=y -CONFIG_SND=y -CONFIG_SND_SOC=y -CONFIG_SND_BCM2835_SOC_I2S=y -CONFIG_HID_GENERIC=m CONFIG_USB=y CONFIG_USB_ANNOUNCE_NEW_DEVICES=y CONFIG_USB_OTG=y +CONFIG_USB_EHCI_HCD=m +CONFIG_USB_EHCI_ROOT_HUB_TT=y +CONFIG_USB_EHCI_HCD_PLATFORM=m +CONFIG_USB_OHCI_HCD=m +CONFIG_USB_OHCI_HCD_PLATFORM=m CONFIG_USB_STORAGE=y -CONFIG_USB_DWC2=y -CONFIG_NOP_USB_XCEIV=y CONFIG_USB_GADGET=y CONFIG_USB_ETH=m CONFIG_USB_ETH_EEM=y @@ -365,8 +329,6 @@ CONFIG_USB_G_SERIAL=m CONFIG_MMC=y CONFIG_MMC_SDHCI=y CONFIG_MMC_SDHCI_PLTFM=y -CONFIG_MMC_SDHCI_IPROC=y -CONFIG_MMC_BCM2835=y CONFIG_NEW_LEDS=y CONFIG_LEDS_CLASS=y CONFIG_LEDS_GPIO=y @@ -380,20 +342,13 @@ CONFIG_LEDS_TRIGGER_DEFAULT_ON=y CONFIG_LEDS_TRIGGER_TRANSIENT=y CONFIG_LEDS_TRIGGER_CAMERA=y CONFIG_DMADEVICES=y -CONFIG_DMA_BCM2835=y CONFIG_VIRTIO_PCI=y CONFIG_VIRTIO_BALLOON=y CONFIG_VIRTIO_INPUT=y CONFIG_VIRTIO_MMIO=y -CONFIG_STAGING=y -CONFIG_SND_BCM2835=m -CONFIG_CLK_RASPBERRYPI=y CONFIG_MAILBOX=y -CONFIG_BCM2835_MBOX=y # CONFIG_IOMMU_SUPPORT is not set -CONFIG_RASPBERRYPI_POWER=y CONFIG_PWM=y -CONFIG_PWM_BCM2835=y CONFIG_EXT2_FS=y CONFIG_EXT2_FS_POSIX_ACL=y CONFIG_EXT4_FS=y @@ -426,18 +381,13 @@ CONFIG_DMA_CMA=y CONFIG_CMA_SIZE_MBYTES=32 CONFIG_PRINTK_TIME=y CONFIG_DEBUG_KERNEL=y -CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT=y CONFIG_MAGIC_SYSRQ=y CONFIG_DEBUG_FS=y CONFIG_PANIC_ON_OOPS=y CONFIG_PANIC_TIMEOUT=20 -CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=y CONFIG_HARDLOCKUP_DETECTOR=y CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y -CONFIG_BOOTPARAM_HUNG_TASK_PANIC=y CONFIG_WQ_WATCHDOG=y -CONFIG_WQ_CPU_INTENSIVE_REPORT=y CONFIG_TEST_LOCKUP=m -# CONFIG_RCU_TRACE is not set CONFIG_FUNCTION_TRACER=y -CONFIG_MEMTEST=y +# CONFIG_RUNTIME_TESTING_MENU is not set diff --git a/board/arm/microchip-sama7g54-ek/dts/microchip/at91-sama7g5ek.dts b/board/arm/microchip-sama7g54-ek/dts/microchip/at91-sama7g5ek.dts index 503d70045..ebba2075a 100644 --- a/board/arm/microchip-sama7g54-ek/dts/microchip/at91-sama7g5ek.dts +++ b/board/arm/microchip-sama7g54-ek/dts/microchip/at91-sama7g5ek.dts @@ -16,3 +16,28 @@ &thermal_sensor { status = "disabled"; }; + +/* + * Enable SDMMC high speed mode, please note that neither 'mmc-ddr-1_8v' + * or 'mmc-hs200-1_8v' work, even though [1] states they are supported, + * the errata [2] tells a different story: "Using mode SDR104, HS200 or + * HS400 may lead to tuning issues, data read errors or clock switching + * failures." — empirical testing has proved this on a rev 5 board. + * + * [1]: https://github.com/linux4sam/linux-at91/commit/5b35500 + * [2]: https://ww1.microchip.com/downloads/en/DeviceDoc/SAMA7G5-Series-Silicon-Errata-and-Data-Sheet-Clarification-DS80001016A.pdf + */ +&sdmmc0 { + /delete-property/ sdhci-caps-mask; + cap-mmc-highspeed; + cap-mmc-hw-reset; +}; + +&sdmmc1 { + /delete-property/ no-1-8-v; + /delete-property/ sdhci-caps-mask; +}; + +&sdmmc2 { + /delete-property/ sdhci-caps-mask; +}; diff --git a/board/arm/microchip-sama7g54-ek/rootfs/etc/rauc/system.conf b/board/arm/microchip-sama7g54-ek/rootfs/etc/rauc/system.conf deleted file mode 100644 index dbdb8e70e..000000000 --- a/board/arm/microchip-sama7g54-ek/rootfs/etc/rauc/system.conf +++ /dev/null @@ -1,12 +0,0 @@ -[system] -compatible=infix-sama7g54-ek -bootloader=uboot -statusfile=/mnt/aux/rauc.status - -[slot.rootfs.0] -device=/dev/disk/by-partlabel/primary -bootname=primary - -[slot.rootfs.1] -device=/dev/disk/by-partlabel/secondary -bootname=secondary diff --git a/board/arm/microchip-sama7g54-ek/uboot/extras.config b/board/arm/microchip-sama7g54-ek/uboot/extras.config index 6d3cf0fc7..556649dcb 100644 --- a/board/arm/microchip-sama7g54-ek/uboot/extras.config +++ b/board/arm/microchip-sama7g54-ek/uboot/extras.config @@ -1,6 +1,8 @@ # Common U-Boot extras for SAMA7G54-EK CONFIG_EFI_PARTITION=y CONFIG_ENV_IMPORT_FDT=y +# CONFIG_ENV_IS_IN_FAT is not set +CONFIG_ENV_IS_NOWHERE=y CONFIG_FIT=y CONFIG_FIT_SIGNATURE=y CONFIG_RSA=y diff --git a/board/arm/raspberrypi-rpi2/Config.in b/board/arm/raspberrypi-rpi2/Config.in index 83daa3fb1..2a86b447c 100644 --- a/board/arm/raspberrypi-rpi2/Config.in +++ b/board/arm/raspberrypi-rpi2/Config.in @@ -2,9 +2,6 @@ config BR2_PACKAGE_RASPBERRYPI_RPI2 bool "Raspberry Pi 2 Model B (32-bit ARMv7)" depends on BR2_arm select SDCARD_AUX - select BR2_PACKAGE_FEATURE_WIFI - select BR2_PACKAGE_BRCMFMAC_SDIO_FIRMWARE_RPI - select BR2_PACKAGE_BRCMFMAC_SDIO_FIRMWARE_RPI_WIFI help Support for the 32-bit ARMv7 Raspberry Pi 2B single-board computer (SBC) with BCM2836 quad-core Cortex-A7 processor. diff --git a/board/arm/raspberrypi-rpi2/raspberrypi-rpi2.mk b/board/arm/raspberrypi-rpi2/raspberrypi-rpi2.mk index 73d4a0b63..a904a1385 100644 --- a/board/arm/raspberrypi-rpi2/raspberrypi-rpi2.mk +++ b/board/arm/raspberrypi-rpi2/raspberrypi-rpi2.mk @@ -1,13 +1,19 @@ # Raspberry Pi 2 Model B specific kernel configuration define RASPBERRYPI_RPI2_LINUX_CONFIG_FIXUPS + $(call KCONFIG_ENABLE_OPT,CONFIG_ARCH_BCM) + $(call KCONFIG_ENABLE_OPT,CONFIG_ARCH_BCM2835) + $(call KCONFIG_ENABLE_OPT,CONFIG_SOUND) $(call KCONFIG_ENABLE_OPT,CONFIG_SND) $(call KCONFIG_ENABLE_OPT,CONFIG_SND_SOC) + $(call KCONFIG_SET_OPT,CONFIG_SND_BCM2835_SOC_I2S,y) + $(call KCONFIG_SET_OPT,CONFIG_SND_BCM2835,m) $(call KCONFIG_ENABLE_OPT,CONFIG_INPUT_MOUSE) $(call KCONFIG_ENABLE_OPT,CONFIG_INPUT_KEYBOARD) $(call KCONFIG_ENABLE_OPT,CONFIG_INPUT_TOUCHSCREEN) $(call KCONFIG_SET_OPT,CONFIG_INPUT_MOUSEDEV,m) $(call KCONFIG_SET_OPT,CONFIG_HID_GENERIC,m) + $(call KCONFIG_SET_OPT,CONFIG_TOUCHSCREEN_EDT_FT5X06,m) $(call KCONFIG_ENABLE_OPT,CONFIG_ARCH_BCM) $(call KCONFIG_ENABLE_OPT,CONFIG_ARCH_BCM2835) @@ -15,46 +21,58 @@ define RASPBERRYPI_RPI2_LINUX_CONFIG_FIXUPS $(call KCONFIG_ENABLE_OPT,CONFIG_BCM2835_WDT) $(call KCONFIG_ENABLE_OPT,CONFIG_DMA_BCM2835) $(call KCONFIG_ENABLE_OPT,CONFIG_RASPBERRYPI_FIRMWARE) + $(call KCONFIG_ENABLE_OPT,CONFIG_RASPBERRYPI_POWER) + $(call KCONFIG_ENABLE_OPT,CONFIG_ARM_RASPBERRYPI_CPUFREQ) + $(call KCONFIG_ENABLE_OPT,CONFIG_CLK_RASPBERRYPI) $(call KCONFIG_ENABLE_OPT,CONFIG_PINCTRL_BCM2835) $(call KCONFIG_ENABLE_OPT,CONFIG_GPIO_BCM2835) + $(call KCONFIG_ENABLE_OPT,CONFIG_PWM_BCM2835) $(call KCONFIG_SET_OPT,CONFIG_BRCMFMAC,m) $(call KCONFIG_ENABLE_OPT,CONFIG_BRCMFMAC_SDIO) $(call KCONFIG_SET_OPT,CONFIG_I2C_BCM2835,m) + $(call KCONFIG_SET_OPT,CONFIG_SPI_BCM2835,y) + $(call KCONFIG_SET_OPT,CONFIG_SPI_BCM2835AUX,y) + $(call KCONFIG_SET_OPT,CONFIG_SENSORS_RASPBERRYPI_HWMON,m) + $(call KCONFIG_SET_OPT,CONFIG_BCM2711_THERMAL,y) $(call KCONFIG_SET_OPT,CONFIG_BCM2835_THERMAL,m) $(call KCONFIG_ENABLE_OPT,CONFIG_MMC_BCM2835) - $(call KCONFIG_ENABLE_OPT,CONFIG_RASPBERRYPI_POWER) + $(call KCONFIG_SET_OPT,CONFIG_MMC_SDHCI_IPROC,y) $(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_8250) $(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_8250_CONSOLE) $(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_8250_BCM2835AUX) $(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_8250_EXTENDED) $(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_8250_SHARE_IRQ) + $(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_AMBA_PL011) + $(call KCONFIG_ENABLE_OPT,CONFIG_SERIAL_AMBA_PL011_CONSOLE) $(call KCONFIG_ENABLE_OPT,CONFIG_NET_VENDOR_BROADCOM) + $(call KCONFIG_SET_OPT,CONFIG_BCMGENET,y) $(call KCONFIG_SET_OPT,CONFIG_SMSC911X,y) + $(call KCONFIG_ENABLE_OPT,CONFIG_USB_USBNET) + $(call KCONFIG_SET_OPT,CONFIG_USB_LAN78XX,y) + $(call KCONFIG_SET_OPT,CONFIG_USB_NET_SMSC95XX,y) $(call KCONFIG_SET_OPT,CONFIG_REGULATOR_GPIO,y) $(call KCONFIG_ENABLE_OPT,CONFIG_COMMON_CLK_BCM2835) - $(call KCONFIG_ENABLE_OPT,CONFIG_CLK_RASPBERRYPI) $(call KCONFIG_ENABLE_OPT,CONFIG_DRM) $(call KCONFIG_ENABLE_OPT,CONFIG_DRM_KMS_HELPER) + $(call KCONFIG_SET_OPT,CONFIG_DRM_SIMPLEDRM,y) $(call KCONFIG_SET_OPT,CONFIG_DRM_V3D,m) $(call KCONFIG_SET_OPT,CONFIG_DRM_VC4,m) - $(call KCONFIG_SET_OPT,CONFIG_STAGING,y) - $(call KCONFIG_SET_OPT,CONFIG_SND_BCM2835,m) - $(call KCONFIG_ENABLE_OPT,CONFIG_DRM_VC4_HDMI_CEC) $(call KCONFIG_ENABLE_OPT,CONFIG_DRM_LOAD_EDID_FIRMWARE) $(call KCONFIG_ENABLE_OPT,CONFIG_DRM_PANEL_BRIDGE) $(call KCONFIG_ENABLE_OPT,CONFIG_DRM_BRIDGE) $(call KCONFIG_SET_OPT,CONFIG_DRM_TOSHIBA_TC358762,m) $(call KCONFIG_SET_OPT,CONFIG_DRM_PANEL_SIMPLE,m) + $(call KCONFIG_SET_OPT,CONFIG_STAGING,y) $(call KCONFIG_ENABLE_OPT,CONFIG_FB) $(call KCONFIG_ENABLE_OPT,CONFIG_FRAMEBUFFER_CONSOLE) $(call KCONFIG_ENABLE_OPT,CONFIG_FRAMEBUFFER_CONSOLE_DETECT_PRIMARY) $(call KCONFIG_ENABLE_OPT,CONFIG_DRM_FBDEV_EMULATION) - - $(call KCONFIG_SET_OPT,CONFIG_TOUCHSCREEN_EDT_FT5X06,m) - $(call KCONFIG_ENABLE_OPT,CONFIG_BACKLIGHT_CLASS_DEVICE) + + $(call KCONFIG_ENABLE_OPT,CONFIG_USB_DWC2) + $(call KCONFIG_ENABLE_OPT,CONFIG_NOP_USB_XCEIV) endef $(eval $(ix-board)) diff --git a/board/arm/rootfs/etc/rauc/system.conf b/board/arm/rootfs/etc/rauc/system.conf new file mode 100644 index 000000000..6b9b51b6f --- /dev/null +++ b/board/arm/rootfs/etc/rauc/system.conf @@ -0,0 +1,28 @@ +[system] +compatible=infix-arm +bootloader=uboot +statusfile=/mnt/aux/rauc.status +mountprefix=/var/lib/rauc/mnt +bundle-formats=-plain +max-bundle-download-size=1073741824 + +[log.event-log] +filename=/var/log/upgrade-json.log +format=json-pretty +max-size=1M +max-files=5 + +[keyring] +directory=/etc/rauc/keys + +[slot.rootfs.0] +device=/dev/disk/by-partlabel/primary +bootname=primary + +[slot.rootfs.1] +device=/dev/disk/by-partlabel/secondary +bootname=secondary + +[slot.net.0] +device=/dev/ram0 +bootname=net diff --git a/board/common/busybox_defconfig b/board/common/busybox_defconfig index 1d10811be..739bbcf08 100644 --- a/board/common/busybox_defconfig +++ b/board/common/busybox_defconfig @@ -1,7 +1,7 @@ # # Automatically generated make config: don't edit -# Busybox version: 1.36.1 -# Sun Feb 9 12:25:37 2025 +# Busybox version: 1.37.0 +# Mon Feb 9 07:51:08 2026 # CONFIG_HAVE_DOT_CONFIG=y @@ -17,6 +17,7 @@ CONFIG_SHOW_USAGE=y CONFIG_FEATURE_VERBOSE_USAGE=y # CONFIG_FEATURE_COMPRESS_USAGE is not set CONFIG_LFS=y +CONFIG_TIME64=y CONFIG_PAM=y CONFIG_FEATURE_DEVPTS=y CONFIG_FEATURE_UTMP=y @@ -469,6 +470,7 @@ CONFIG_FEATURE_FIND_NEWER=y CONFIG_FEATURE_FIND_SAMEFILE=y CONFIG_FEATURE_FIND_EXEC=y CONFIG_FEATURE_FIND_EXEC_PLUS=y +CONFIG_FEATURE_FIND_EXEC_OK=y CONFIG_FEATURE_FIND_USER=y CONFIG_FEATURE_FIND_GROUP=y CONFIG_FEATURE_FIND_NOT=y @@ -795,6 +797,7 @@ CONFIG_FLASH_ERASEALL=y CONFIG_FLASH_LOCK=y CONFIG_FLASH_UNLOCK=y CONFIG_FLASHCP=y +CONFIG_GETFATTR=y CONFIG_HDPARM=y CONFIG_FEATURE_HDPARM_GET_IDENTITY=y # CONFIG_FEATURE_HDPARM_HDIO_SCAN_HWIF is not set @@ -933,6 +936,7 @@ CONFIG_IPRULE=y CONFIG_IPNEIGH=y CONFIG_FEATURE_IP_ADDRESS=y CONFIG_FEATURE_IP_LINK=y +CONFIG_FEATURE_IP_LINK_CAN=y CONFIG_FEATURE_IP_ROUTE=y CONFIG_FEATURE_IP_ROUTE_DIR="/etc/iproute2" CONFIG_FEATURE_IP_TUNNEL=y @@ -1007,6 +1011,7 @@ CONFIG_FEATURE_WGET_HTTPS=y CONFIG_WHOIS=y CONFIG_ZCIP=y CONFIG_UDHCPD=y +CONFIG_FEATURE_UDHCPD_BOOTP=y CONFIG_FEATURE_UDHCPD_BASE_IP_ON_MAC=y CONFIG_FEATURE_UDHCPD_WRITE_LEASES_EARLY=y CONFIG_DHCPD_LEASES_FILE="/var/lib/misc/udhcpd.leases" @@ -1016,7 +1021,12 @@ CONFIG_UDHCPC=y CONFIG_FEATURE_UDHCPC_ARPING=y CONFIG_FEATURE_UDHCPC_SANITIZEOPT=y CONFIG_UDHCPC_DEFAULT_SCRIPT="/usr/share/udhcpc/default.script" +CONFIG_UDHCPC6_DEFAULT_SCRIPT="" # CONFIG_UDHCPC6 is not set +# CONFIG_FEATURE_UDHCPC6_RFC3646 is not set +# CONFIG_FEATURE_UDHCPC6_RFC4704 is not set +# CONFIG_FEATURE_UDHCPC6_RFC4833 is not set +# CONFIG_FEATURE_UDHCPC6_RFC5970 is not set # # Common options for DHCP applets @@ -1027,7 +1037,7 @@ CONFIG_UDHCP_DEBUG=0 CONFIG_UDHCPC_SLACK_FOR_BUGGY_SERVERS=80 CONFIG_FEATURE_UDHCP_RFC3397=y CONFIG_FEATURE_UDHCP_8021Q=y -CONFIG_IFUPDOWN_UDHCPC_CMD_OPTIONS="-b -R -n -O search" +CONFIG_IFUPDOWN_UDHCPC_CMD_OPTIONS="" # # Print Utilities @@ -1132,7 +1142,7 @@ CONFIG_SH_IS_ASH=y CONFIG_BASH_IS_NONE=y CONFIG_SHELL_ASH=y CONFIG_ASH=y -CONFIG_ASH_OPTIMIZE_FOR_SIZE=y +# CONFIG_ASH_OPTIMIZE_FOR_SIZE is not set CONFIG_ASH_INTERNAL_GLOB=y CONFIG_ASH_BASH_COMPAT=y # CONFIG_ASH_BASH_SOURCE_CURDIR is not set @@ -1146,7 +1156,6 @@ CONFIG_ASH_IDLE_TIMEOUT=y CONFIG_ASH_ECHO=y CONFIG_ASH_PRINTF=y CONFIG_ASH_TEST=y -CONFIG_ASH_SLEEP=y CONFIG_ASH_HELP=y CONFIG_ASH_GETOPTS=y CONFIG_ASH_CMDCMD=y @@ -1197,7 +1206,7 @@ CONFIG_FEATURE_SH_MATH_64=y CONFIG_FEATURE_SH_MATH_BASE=y CONFIG_FEATURE_SH_EXTRA_QUIET=y # CONFIG_FEATURE_SH_STANDALONE is not set -# CONFIG_FEATURE_SH_NOFORK is not set +CONFIG_FEATURE_SH_NOFORK=y CONFIG_FEATURE_SH_READ_FRAC=y # CONFIG_FEATURE_SH_HISTFILESIZE is not set CONFIG_FEATURE_SH_EMBEDDED_SCRIPTS=y diff --git a/board/common/post-build.sh b/board/common/post-build.sh index d707991f9..2f9e665ec 100755 --- a/board/common/post-build.sh +++ b/board/common/post-build.sh @@ -44,6 +44,10 @@ if [ -n "${ID_LIKE}" ]; then ID="${ID} ${ID_LIKE}" fi +# Initialize default hostname for hostname.d pattern +mkdir -p "$TARGET_DIR/etc/hostname.d" +cp "$TARGET_DIR/etc/hostname" "$TARGET_DIR/etc/hostname.d/10-default" + # This is a symlink to /usr/lib/os-release, so we remove this to keep # original Buildroot information. ixmsg "Creating /etc/os-release" diff --git a/board/common/rootfs/etc/finit.d/10-infix.conf b/board/common/rootfs/etc/finit.d/10-infix.conf index d2aa8fce5..fc65d79fb 100644 --- a/board/common/rootfs/etc/finit.d/10-infix.conf +++ b/board/common/rootfs/etc/finit.d/10-infix.conf @@ -1,3 +1,3 @@ -task name:ixinit log:tag:ixinit [S] \ +task name:ixinit [S] \ /usr/libexec/finit/runparts -bp /usr/libexec/infix/init.d \ -- Probing system diff --git a/board/common/rootfs/etc/finit.d/available/mkcert.conf b/board/common/rootfs/etc/finit.d/available/mkcert.conf deleted file mode 100644 index 125e831e0..000000000 --- a/board/common/rootfs/etc/finit.d/available/mkcert.conf +++ /dev/null @@ -1 +0,0 @@ -task [S] /usr/libexec/infix/mkcert -- Verifying self-signed https certificate diff --git a/board/common/rootfs/etc/finit.d/available/netconf.conf b/board/common/rootfs/etc/finit.d/available/netconf.conf index 656c67422..a01ffb642 100644 --- a/board/common/rootfs/etc/finit.d/available/netconf.conf +++ b/board/common/rootfs/etc/finit.d/available/netconf.conf @@ -1,3 +1,3 @@ -service name:netopeer notify:none log env:/etc/default/confd \ - [12345] netopeer2-server -F -t $CONFD_TIMEOUT -v 1 \ +service name:netopeer notify:none log:null env:/etc/default/confd \ + [12345] netopeer2-server -F -t $CONFD_TIMEOUT -v 1 \ -- NETCONF server diff --git a/board/common/rootfs/etc/finit.d/available/restconf.conf b/board/common/rootfs/etc/finit.d/available/restconf.conf index 993fc0774..bfe5a0500 100644 --- a/board/common/rootfs/etc/finit.d/available/restconf.conf +++ b/board/common/rootfs/etc/finit.d/available/restconf.conf @@ -1,3 +1,3 @@ -service name:rousette notify:none log:null env:/etc/default/confd \ - [12345] rousette --syslog -t $CONFD_TIMEOUT \ +service notify:none log:null env:/etc/default/confd \ + [12345] rousette --syslog -t $CONFD_TIMEOUT \ -- RESTCONF server diff --git a/board/common/rootfs/etc/finit.d/dbus.conf b/board/common/rootfs/etc/finit.d/dbus.conf index 790c757a8..43b4eb67c 100644 --- a/board/common/rootfs/etc/finit.d/dbus.conf +++ b/board/common/rootfs/etc/finit.d/dbus.conf @@ -1,5 +1,5 @@ # Override Finit plugin service cgroup.system name:dbus notify:none pid:!/run/messagebus.pid \ - [S123456789] /usr/bin/dbus-daemon --nofork --system --syslog-only \ + [S123456789] /usr/bin/dbus-daemon --nofork --system --syslog-only \ -- D-Bus message bus daemon diff --git a/board/common/rootfs/etc/finit.d/enabled/mkcert.conf b/board/common/rootfs/etc/finit.d/enabled/mkcert.conf deleted file mode 120000 index 166f9f03f..000000000 --- a/board/common/rootfs/etc/finit.d/enabled/mkcert.conf +++ /dev/null @@ -1 +0,0 @@ -../available/mkcert.conf \ No newline at end of file diff --git a/board/common/rootfs/usr/libexec/infix/hostname b/board/common/rootfs/usr/libexec/infix/hostname index bfd1952b3..cefc86b5e 100755 --- a/board/common/rootfs/usr/libexec/infix/hostname +++ b/board/common/rootfs/usr/libexec/infix/hostname @@ -10,49 +10,43 @@ HOSTNAME_D="/etc/hostname.d" -# Ensure directory exists -mkdir -p "$HOSTNAME_D" - -# Find the highest priority file (reverse sort, take first) -hostname_file=$(ls -1 "$HOSTNAME_D" 2>/dev/null | sort -r | head -1) - -if [ -z "$hostname_file" ]; then +# Find highest priority file (reverse lexicographic) +set -- "$HOSTNAME_D"/* +if [ ! -e "$1" ]; then logger -it confd "No hostname sources found in $HOSTNAME_D" exit 1 fi -# Read hostname from the file (first line only, strip whitespace) -new_hostname=$(cat "$HOSTNAME_D/$hostname_file" | head -1 | tr -d '\n\r\t ') -if [ -z "$new_hostname" ]; then - logger -it confd "Empty hostname in $hostname_file" +file=$(printf '%s\n' "$@" | sort -r | head -n1) +IFS= read -r new < "$file" +new=$(printf '%s' "$new" | tr -d '\r\n\t ') +if [ -z "$new" ]; then + logger -it confd "Empty hostname in $file" exit 1 fi -if [ ${#new_hostname} -gt 64 ]; then - logger -it confd "Hostname too long (${#new_hostname} > 64) in $hostname_file" +if [ ${#new} -gt 64 ]; then + logger -it confd "Hostname too long (${#new} > 64) in $file" exit 1 fi # Check if hostname has actually changed -current_hostname=$(hostname) -if [ "$new_hostname" = "$current_hostname" ]; then +if [ "$new" = "$(hostname)" ]; then # No change needed, exit silently exit 0 fi # Set the hostname -logger -it confd "Setting hostname to '$new_hostname' from $hostname_file" -hostname "$new_hostname" - -# Update /etc/hostname (for persistence across reboots) -echo "$new_hostname" > /etc/hostname +if hostname "$new"; then + echo "$new" > /etc/hostname +fi # Update /etc/hosts (127.0.1.1 entry for proper name resolution) if grep -q "^127\.0\.1\.1" /etc/hosts; then - sed -i -E "s/^(127\.0\.1\.1\s+).*/\1$new_hostname/" /etc/hosts + sed -i -E "s/^(127\.0\.1\.1\s+).*/\1$new/" /etc/hosts else # Add entry if it doesn't exist - echo "127.0.1.1 $new_hostname" >> /etc/hosts + echo "127.0.1.1 $new" >> /etc/hosts fi # Notify services of hostname change, skip while in bootstrap @@ -61,8 +55,8 @@ if ! runlevel >/dev/null 2>&1; then exit 0 fi -initctl -bq status lldpd && lldpcli configure system hostname "$new_hostname" 2>/dev/null -initctl -bq status mdns && avahi-set-host-name "$new_hostname" 2>/dev/null +initctl -bq status lldpd && lldpcli configure system hostname "$new" 2>/dev/null +initctl -bq status mdns && avahi-set-host-name "$new" 2>/dev/null initctl -bq touch netbrowse 2>/dev/null # If called from dhcp script we need to reload to activate new name in syslogd diff --git a/board/common/rootfs/usr/libexec/infix/init.d/00-probe b/board/common/rootfs/usr/libexec/infix/init.d/00-probe index d70671276..10f2ef53a 100755 --- a/board/common/rootfs/usr/libexec/infix/init.d/00-probe +++ b/board/common/rootfs/usr/libexec/infix/init.d/00-probe @@ -519,6 +519,48 @@ def probe_dtsystem(out): return 0 +def probe_wifi_radios(out): + """Probe wifi radios via sysfs/iw and store in output dict.""" + ieee80211 = "/sys/class/ieee80211" + if not os.path.exists(ieee80211): + return + + radios = sorted(os.listdir(ieee80211)) + if not radios: + return + + out["wifi-radios"] = [] + for phy in radios: + info = {"name": phy, "bands": []} + try: + result = subprocess.run( + ["iw", "phy", phy, "info"], + capture_output=True, text=True, timeout=5 + ) + if result.returncode != 0: + continue + except Exception: + continue + + freqs = [] + for line in result.stdout.splitlines(): + stripped = line.strip() + if stripped.startswith("* ") and "MHz" in stripped: + try: + freqs.append(int(float(stripped.split()[1]))) + except (ValueError, IndexError): + pass + + if any(2400 <= f <= 2500 for f in freqs): + info["bands"].append({"name": "2.4GHz"}) + if any(5150 <= f <= 5900 for f in freqs): + info["bands"].append({"name": "5GHz"}) + if any(5955 <= f <= 7115 for f in freqs): + info["bands"].append({"name": "6GHz"}) + + out["wifi-radios"].append(info) + + def main(): out = { "vendor": None, @@ -544,6 +586,8 @@ def main(): if err: return err + probe_wifi_radios(out) + if not out["factory-password-hash"]: sys.stdout.write("\n\n\033[31mCRITICAL BOOTSTRAP ERROR\n" + "NO FACTORY PASSWORD FOUND\033[0m\n\n") diff --git a/board/common/rootfs/usr/libexec/infix/init.d/05-hostname b/board/common/rootfs/usr/libexec/infix/init.d/05-hostname deleted file mode 100755 index 2984ea3cc..000000000 --- a/board/common/rootfs/usr/libexec/infix/init.d/05-hostname +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -# Initialize default hostname for hostname.d pattern -# This runs very early in boot to set up the default hostname entry - -HOSTNAME_D="/etc/hostname.d" - -# Ensure directory exists -mkdir -p "$HOSTNAME_D" - -# If no default exists yet, create it from /etc/hostname (from squashfs) -if [ ! -f "$HOSTNAME_D/10-default" ] && [ -f /etc/hostname ]; then - cp /etc/hostname "$HOSTNAME_D/10-default" -fi - -# Apply hostname using the deterministic helper -if [ -x /usr/libexec/infix/hostname ]; then - /usr/libexec/infix/hostname -fi diff --git a/board/common/rootfs/usr/libexec/infix/init.d/10-sysctl-sync-ip-conf b/board/common/rootfs/usr/libexec/infix/init.d/10-sysctl-sync-ip-conf index d2bcd2fa4..d1a0df3b1 100755 --- a/board/common/rootfs/usr/libexec/infix/init.d/10-sysctl-sync-ip-conf +++ b/board/common/rootfs/usr/libexec/infix/init.d/10-sysctl-sync-ip-conf @@ -5,24 +5,21 @@ # interfaces at boot, such that physical interfaces will start from # the same point as virtual ones. tmp=$(mktemp) -fil=$(mktemp) +out=$(mktemp) # Ignore unreadable entries, like net.ipv6.conf.default.stable_secret sysctl net.ipv4.conf.default >"$tmp" 2>/dev/null sysctl net.ipv6.conf.default >>"$tmp" 2>/dev/null -# Filter out read-only entries like net.ipv4.conf.default.mc_forwarding -# to prevent misleading error messages in syslog -while IFS= read -r line; do - entry=$(echo "$line" | awk '{print $1}') - path="/proc/sys/$(echo "$entry" | tr . /)" - if [ -w "$path" ]; then - echo "$line" >> "$fil" - fi -done < "$tmp" - -for iface in $(ip -j link show | jq -r .[].ifname); do - sed -e "s/.default./.${iface}./g" "$fil" | sysctl -q -p - +# Build a single sysctl input with settings for all interfaces +for dir in /sys/class/net/*/; do + iface=${dir%/} + iface=${iface##*/} + sed "s/.default./.${iface}./g" "$tmp" >> "$out" done -rm "$tmp" "$fil" +# Apply all at once, suppress errors from read-only entries +# (e.g., net.ipv4.conf.*.mc_forwarding) +sysctl -q -p - < "$out" 2>/dev/null + +rm "$tmp" "$out" diff --git a/board/common/rootfs/usr/libexec/infix/init.d/30-cfg-migrate b/board/common/rootfs/usr/libexec/infix/init.d/30-cfg-migrate deleted file mode 100755 index def751055..000000000 --- a/board/common/rootfs/usr/libexec/infix/init.d/30-cfg-migrate +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh -# Check if /cfg/startup-config.cfg needs to be migrated to new syntax. -# Backup of the original is created in /cfg/backup/ for old versions, -# the migrate tool inserts old version in name before .cfg extension. -CONFIG_FILE="/cfg/startup-config.cfg" -BACKUP_FILE="/cfg/backup/startup-config.cfg" -BACKUP_DIR="$(dirname "$BACKUP_FILE")" - -mkdir -p "$BACKUP_DIR" -chown root:wheel "$BACKUP_DIR" -chmod 0770 "$BACKUP_DIR" - -if [ ! -f "$CONFIG_FILE" ]; then - logger -I $$ -k -p user.notice -t $(basename "$0") "No $(basename "$CONFIG_FILE" .cfg) yet, likely factory reset." - exit 0 -elif migrate -cq "$CONFIG_FILE"; then - exit 0 -fi - -migrate -i -b "$BACKUP_FILE" "$CONFIG_FILE" diff --git a/board/common/rootfs/usr/libexec/infix/mkcert b/board/common/rootfs/usr/libexec/infix/mkcert index 1bebec23b..6441a776c 100755 --- a/board/common/rootfs/usr/libexec/infix/mkcert +++ b/board/common/rootfs/usr/libexec/infix/mkcert @@ -1,7 +1,12 @@ #!/bin/sh +# Generate a self-signed TLS certificate. Called from confd keystore +# on first boot (or factory reset) when the gencert entry has empty +# keys. Output is written to a temporary directory that confd reads +# and then removes after importing into the keystore. -KEY=/cfg/ssl/private/self-signed.key -CRT=/cfg/ssl/certs/self-signed.crt +TMPDIR=/tmp/ssl +KEY=$TMPDIR/self-signed.key +CRT=$TMPDIR/self-signed.crt country=US state=California @@ -20,23 +25,11 @@ if [ -z "$cn" ]; then fi fi -generate() -{ - mkdir -p /cfg/ssl/private /cfg/ssl/certs - chmod 700 /cfg/ssl/private +mkdir -p "$TMPDIR" +chmod 700 "$TMPDIR" - gencert --country "$country" --state "$state" --city "$city" --organisation "$org" \ - --organisation-unit "$unit" --common-name "$cn" \ - --out-certificate $CRT --out-key $KEY -} +gencert --country "$country" --state "$state" --city "$city" --organisation "$org" \ + --organisation-unit "$unit" --common-name "$cn" \ + --out-certificate "$CRT" --out-key "$KEY" -CN=$(openssl x509 -noout -subject -in "${CRT}" 2>/dev/null |sed 's/.*CN=//') -if [ -z "$CN" ] || [ "$CN" != "$cn" ]; then - generate "$cn" -fi - -cp "${KEY}" "/etc/ssl/private/" -cp "${CRT}" "/etc/ssl/certs/" -initctl cond set mkcert - -exit 0 +exit $? diff --git a/board/common/rootfs/usr/libexec/infix/mnt b/board/common/rootfs/usr/libexec/infix/mnt index 4421613f3..dfe732fb2 100755 --- a/board/common/rootfs/usr/libexec/infix/mnt +++ b/board/common/rootfs/usr/libexec/infix/mnt @@ -59,17 +59,9 @@ is_mmc() { [ -n "$mmc" ] && return $mmc - # Check if primary or secondary partition (our rootfs) is on MMC - for label in primary secondary; do - devname=$(find_partition_by_label "$label" 2>/dev/null) - if [ -n "$devname" ]; then - case "$devname" in - mmcblk*) - mmc=0 - return 0 - ;; - esac - fi + # Fast sysfs check — avoids triggering the slow partition scan + for d in /sys/class/block/mmcblk[0-9]; do + [ -d "$d" ] && mmc=0 && return 0 done mmc=1 @@ -78,78 +70,68 @@ is_mmc() wait_mmc() { - # Try up to 50 times with 0.2s sleep = 10 second timeout - for _ in $(seq 50); do - if ls /dev/mmcblk* >/dev/null 2>&1; then - logger $opt -p user.notice -t "$nm" "MMC device available after delay" - return 0 - fi - sleep .2 + tries=50 + while [ $tries -gt 0 ]; do + for d in /dev/mmcblk[0-9]; do + if [ -b "$d" ]; then + logger $opt -p user.notice -t "$nm" "MMC device available after delay" + return 0 + fi + done + sleep .2 + tries=$((tries - 1)) done logger $opt -p user.warn -t "$nm" "Timeout waiting for MMC device" return 1 } -# This early on we don't have the luxury of /dev/disk/by-label/$1 +# Read filesystem label from an ext2/3/4 formatted whole disk. +# Superblock is at byte 1024, magic 0xEF53 at offset 56, label at 120. +# Handles both LE and BE byte order (bi-endian MIPS, etc.) +read_ext_label() +{ + magic=$(dd if="$1" bs=1 skip=1080 count=2 2>/dev/null \ + | od -t x2 -An | tr -d ' \n') + case "$magic" in + ef53|53ef) ;; + *) return 1 ;; + esac + + dd if="$1" bs=1 skip=1144 count=16 2>/dev/null | tr -d '\000' +} + +# Look up a block device by its GPT partition name using the kernel's +# sysfs uevent data (zero forks). Falls back to reading the ext2/3/4 +# superblock for whole disks without GPT (e.g., virtual or USB setups). find_partition_by_label() { - label="$1" + # The kernel exposes GPT partition names as PARTNAME in uevent + for uevent in /sys/class/block/*/uevent; do + while IFS='=' read -r key val; do + if [ "$key" = "PARTNAME" ]; then + if [ "$val" = "$1" ]; then + devname="${uevent%/uevent}" + echo "${devname##*/}" + return 0 + fi + break + fi + done < "$uevent" + done + # Fallback: ext filesystem label on whole disk for diskpath in /sys/class/block/*; do - devname=$(basename "$diskpath") - - # Skip partitions, only check whole disks [ -f "$diskpath/partition" ] && continue - - # Skip ram, loop, and other virtual devices + devname="${diskpath##*/}" case "$devname" in - ram*|loop*|nullb*|dm-*) continue ;; + ram*|loop*|nullb*|dm-*|*boot[0-9]*|*rpmb) continue ;; esac - - disk="/dev/$devname" - - # - # 1. Try GPT/MBR partition label using sgdisk - # - result=$(sgdisk -p "$disk" 2>/dev/null | awk -v label="$label" -v devname="$devname" ' - /^ *[0-9]/ { - if ($7 == label) { - if (devname ~ /^(mmcblk|nvme|loop)/) - print devname "p" $1; - else - print devname $1; - exit 0; - } - } - ') - - if [ -n "$result" ]; then - echo "$result" + fslabel=$(read_ext_label "/dev/$devname") + if [ "$fslabel" = "$1" ]; then + echo "$devname" return 0 fi - - # - # 2. Fallback: Check if the whole disk is an ext4/ext2/ext3 filesystem - # - - # Check for ext4/ext2/ext3 magic number (0xEF53) at offset 1080 (1024+56). - magic_number=$(dd if="$disk" bs=1 skip=1080 count=2 2>/dev/null | od -t x2 -A n | tr -d ' \n') - - # Check for both Little-Endian ('53ef') and Big-Endian ('ef53') interpretations of 0xEF53. - # This supports bi-endian architectures like MIPS that may run in BE mode, - # as well as the LE mode which is standard for RISC-V and ext filesystems. - if [ "$magic_number" = "ef53" ] || [ "$magic_number" = "53ef" ]; then - - # Read the volume label from offset 1144 (1024+120) - fslabel=$(dd if="$disk" bs=1 skip=1144 count=16 2>/dev/null | tr -d '\000') - logger $opt -p user.notice -t "$nm" "Found label $fslabel on disk $disk ..." - - if [ "$fslabel" = "$label" ]; then - echo "$devname" - return 0 - fi - fi done return 1 @@ -319,12 +301,6 @@ mount_rw() fi fi - # TODO: Also look for UBI partitions - - # Disable periodic fsck, yet keeping safety checks on ext4 - if grep "LABEL=$label" /etc/fstab |grep -q ext4; then - tune2fs -c 0 -i 0 LABEL="$1" 2>/dev/null - fi mount LABEL="$1" 2>/dev/null && return 0 return 1 diff --git a/configs/aarch64_defconfig b/configs/aarch64_defconfig index 5fcaf15f0..4818c08a1 100644 --- a/configs/aarch64_defconfig +++ b/configs/aarch64_defconfig @@ -149,8 +149,11 @@ INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://www.kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" BR2_PACKAGE_FEATURE_GPS=y +BR2_PACKAGE_FEATURE_WIFI=y BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y BR2_PACKAGE_FEATURE_WIFI_REALTEK=y +BR2_PACKAGE_BRCMFMAC_SDIO_FIRMWARE_RPI=y +BR2_PACKAGE_BRCMFMAC_SDIO_FIRMWARE_RPI_WIFI=y BR2_PACKAGE_CONFD=y BR2_PACKAGE_NETD=y BR2_PACKAGE_CONFD_TEST_MODE=y diff --git a/configs/aarch64_minimal_defconfig b/configs/aarch64_minimal_defconfig index e535b5350..10b271ade 100644 --- a/configs/aarch64_minimal_defconfig +++ b/configs/aarch64_minimal_defconfig @@ -126,7 +126,6 @@ INFIX_DESC="Infix is an immutable, friendly, and secure operating system that tu INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://www.kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" -BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_CONFD=y BR2_PACKAGE_NETD=y BR2_PACKAGE_CONFD_TEST_MODE=y diff --git a/configs/arm_minimal_defconfig b/configs/arm_minimal_defconfig index f57a0263c..bb8840d9d 100644 --- a/configs/arm_minimal_defconfig +++ b/configs/arm_minimal_defconfig @@ -126,7 +126,6 @@ INFIX_DESC="Infix is an immutable, friendly, and secure operating system that tu INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://www.kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" -BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_CONFD=y BR2_PACKAGE_NETD=y BR2_PACKAGE_CONFD_TEST_MODE=y diff --git a/configs/sama7g54_ek_emmc_boot_defconfig b/configs/sama7g54_ek_emmc_boot_defconfig index 19f7f923c..18b0e1c93 100644 --- a/configs/sama7g54_ek_emmc_boot_defconfig +++ b/configs/sama7g54_ek_emmc_boot_defconfig @@ -20,9 +20,6 @@ BR2_TARGET_AT91BOOTSTRAP3_CUSTOM_TARBALL=y BR2_TARGET_AT91BOOTSTRAP3_CUSTOM_TARBALL_LOCATION="$(call github,linux4sam,at91bootstrap,v4.0.9)/at91bootstrap-v4.0.9.tar.gz" BR2_TARGET_AT91BOOTSTRAP3_DEFCONFIG="sama7g5ekemmc_uboot" BR2_TARGET_UBOOT=y -BR2_TARGET_UBOOT_BUILD_SYSTEM_KCONFIG=y -BR2_TARGET_UBOOT_CUSTOM_TARBALL=y -BR2_TARGET_UBOOT_CUSTOM_TARBALL_LOCATION="$(call github,linux4sam,u-boot-at91,linux4microchip-2025.04)/u-boot-at91-linux4microchip-2025.04.tar.gz" BR2_TARGET_UBOOT_BOARD_DEFCONFIG="sama7g5ek_mmc" BR2_TARGET_UBOOT_CONFIG_FRAGMENT_FILES="$(BR2_EXTERNAL_INFIX_PATH)/board/common/uboot/extras.config $(BR2_EXTERNAL_INFIX_PATH)/board/arm/microchip-sama7g54-ek/uboot/extras.config $(BR2_EXTERNAL_INFIX_PATH)/board/arm/microchip-sama7g54-ek/uboot/emmc-extras.config" BR2_TARGET_UBOOT_NEEDS_DTC=y diff --git a/configs/sama7g54_ek_sd_boot_defconfig b/configs/sama7g54_ek_sd_boot_defconfig index 57274befb..135e26f0a 100644 --- a/configs/sama7g54_ek_sd_boot_defconfig +++ b/configs/sama7g54_ek_sd_boot_defconfig @@ -20,9 +20,6 @@ BR2_TARGET_AT91BOOTSTRAP3_CUSTOM_TARBALL=y BR2_TARGET_AT91BOOTSTRAP3_CUSTOM_TARBALL_LOCATION="$(call github,linux4sam,at91bootstrap,v4.0.9)/at91bootstrap-v4.0.9.tar.gz" BR2_TARGET_AT91BOOTSTRAP3_DEFCONFIG="sama7g5eksd_uboot" BR2_TARGET_UBOOT=y -BR2_TARGET_UBOOT_BUILD_SYSTEM_KCONFIG=y -BR2_TARGET_UBOOT_CUSTOM_TARBALL=y -BR2_TARGET_UBOOT_CUSTOM_TARBALL_LOCATION="$(call github,linux4sam,u-boot-at91,linux4microchip-2025.04)/u-boot-at91-linux4microchip-2025.04.tar.gz" BR2_TARGET_UBOOT_BOARD_DEFCONFIG="sama7g5ek_mmc1" BR2_TARGET_UBOOT_CONFIG_FRAGMENT_FILES="$(BR2_EXTERNAL_INFIX_PATH)/board/common/uboot/extras.config $(BR2_EXTERNAL_INFIX_PATH)/board/arm/microchip-sama7g54-ek/uboot/extras.config $(BR2_EXTERNAL_INFIX_PATH)/board/arm/microchip-sama7g54-ek/uboot/sd-extras.config" BR2_TARGET_UBOOT_NEEDS_DTC=y diff --git a/configs/x86_64_minimal_defconfig b/configs/x86_64_minimal_defconfig index 7d1d55f47..2ad29a9b0 100644 --- a/configs/x86_64_minimal_defconfig +++ b/configs/x86_64_minimal_defconfig @@ -125,7 +125,6 @@ INFIX_DESC="Infix is an immutable, friendly, and secure operating system that tu INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://www.kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" -BR2_PACKAGE_FEATURE_GPS=y BR2_PACKAGE_CONFD=y BR2_PACKAGE_NETD=y BR2_PACKAGE_CONFD_TEST_MODE=y diff --git a/doc/branding.md b/doc/branding.md index f98941126..7056c95fa 100644 --- a/doc/branding.md +++ b/doc/branding.md @@ -59,6 +59,8 @@ them: XPath: `/ietf-system:system/authentication/user/password` - **Default SSH and NETCONF hostkey:** `genkey` (regenerated at factory reset) XPath: `/ietf-keystore:keystore/asymmetric-keys/asymmetric-key[name='genkey']` +- **Default HTTPS certificate:** `gencert` (self-signed, regenerated at factory reset) + XPath: `/ietf-keystore:keystore/asymmetric-keys/asymmetric-key[name='gencert']` - **Hostname format specifiers:** XPath: `/ietf-system:system/hostname` - `%i`: OS ID, from `/etc/os-release`, from Menuconfig branding @@ -219,8 +221,12 @@ Notice how both the public and private keys are left empty here, this cause them to be always automatically regenerated after each factory reset. Keeping the `factory-config` snippet like this means we can use the same file on multiple devices, without risking them sharing the same host -keys. Sometimes you may want the same host keys, but that is the easy -use-case and not documented here. +keys or TLS certificates. Sometimes you may want the same keys, but +that is the easy use-case and not documented here. + +The `genkey` entry is the default SSH host key, and `gencert` is the +default self-signed HTTPS certificate used by the web server (nginx). +Both are regenerated on factory reset when their keys are empty. ```json "ietf-keystore:keystore": { @@ -233,6 +239,18 @@ use-case and not documented here. "private-key-format": "ietf-crypto-types:rsa-private-key-format", "cleartext-private-key": "", "certificates": {} + }, + { + "name": "gencert", + "public-key-format": "infix-crypto-types:x509-public-key-format", + "public-key": "", + "private-key-format": "infix-crypto-types:rsa-private-key-format", + "cleartext-private-key": "", + "certificates": { + "certificate": [ + { "name": "self-signed", "cert-data": "" } + ] + } } ] } @@ -292,9 +310,22 @@ use-case and not documented here. "port": 22 } ] + }, + "infix-services:web": { + "certificate": "gencert", + "enabled": true, + "console": { "enabled": true }, + "netbrowse": { "enabled": true }, + "restconf": { "enabled": true } } ``` +The `certificate` leaf references an asymmetric key in the keystore +that has an associated certificate. The default `gencert` entry uses +a self-signed certificate. To use a custom (e.g., CA-signed) +certificate, create a new keystore entry with +`x509-public-key-format` and point the web `certificate` leaf to it. + ## Integration When integrating your software stack with Infix there may be protocols diff --git a/doc/cli/configure.md b/doc/cli/configure.md index 514e6aaec..b8300af45 100644 --- a/doc/cli/configure.md +++ b/doc/cli/configure.md @@ -40,16 +40,14 @@ admin@host:/config/interface/eth0/> up admin@host:/config/> ``` ----- - -> **Note:** commands in configure context are automatically generated -> from the system's YANG models, hence different products likely have a -> different set of commands. However, both the `ietf-interfaces.yang` -> and `ietf-ip.yang` models, for instance, that provide the networking +> **Note** +> +> Commands in configure context are automatically generated from the +> system's YANG models, hence different products likely have a different +> set of commands. However, both the `ietf-interfaces.yang` and +> `ietf-ip.yang` models, for instance, that provide the networking > support are common to all systems. ----- - ## Set IP Address on an Interface ``` @@ -74,7 +72,6 @@ interfaces { } ``` - ## Saving Changes Apply the changes (from candidate to `running-config`): @@ -85,12 +82,12 @@ admin@host:/> show running-config ... interfaces { interface eth0 { - type ethernetCsmacd; - ipv4 { - address 192.168.2.200 { - prefix-length 24; - } - } + type ethernetCsmacd; + ipv4 { + address 192.168.2.200 { + prefix-length 24; + } + } } ... ``` @@ -106,12 +103,10 @@ admin@host:/> copy running-config startup-config The `startup-config` can also be inspected with the `show` command to verify the changes are saved. ----- - -> **Note:** all commands need to be spelled out, no short forms are -> allowed in the CLI. Use the `TAB` key to make your life easier. - ----- +> **Important** +> +> All commands need to be spelled out, no short forms are allowed in the +> CLI. Use the `TAB` key to make your life easier. ## Changing Hostname @@ -128,12 +123,10 @@ admin@example:/> Notice how the hostname in the prompt does not change until the change is committed. ----- - -> **Note:** critical services like syslog, mDNS, LLDP, and similar that -> advertise the hostname, are restarted when the hostname is changed. - ----- +> **Note** +> +> Critical services like syslog, mDNS, LLDP, and similar that advertise +> the hostname, are restarted when the hostname is changed. ## Changing Password @@ -142,14 +135,14 @@ User management, including passwords, is also a part of `ietf-system`. ``` admin@host:/config/> edit system authentication user admin admin@host:/config/system/authentication/user/admin/> change password -New password: -Retype password: +New password: +Retype password: admin@host:/config/system/authentication/user/admin/> leave ``` The `change password` command starts an interactive dialogue that asks for the new password, with a confirmation, and then salts and encrypts -the password with sha512crypt. +the password with sha512crypt. It is also possible to use the `set password ...` command. This allows setting an already hashed password. To manually hash a password, use @@ -157,13 +150,11 @@ the `do password encrypt` command. This launches the admin-exec command to hash, and optionally salt, your password. This encrypted string can then be used with `set password ...`. ----- - -> **Tip:** if you are having trouble thinking of a password, there is -> also `do password generate`, which generates random but readable -> strings using the UNIX command `pwgen`. - ----- +> **Tip** +> +> If you are having trouble thinking of a password, there is also `do +> password generate`, which generates random but readable strings using +> the UNIX command `pwgen`. ## SSH Authorized Key @@ -185,11 +176,11 @@ key-data AAAAB3NzaC1yc2EAAAADAQABAAABgQC8iBL42yeMBioFay7lty1C4ZDTHcHyo739gc91rTT admin@host:/config/system/authentication/user/admin/authorized-key/example@host/> leave ``` ----- - -> **Note:** the `ssh-keygen` program already base64 encodes the public -> key data, so there is no need to use the `text-editor` command, `set` -> does the job. +> **Note** +> +> The `ssh-keygen` program already base64 encodes the public key data, +> so there is no need to use the `text-editor` command, `set` does the +> job. ---- @@ -229,13 +220,12 @@ admin@host:/config/> leave See the bridging example below for more. ----- - -> **Note:** in the CLI you do not have to create the `veth0b` interface. -> The system _infers_ this for you. When setting up a VETH pair using -> NETCONF, however, you must include the `veth0b` interface. - ----- +> **Tip** +> +> In the CLI you do not have to create the `veth0b` interface. The +> system _infers_ this for you. This does not apply when setting up a +> VETH pair using NETCONF or RESTCONF, then you must submit a complete +> configuration. ## Creating a Bridge @@ -291,10 +281,8 @@ the VETH pair from the previous example) are now bridged. Any traffic ingressing one port will egress the other. Only reserved IEEE multicast is filtered, except LLDP frames as shown above. ----- - -> **Note:** the bridge can be named anything, provided the interface -> name is not already taken. However, for any name outside the pattern -> `br[0-9]+`, you have to set the interface type manually to `bridge`. - ----- +> **Important** +> +> The bridge can be named anything, provided the interface name is not +> already taken. However, for any name outside the pattern `br[0-9]+`, +> you have to set the interface type manually to `bridge`. diff --git a/doc/cli/introduction.md b/doc/cli/introduction.md index a104f8a4b..4630c3562 100644 --- a/doc/cli/introduction.md +++ b/doc/cli/introduction.md @@ -34,13 +34,11 @@ admin@host-12-34-56:/> show # Try: Tab or ? admin@host-12-34-56:/> # Try: Tab or ? ``` ----- - -> **Note:** even on an empty command line, you can tap the `Tab` or `?` keys. +> **Tip** +> +> Even on an empty command line, you can tap the `Tab` or `?` keys. > See [`help keybindings`](keybindings.md) for more tips! ----- - ## Key Concepts The two modes in the CLI are the admin-exec and the configure context. @@ -78,15 +76,15 @@ and *running* that can be managed and inspected using the `copy`, `show`, and `configure` commands. The traditional names used in the CLI for these are listed below: - - `factory-config` the default configuration from factory for the - device, i.e., what the system returns to after a `factory-reset` - - `startup-config` created from `factory-config` at first boot after - factory reset. Loaded as the system configuration on each boot - - `running-config` what is actively running on the system. If no - changes have been made since boot, it is the same as `startup-config` - - `candidate-config` is created from `running-config` when entering the - configure context. Any changes made here can be discarded (`abort`, - `rollback`) or committed (`commit`, `leave`) to `running-config` +- `factory-config` the default configuration from factory for the + device, i.e., what the system returns to after a `factory-reset` +- `startup-config` created from `factory-config` at first boot after + factory reset. Loaded as the system configuration on each boot +- `running-config` what is actively running on the system. If no + changes have been made since boot, it is the same as `startup-config` +- `candidate-config` is created from `running-config` when entering the + configure context. Any changes made here can be discarded (`abort`, + `rollback`) or committed (`commit`, `leave`) to `running-config` Edit the *running* configuration using the `configure` command. This copies *running* to *candidate*, a temporary datastore, where changes @@ -131,16 +129,13 @@ In *configure context* the following commands are available: | `do command` | Call admin-exec command: `do show log` | | `commit` | | - ## Example Session ----- - +> **Tip** +> > Remember to use the `TAB` and `?` keys to speed up your navigation. > See [`help keybindings`](keybindings.md) for more tips! ----- - In this example we enter configure context to add an IPv4 address to interface `eth0`, then we apply the changes using the `leave` command. @@ -151,8 +146,8 @@ save the changes for the next reboot. admin@host-12-34-56:/> configure admin@host-12-34-56:/config/> edit interface eth0 admin@host-12-34-56:/config/interface/eth0/> set ipv4 - address autoconf bind-ni-name enabled - forwarding mtu neighbor + address autoconf bind-ni-name dhcp + enabled forwarding mtu neighbor admin@host-12-34-56:/config/interface/eth0/> set ipv4 address 192.168.2.200 prefix-length 24 admin@host-12-34-56:/config/interface/eth0/> show type ethernetCsmacd; @@ -193,10 +188,12 @@ admin@host-12-34-56:/> copy startup-config running-config Or restart the device, for example if the change to the configuration caused you to lose contact with the system (it happens to the best of -us). The system will start up from the last "save gave". +us). The system will start up from the last "save game". -> **Tip:** when restoring a backup of a configuration, or having manually -> edited a config file, you can validate it using system's YANG models, -> it is *not* applied if validation is successful: +> **Tip:** Restoring Backups +> +> When restoring a backup of a configuration, or having manually edited +> a config file, you can validate it using the system's YANG models, it +> is *not* applied if validation is successful: > > `copy /media/backup/old.cfg running-config validate` diff --git a/doc/cli/keybindings.md b/doc/cli/keybindings.md index 2633ccd9d..6687ccb1e 100644 --- a/doc/cli/keybindings.md +++ b/doc/cli/keybindings.md @@ -33,7 +33,7 @@ CLI has several keybindings, most significant first: | Ctrl-t | | Transpose/Swap characters before and at cursor | | Meta-# | Alt-Shift-3 | Prepend # to current line and submit to history | -## What is Meta? +## What is Meta The Meta key is called Alt on most modern keyboards. If you have neither, first tap the Esc key instead of holding down Alt/Meta. @@ -53,4 +53,3 @@ See possible arguments, with brief help text, to a command: ... Type the command, then tap the `?` key. - diff --git a/doc/cli/netcalc.md b/doc/cli/netcalc.md index 8514bb1fd..1c2e5416a 100644 --- a/doc/cli/netcalc.md +++ b/doc/cli/netcalc.md @@ -18,7 +18,6 @@ A subnet can be entered in two ways: An optional `split LEN` can be given as argument, the new length value must be bigger than the current prefix length. See example below. - ## Examples Its most commonly used features are to understand how many addresses an diff --git a/doc/cli/quick.md b/doc/cli/quick.md index dd8dfe02d..4762b3c06 100644 --- a/doc/cli/quick.md +++ b/doc/cli/quick.md @@ -23,10 +23,8 @@ keybindings are really useful to learn! | `help text-editor` | Help with the built-in text-editor command | | `help keybindings` | Lists available keybindings & other helpful tricks | ----- - -> In `configure` context the `help setting` command shows the YANG +> **Tip:** Online Help +> +> In `configure` context the `help ` command shows the YANG > description text for each node and container. To reach the admin > exec help from configure context, e.g., `do help text-editor` - ----- diff --git a/doc/cli/upgrade.md b/doc/cli/upgrade.md index 10cdf5c1b..09ef60ada 100644 --- a/doc/cli/upgrade.md +++ b/doc/cli/upgrade.md @@ -10,7 +10,7 @@ use the `upgrade` command and a URI to a ftp/tftp/sftp or http/https server that hosts the file: ``` -admin@host:/> upgrade tftp://192.168.122.1/firmware-x86_64-v23.11.pkg +admin@example:/> upgrade tftp://192.168.122.1/firmware-x86_64-v23.11.pkg installing 0% Installing 0% Determining slot states @@ -31,7 +31,7 @@ installing 99% Updating slots done. 100% Installing done. Installing `tftp://192.168.122.1/firmware-x86_64-v23.11.pkg` succeeded -admin@host:/> +admin@example:/> ``` The secondary partition (`rootfs.1`) has now been upgraded and will be used as diff --git a/doc/keystore.md b/doc/keystore.md index 55da5961c..79db20def 100644 --- a/doc/keystore.md +++ b/doc/keystore.md @@ -11,6 +11,7 @@ The keystore supports two types of cryptographic keys: 1. **Asymmetric Keys** — public/private key pairs used for: - SSH host authentication (RSA keys) + - HTTPS/TLS certificates (X.509 keys) - WireGuard VPN tunnels (X25519 keys) 2. **Symmetric Keys** — shared secrets used for: @@ -22,10 +23,11 @@ managed via CLI, NETCONF, or RESTCONF. ### Supported Formats -| **Asymmetric Key Format** | **Use Case** | **Key Type** | -|----------------------------------------------------------|---------------|--------------| -| `rsa-private-key-format` / `ssh-public-key-format` | SSH host keys | RSA | -| `x25519-private-key-format` / `x25519-public-key-format` | WireGuard VPN | Curve25519 | +| **Asymmetric Key Format** | **Use Case** | **Key Type** | +|----------------------------------------------------------|--------------------|--------------| +| `rsa-private-key-format` / `ssh-public-key-format` | SSH host keys | RSA | +| `rsa-private-key-format` / `x509-public-key-format` | TLS certificates | RSA + X.509 | +| `x25519-private-key-format` / `x25519-public-key-format` | WireGuard VPN | Curve25519 | | **Symmetric Key Format** | **Use Case** | |-----------------------------|-----------------------------------| @@ -46,6 +48,53 @@ keystore with the name `genkey`. See [SSH Management](management.md) for details on generating and importing custom SSH host keys. +### TLS Certificates + +TLS certificates are used by the web server (nginx) for HTTPS connections. +The default certificate is a self-signed certificate automatically generated +on first boot and stored in the keystore with the name `gencert`. Like SSH +host keys, the certificate is regenerated on factory reset when its keys are +empty. + +The web server's `certificate` leaf references which keystore entry to use: + +```json +"infix-services:web": { + "certificate": "gencert", + "enabled": true +} +``` + +To use a custom (e.g., CA-signed) certificate, create a new asymmetric key +entry with `x509-public-key-format`, populate it with your certificate and +private key, then point the web `certificate` leaf to it: + +```json +"ietf-keystore:keystore": { + "asymmetric-keys": { + "asymmetric-key": [ + { + "name": "my-cert", + "public-key-format": "infix-crypto-types:x509-public-key-format", + "public-key": "", + "private-key-format": "infix-crypto-types:rsa-private-key-format", + "cleartext-private-key": "", + "certificates": { + "certificate": [ + { "name": "ca-signed", "cert-data": "" } + ] + } + } + ] + } +} +``` + +> [!NOTE] +> The `public-key` and `cert-data` fields contain base64-encoded PEM data +> with the `-----BEGIN/END-----` markers stripped. The system reconstructs +> the PEM files when writing them to disk for nginx. + ### WireGuard Keys WireGuard uses X25519 elliptic curve cryptography for key exchange. Each @@ -116,6 +165,7 @@ wg-psk octet-string zYr83O4Ykj9i1gN+/aaosJxQx... Asymmetric Keys NAME TYPE PUBLIC KEY genkey rsa MIIBCgKCAQEAnj0YinjhYDgYbEGuh7... +gencert x509 MIIDXTCCAkWgAwIBAgIJAJC1HiIAZA... wg-tunnel x25519 bN1CwZ1lTP6KsrCwZ1lTP6KsrCwZ1... diff --git a/doc/management.md b/doc/management.md index 486cd6846..2a75214cb 100644 --- a/doc/management.md +++ b/doc/management.md @@ -136,6 +136,7 @@ the unit's neighbors, collected via mDNS (see
admin@example:/> configure
 admin@example:/config/> edit web
 admin@example:/config/web/> help
+  certificate                       Reference to asymmetric key in central keystore.
   enabled                           Enable or disable on all web services.
   console                           Web console interface.
   netbrowse                         mDNS Network Browser.
@@ -191,6 +192,23 @@ admin@example:/config/web/restconf/> no enabled
 admin@example:/config/web/restconf/>
 
+### HTTPS Certificate + +The Web server uses a TLS certificate from the central +[keystore](keystore.md). By default it uses `gencert`, a self-signed +certificate that is automatically generated on first boot. + +To use a different certificate, e.g., one signed by a CA, first add +it to the keystore as an asymmetric key with `x509-public-key-format`, +then point the web `certificate` leaf to it: + +
admin@example:/config/web/> set certificate my-cert
+admin@example:/config/web/>
+
+ +See [Keystore](keystore.md#tls-certificates) for details on managing +TLS certificates. + ## System Upgrade See [Upgrade & Boot Order](upgrade.md) for information on upgrading. diff --git a/package/Config.in b/package/Config.in index 0c0115b48..110d247b2 100644 --- a/package/Config.in +++ b/package/Config.in @@ -20,6 +20,7 @@ source "$BR2_EXTERNAL_INFIX_PATH/package/firewall/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/greenpak-programmer/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/ifupdown-ng/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/iito/Config.in" +source "$BR2_EXTERNAL_INFIX_PATH/package/initviz/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/k8s-logger/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/keyack/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/klish-plugin-infix/Config.in" diff --git a/package/confd/confd.conf b/package/confd/confd.conf index 482ee26e8..cc8d05720 100644 --- a/package/confd/confd.conf +++ b/package/confd/confd.conf @@ -1,27 +1,11 @@ #set DEBUG=1 -run name:bootstrap log:prio:user.notice norestart \ - [S] /usr/libexec/confd/bootstrap \ - -- Bootstrapping YANG datastore - -run name:error :1 log:console norestart if: \ - [S] /usr/libexec/confd/error -- - -service name:confd log:prio:daemon.err \ - [S12345] sysrepo-plugind -f -p /run/confd.pid -n -v warning \ +# Single daemon handles gen-config, datastore init, config load, and plugins +# log:prio:daemon.err +service log:console env:/etc/default/confd \ + [S12345] confd -f -v warning \ + -F /etc/factory-config.cfg \ + -S /cfg/startup-config.cfg \ + -E /etc/failure-config.cfg \ + -t $CONFD_TIMEOUT \ -- Configuration daemon - -# Bootstrap system with startup-config -run name:startup log:prio:user.notice norestart env:/etc/default/confd \ - [S] /usr/libexec/confd/load -t $CONFD_TIMEOUT startup-config \ - -- Loading startup-config - -# Run if loading startup-config fails for some reason -run name:failure log:prio:user.crit norestart env:/etc/default/confd \ - if: \ - [S] /usr/libexec/confd/load -t $CONFD_TIMEOUT failure-config \ - -- Loading failure-config - -run name:error :2 log:console norestart \ - if: \ - [S] /usr/libexec/confd/error -- diff --git a/package/confd/confd.mk b/package/confd/confd.mk index 7cfb2347f..6f81deb0f 100644 --- a/package/confd/confd.mk +++ b/package/confd/confd.mk @@ -4,13 +4,13 @@ # ################################################################################ -CONFD_VERSION = 1.7 +CONFD_VERSION = 1.8 CONFD_SITE_METHOD = local CONFD_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/confd CONFD_LICENSE = BSD-3-Clause CONFD_LICENSE_FILES = LICENSE CONFD_REDISTRIBUTE = NO -CONFD_DEPENDENCIES = host-sysrepo sysrepo rousette netopeer2 jansson libite sysrepo libsrx libglib2 +CONFD_DEPENDENCIES = host-sysrepo sysrepo rousette netopeer2 jansson libite sysrepo libsrx libglib2 libev CONFD_AUTORECONF = YES CONFD_CONF_OPTS += --disable-silent-rules --with-crypt=$(BR2_PACKAGE_CONFD_DEFAULT_CRYPT) CONFD_SYSREPO_SHM_PREFIX = sr_buildroot$(subst /,_,$(CONFIG_DIR))_confd diff --git a/package/confd/resolvconf.conf b/package/confd/resolvconf.conf index e08a1acc1..1edc74b71 100644 --- a/package/confd/resolvconf.conf +++ b/package/confd/resolvconf.conf @@ -1,3 +1,2 @@ -# Create initial /etc/resolv.conf after successful bootstrap, regardless -# of startup-config or failure-config. Condition set by confd. -task [S12345] resolvconf -u -- Update DNS configuration +# Update /etc/resolv.conf after successful bootstrap and reconf. +task [S12345] resolvconf -u -- diff --git a/package/initviz/Config.in b/package/initviz/Config.in new file mode 100644 index 000000000..df811d260 --- /dev/null +++ b/package/initviz/Config.in @@ -0,0 +1,22 @@ +config BR2_PACKAGE_INITVIZ + bool "initviz" + depends on BR2_USE_MMU # fork() + help + InitViz is a performance analysis and visualization tool for the + boot process and system services. It consists of the bootchartd + data collection daemon (bootchartd) that runs during boot to + capture system activity, and InitViz the host visualization tool. + + InitViz is a reimplementation and successor to the bootchart2 + project, offering a more feature-rich solution compared to the + bootchartd subset available as a BusyBox applet. + + To profile the boot process, append the following to the kernel + command line: + + init=/sbin/bootchartd initcall_debug printk.time=y quiet + + The collected data can be visualized using the host-initviz + tool, initviz.py, which is currently not built here. + + https://github.com/finit-project/InitViz diff --git a/package/initviz/initviz.hash b/package/initviz/initviz.hash new file mode 100644 index 000000000..b0f379cbd --- /dev/null +++ b/package/initviz/initviz.hash @@ -0,0 +1,3 @@ +# Locally calculated +sha256 28a059ca6d3cbc5f65809a18167d089fd0dc2be13cd6c640c56ddae47be01849 initviz-1.0.0-rc1.tar.gz +sha256 54e1afa760fa3649fa47c7838ac937771e74af695d4cf7d907bc61c107c83dc9 COPYING diff --git a/package/initviz/initviz.mk b/package/initviz/initviz.mk new file mode 100644 index 000000000..15d28eb35 --- /dev/null +++ b/package/initviz/initviz.mk @@ -0,0 +1,26 @@ +################################################################################ +# +# initviz +# +################################################################################ + +INITVIZ_VERSION = 1.0.0-rc1 +INITVIZ_SITE = https://github.com/finit-project/InitViz/releases/download/$(INITVIZ_VERSION) +INITVIZ_SOURCE = initviz-$(INITVIZ_VERSION).tar.gz +INITVIZ_LICENSE = GPL-2.0-or-later +INITVIZ_LICENSE_FILES = COPYING + +# Target package: bootchartd collector daemon +define INITVIZ_BUILD_CMDS + $(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) \ + $(MAKE) -C $(@D) collector +endef + +define INITVIZ_INSTALL_TARGET_CMDS + $(TARGET_MAKE_ENV) $(MAKE) -C $(@D) \ + DESTDIR=$(TARGET_DIR) \ + EARLY_PREFIX= \ + install-collector +endef + +$(eval $(generic-package)) diff --git a/package/klish/klish.svc b/package/klish/klish.svc index 6344c8a28..b044b89b2 100644 --- a/package/klish/klish.svc +++ b/package/klish/klish.svc @@ -1 +1 @@ -service log [2345] /usr/bin/klishd -d -- CLI backend daemon +service log:null [12345] /usr/bin/klishd -d -- CLI backend daemon diff --git a/package/mdns-alias/mdns-alias.svc b/package/mdns-alias/mdns-alias.svc index dfe65d8ff..376ccb586 100644 --- a/package/mdns-alias/mdns-alias.svc +++ b/package/mdns-alias/mdns-alias.svc @@ -1,2 +1,3 @@ -service env:-/etc/default/mdns-alias log:prio:daemon.debug,tag:mdns \ - [2345] mdns-alias $MDNS_ALIAS_ARGS -- mDNS alias advertiser +service log:null env:-/etc/default/mdns-alias \ + [2345] mdns-alias $MDNS_ALIAS_ARGS \ + -- mDNS alias advertiser diff --git a/package/skeleton-init-finit/skeleton/etc/default/zebra b/package/skeleton-init-finit/skeleton/etc/default/zebra index 4467b9af2..19e0897c7 100644 --- a/package/skeleton-init-finit/skeleton/etc/default/zebra +++ b/package/skeleton-init-finit/skeleton/etc/default/zebra @@ -1,2 +1,2 @@ -# --log-level debug -ZEBRA_ARGS="-A 127.0.0.1 -u frr -g frr --log syslog --log-level err" +# --log-level debug --graceful_restart 60 +ZEBRA_ARGS="-A 127.0.0.1 -a -s 90000000 -u frr -g frr --log syslog --log-level err" diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/dnsmasq.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/dnsmasq.conf index a14723e8a..c95bf5819 100644 --- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/dnsmasq.conf +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/dnsmasq.conf @@ -1 +1 @@ -service [S12345] dnsmasq -k -u root -- DHCP/DNS proxy +service [S12345] dnsmasq -k -u root -- DHCP/DNS proxy diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/mgmtd.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/mgmtd.conf index 825aff9cc..fbf2c0861 100644 --- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/mgmtd.conf +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/mgmtd.conf @@ -1,2 +1,3 @@ service pid:!/run/frr/mgmtd.pid env:-/etc/default/mgmtd \ - [2345] mgmtd $MGMTD_ARGS -- FRR MGMT daemon + [2345] mgmtd $MGMTD_ARGS \ + -- FRR MGMT daemon diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ospfd.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ospfd.conf index e4652ff1f..20b523396 100644 --- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ospfd.conf +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ospfd.conf @@ -1,2 +1,3 @@ -service env:-/etc/default/ospfd \ - [2345] ospfd $OSPFD_ARGS -- OSPF daemon +service pid:!/run/frr/ospfd.pid env:-/etc/default/ospfd \ + [2345] ospfd $OSPFD_ARGS \ + -- OSPF daemon diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripd.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripd.conf index bb311b582..8a8f93308 100644 --- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripd.conf +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/ripd.conf @@ -1,3 +1,3 @@ -service env:-/etc/default/ripd \ - [2345] ripd $RIPD_ARGS +service pid:!/run/frr/ripd.pid env:-/etc/default/ripd \ + [2345] ripd $RIPD_ARGS \ -- RIP daemon diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/zebra.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/zebra.conf index dce1abcac..d423b539c 100644 --- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/zebra.conf +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/zebra.conf @@ -1,3 +1,5 @@ -service pid:!/run/frr/zebra.pid env:-/etc/default/zebra \ - [2345] zebra $ZEBRA_ARGS +# Unfortunately Zebra in Frr v10.5.1 is a bit buggy and does not +# properly flush unused routes the kernel has removed. <~pid/netd> +service pid:!/run/frr/zebra.pid env:-/etc/default/zebra \ + [2345] zebra $ZEBRA_ARGS \ -- Zebra routing daemon diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/nginx.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/nginx.conf index 0286c2154..2905bb11e 100644 --- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/nginx.conf +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/nginx.conf @@ -1,2 +1,2 @@ -service env:-/etc/default/nginx \ +service env:-/etc/default/nginx \ [2345] nginx -g 'daemon off;' $NGINX_ARGS -- Web server diff --git a/package/statd/statd.conf b/package/statd/statd.conf index 53f5214da..d88025583 100644 --- a/package/statd/statd.conf +++ b/package/statd/statd.conf @@ -1,3 +1,3 @@ #set DEBUG=1 -service name:statd log [S12345] statd -f -p /run/statd.pid -n -- Status daemon +service name:statd [12345] statd -f -p /run/statd.pid -n -- Status daemon diff --git a/patches/uboot/2025.01/0003-arm-dts-at91-sama7g5ek-increase-clock-for-sdmmc-from.patch b/patches/uboot/2025.01/0003-arm-dts-at91-sama7g5ek-increase-clock-for-sdmmc-from.patch index 0e96b9214..e85ad96bc 100644 --- a/patches/uboot/2025.01/0003-arm-dts-at91-sama7g5ek-increase-clock-for-sdmmc-from.patch +++ b/patches/uboot/2025.01/0003-arm-dts-at91-sama7g5ek-increase-clock-for-sdmmc-from.patch @@ -16,7 +16,7 @@ improve the boot time when reading the kernel binary. Tested on sama7g5ek rev 5 using mmcinfo command. Signed-off-by: Mihai Sain -Signed-off-by: Mattias Walström +Signed-off-by: Joachim Wiberg --- arch/arm/dts/at91-sama7g5ek.dts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/confd/bin/Makefile.am b/src/confd/bin/Makefile.am index 49bed009d..83ba7da5a 100644 --- a/src/confd/bin/Makefile.am +++ b/src/confd/bin/Makefile.am @@ -1,4 +1,4 @@ -pkglibexec_SCRIPTS = bootstrap error load gen-service gen-hostname \ +pkglibexec_SCRIPTS = gen-config gen-hostname \ gen-interfaces gen-motd gen-hardware gen-version \ mstpd-wait-online wait-interface sbin_SCRIPTS = dagger migrate firewall diff --git a/src/confd/bin/error b/src/confd/bin/error deleted file mode 100755 index 4ed1ca3e9..000000000 --- a/src/confd/bin/error +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -# Override using an overlay in your br2-external to change the behavior - -logger -sik -p user.error "The device has reached an unrecoverable error, please RMA." diff --git a/src/confd/bin/bootstrap b/src/confd/bin/gen-config similarity index 60% rename from src/confd/bin/bootstrap rename to src/confd/bin/gen-config index 371d80286..d58e8b7b0 100755 --- a/src/confd/bin/bootstrap +++ b/src/confd/bin/gen-config @@ -1,42 +1,32 @@ #!/bin/sh -# Bootstrap system factory-config, failure-config, test-config and sysrepo db. +# Generate factory-config, failure-config, and test-config files. # -######################################################################## -# The system factory-config, failure-config and test-config are derived -# from default settings snippets, from /usr/share/confd/factory.d, and -# some generated snippets, e.g., hostname (based on base MAC address) -# and number of interfaces. +# These configs are derived from default settings snippets in +# /usr/share/confd/factory.d, and generated snippets (e.g., hostname +# based on base MAC address, number of interfaces). # -# The resulting factory-config is used to create the syrepo db (below) -# {factory} datastore. Hence, the factory-config file must match the -# the YANG models of the active image. +# The sysrepo datastore operations (loading factory defaults, startup +# config, migration) are handled by the confd daemon. ######################################################################## # NOTE: with the Infix defaults, a br2-external can provide a build-time # /etc/factory-config.cfg to override the behavior of this script. # # This applies also for /etc/failure-config.cfg, but we recommend # strongly that you instead provide gen-err-custom, see below. -# -# TODO: Look for statically defined factory-config, based on system's -# product ID, or just custom site-specific factory on /cfg. ######################################################################## -STATUS="" # Log functions -critical() +err() { - logger -i -p user.crit -t bootstrap "$1" 2>/dev/null || echo "$1" + logger -i -p user.err -t gen-config "$1" 2>/dev/null || echo "$1" } -err() +log() { - logger -i -p user.err -t bootstrap "$1" 2>/dev/null || echo "$1" + logger -i -p user.notice -t gen-config "$1" 2>/dev/null || echo "$1" } -# When logging errors, generating /etc/issue* or /etc/banner (SSH) -. /etc/os-release - -# /etc/confdrc controls the behavior or most of the gen-scripts, +# /etc/confdrc controls the behavior of most of the gen-scripts, # customize in an overlay when using Infix as an br2-external. RC=/etc/confdrc if [ "$1" = "-f" ] && [ -f "$2" ]; then @@ -79,25 +69,6 @@ collate() fi } -# Report error on console, syslog, and set login banners for getty + ssh -console_error() -{ - critical "$1" - - # shellcheck disable=SC3037 - /bin/echo -e "\n\n\e[31mCRITICAL BOOTSTRAP ERROR\n$1\e[0m\n" > /dev/console - - [ -z "$STATUS" ] || return - STATUS="CRITICAL ERROR: $1" - - printf "\n$STATUS\n" | tee -a \ - /etc/banner \ - /etc/issue \ - /etc/issue.net \ - >/dev/null - return 0 -} - gen_factory_cfg() { # Fetch defaults, simplifies sort in collate() @@ -145,49 +116,18 @@ gen_test_cfg() collate "$TEST_GEN" "$TEST_CFG" "$TEST_D" } -# Both factory-config and failure-config are generated every boot -# regardless if there is a static /etc/factory-config.cfg or not. +log "Starting up, calling gen_factory_cfg()" gen_factory_cfg +log "Starting up, calling gen_failure_cfg()" gen_failure_cfg if [ -f "/mnt/aux/test-mode" ]; then gen_test_cfg - sysrepoctl -c infix-test -e test-mode-enable -fi - -if [ -n "$TESTING" ]; then - echo "Done." - exit 0 fi -mkdir -p /etc/sysrepo/ -if [ -f "$FACTORY_CFG" ]; then - cp "$FACTORY_CFG" "$INIT_DATA" -else - cp "$FAILURE_CFG" "$INIT_DATA" -fi -rc=$? - -# Ensure 'admin' group users always have access -chgrp wheel "$CFG_PATH_" -chmod g+w "$CFG_PATH_" -# Ensure factory-config has correct syntax -if ! migrate -cq "$INIT_DATA"; then - if migrate -iq -b "${INIT_DATA%.*}.bak" "$INIT_DATA"; then - err "${INIT_DATA}: found and fixed old syntax!" - fi -fi - -if ! sysrepoctl -z "$INIT_DATA"; then - rc=$? - err "Failed loading factory-default datastore" -else - # Clear running-config so we can load/create startup in the next step - temp=$(mktemp) - echo "{}" > "$temp" - sysrepocfg -f json -I"$temp" -d running - rc=$? - rm "$temp" -fi +# Ensure 'admin' group users always have access to /cfg +mkdir -p "$CFG_PATH_" +chgrp wheel "$CFG_PATH_" 2>/dev/null +chmod g+w "$CFG_PATH_" 2>/dev/null -exit $rc +log "All done." diff --git a/src/confd/bin/gen-hardware b/src/confd/bin/gen-hardware index 33000a715..8679bc730 100755 --- a/src/confd/bin/gen-hardware +++ b/src/confd/bin/gen-hardware @@ -6,7 +6,11 @@ if jq -e '.["usb-ports"]' /run/system.json > /dev/null; then else usb_ports="" fi -wifi_radios=$(/usr/libexec/infix/iw.py list 2>/dev/null | jq -r '.[]' || echo "") +if jq -e '.["wifi-radios"]' /run/system.json > /dev/null 2>&1; then + wifi_radios=$(jq -r '.["wifi-radios"][].name' /run/system.json) +else + wifi_radios="" +fi gen_port() @@ -27,11 +31,9 @@ gen_radio() { radio="$1" - # Detect supported bands from iw.py info JSON output - phy_info=$(/usr/libexec/infix/iw.py info "$radio" 2>/dev/null || echo '{"bands":[]}') - # Check if 2.4GHz band exists (band name "2.4GHz") + # Read band info from system.json (probed at boot by 00-probe) + phy_info=$(jq -r --arg r "$radio" '.["wifi-radios"][] | select(.name == $r)' /run/system.json 2>/dev/null || echo '{"bands":[]}') has_2ghz=$(echo "$phy_info" | jq '[.bands[] | select(.name == "2.4GHz")] | length') - # Check if 5GHz band exists (band name "5GHz") has_5ghz=$(echo "$phy_info" | jq '[.bands[] | select(.name == "5GHz")] | length') # Determine band setting diff --git a/src/confd/bin/gen-service b/src/confd/bin/gen-service deleted file mode 100755 index 66d50ed27..000000000 --- a/src/confd/bin/gen-service +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/sh -# Very basic avahi .service generator, works for tcp (http) services -. /etc/os-release - -cmd=$1 -host=$2 -name=$3 -type=$4 -port=$5 -desc=$6 -shift 6 -file="/etc/avahi/services/$name.service" - -case $cmd in - delete) - rm -f "$file" - exit 0 - ;; - update) - if [ ! -f "$file" ]; then - exit 0 - fi - ;; - *) - ;; -esac - -cat <"$file" - - - - $desc - - $type - $port - $host.local - vv=1 - vendor=$(jq -r .vendor /run/system.json) - product=$(jq -r '."product-name"' /run/system.json) - serial=$(jq -r '."serial-number"' /run/system.json) - deviceid=$(jq -r '."mac-address"' /run/system.json) - vn=$VENDOR_NAME - on=$NAME - ov=$VERSION_ID$(for txt in "$@"; do printf "\n %s" "$txt"; done) - - -EOF diff --git a/src/confd/bin/load b/src/confd/bin/load deleted file mode 100755 index f1e157966..000000000 --- a/src/confd/bin/load +++ /dev/null @@ -1,143 +0,0 @@ -#!/bin/sh -# load [-b] -# -# Import a configuration to the sysrepo datastore using `sysrepocfg -Ifile` -# -# If the '-b' option is used we set the Finit condition if -# sysrepocfg returns OK. This to be able to detect and trigger the Infix -# Fail Secure Mode at boot. -# - -banner_append() -{ - printf "\n%s\n" "$*" | tee -a \ - /etc/banner \ - /etc/issue \ - /etc/issue.net \ - >/dev/null - return 0 -} - -# Ensure correct ownership and permissions, in particular after factory reset -# Created by the system, writable by any user in the admin group. -perms() -{ - chown root:wheel "$1" - chmod 0660 "$1" -} - -note() -{ - msg="$*" - logger -I $$ -p user.notice -t load -- "$msg" -} - -err() -{ - msg="$*" - logger -I $$ -p user.error -t load -- "$msg" -} - - -# shellcheck disable=SC1091 -. /etc/confdrc - -sysrepocfg=sysrepocfg -while getopts "t:" opt; do - case ${opt} in - t) - sysrepocfg="$sysrepocfg -t $OPTARG" - ;; - *) - ;; - esac -done -shift $((OPTIND - 1)) - -if [ $# -lt 1 ]; then - err "No configuration file supplied" - exit 1 -fi - - -config=$1 - -if [ -f "/mnt/aux/test-mode" ] && [ "$config" = "startup-config" ]; then - - if [ -f "/mnt/aux/test-override-startup" ]; then - rm -f "/mnt/aux/test-override-startup" - else - note "Test mode detected, switching to test-config" - config="test-config" - fi -fi - -if [ -f "$config" ]; then - fn="$config" -else - if [ -f "$CFG_PATH_/${config}.cfg" ]; then - fn="$CFG_PATH_/${config}.cfg" - else - fn="$SYS_PATH_/${config}.cfg" - fi -fi - -if [ ! -f "$fn" ]; then - case "$config" in - startup-config) - note "startup-config missing, initializing running datastore from factory-config" - $sysrepocfg -C factory-default - rc=$? - note "saving factory-config to $STARTUP_CFG ..." - $sysrepocfg -f json -X"$STARTUP_CFG" - perms "$STARTUP_CFG" - exit $rc - ;; - *) - err "No such file, $fn, aborting!" - exit 1 - ;; - esac -fi - -note "Loading $config ..." -if ! $sysrepocfg -v2 -I"$fn" -f json; then - case "$config" in - startup-config) - err "Failed loading $fn, reverting to Fail Secure mode!" - # On failure to load startup-config the system is in an undefined state - cat <<-EOF >/tmp/factory.json - { - "infix-factory-default:factory-default": {} - } - EOF - - if ! $sysrepocfg -f json -R /tmp/factory.json; then - rm -f /etc/sysrepo/data/*startup* - rm -f /etc/sysrepo/data/*running* - rm -f /dev/shm/sr_* - killall sysrepo-plugind - fi - ;; - failure-config) - err "Failed loading $fn, aborting!" - banner_append "CRITICAL ERROR: Logins are disabled, no credentials available" - initctl -nbq runlevel 9 - ;; - *) - err "Unknown config $config, aborting!" - ;; - esac - - exit 1 -else - note "Success, syncing with startup datastore." - $sysrepocfg -v2 -d startup -C running -fi - -note "Loaded $fn successfully." -if [ "$config" = "failure-config" ]; then - banner_append "ERROR: Corrupt startup-config, system has reverted to default login credentials" -else - perms "$fn" -fi diff --git a/src/confd/configure.ac b/src/confd/configure.ac index 6266615be..bed811611 100644 --- a/src/confd/configure.ac +++ b/src/confd/configure.ac @@ -1,6 +1,6 @@ AC_PREREQ(2.61) # confd version is same as system YANG model version, step on breaking changes -AC_INIT([confd], [1.7], [https://github.com/kernelkit/infix/issues]) +AC_INIT([confd], [1.8], [https://github.com/kernelkit/infix/issues]) AM_INIT_AUTOMAKE(1.11 foreign subdir-objects) AM_SILENT_RULES(yes) @@ -21,6 +21,7 @@ AC_CONFIG_FILES([ share/migrate/1.5/Makefile share/migrate/1.6/Makefile share/migrate/1.7/Makefile + share/migrate/1.8/Makefile yang/Makefile yang/confd/Makefile yang/test-mode/Makefile @@ -77,9 +78,19 @@ PKG_CHECK_MODULES([crypt], [libxcrypt >= 4.4.27]) PKG_CHECK_MODULES([glib], [glib-2.0 >= 2.50 gio-2.0 gio-unix-2.0]) PKG_CHECK_MODULES([jansson], [jansson >= 2.0.0]) PKG_CHECK_MODULES([libite], [libite >= 2.6.1]) -PKG_CHECK_MODULES([sysrepo], [sysrepo >= 2.2.36]) +PKG_CHECK_MODULES([sysrepo], [sysrepo >= 4.2.10]) +PKG_CHECK_MODULES([libyang], [libyang >= 4.2.2]) PKG_CHECK_MODULES([libsrx], [libsrx >= 1.0.0]) +AC_CHECK_HEADER([ev.h], + [saved_LIBS="$LIBS" + AC_CHECK_LIB([ev], [ev_loop_new], + [EV_LIBS="-lev"], + [AC_MSG_ERROR("libev not found")]) + LIBS="$saved_LIBS"], + [AC_MSG_ERROR("ev.h not found")]) +AC_SUBST([EV_LIBS]) + # Control build with automake flags AM_CONDITIONAL(CONTAINERS, [test "x$enable_containers" != "xno"]) diff --git a/src/confd/share/factory.d/10-infix-services.json b/src/confd/share/factory.d/10-infix-services.json index f7b36180f..5b7edadbd 100644 --- a/src/confd/share/factory.d/10-infix-services.json +++ b/src/confd/share/factory.d/10-infix-services.json @@ -22,6 +22,7 @@ "hostkey": [ "genkey" ] }, "infix-services:web": { + "certificate": "gencert", "enabled": true, "console": { "enabled": true diff --git a/src/confd/share/factory.d/10-keystore.json b/src/confd/share/factory.d/10-keystore.json new file mode 100644 index 000000000..e4bcc9beb --- /dev/null +++ b/src/confd/share/factory.d/10-keystore.json @@ -0,0 +1,28 @@ +{ + "ietf-keystore:keystore": { + "asymmetric-keys": { + "asymmetric-key": [ + { + "name": "genkey", + "public-key-format": "infix-crypto-types:ssh-public-key-format", + "public-key": "", + "private-key-format": "infix-crypto-types:rsa-private-key-format", + "cleartext-private-key": "", + "certificates": {} + }, + { + "name": "gencert", + "public-key-format": "infix-crypto-types:x509-public-key-format", + "public-key": "", + "private-key-format": "infix-crypto-types:rsa-private-key-format", + "cleartext-private-key": "", + "certificates": { + "certificate": [ + { "name": "self-signed", "cert-data": "" } + ] + } + } + ] + } + } +} diff --git a/src/confd/share/factory.d/10-netconf-server.json b/src/confd/share/factory.d/10-netconf-server.json index 0dcfdd2cb..1effd768c 100644 --- a/src/confd/share/factory.d/10-netconf-server.json +++ b/src/confd/share/factory.d/10-netconf-server.json @@ -1,18 +1,4 @@ { - "ietf-keystore:keystore": { - "asymmetric-keys": { - "asymmetric-key": [ - { - "name": "genkey", - "public-key-format": "infix-crypto-types:ssh-public-key-format", - "public-key": "", - "private-key-format": "infix-crypto-types:rsa-private-key-format", - "cleartext-private-key": "", - "certificates": {} - } - ] - } - }, "ietf-netconf-server:netconf-server": { "listen": { "endpoints": { diff --git a/src/confd/share/factory.d/Makefile.am b/src/confd/share/factory.d/Makefile.am index 72b403041..4256ab4c3 100644 --- a/src/confd/share/factory.d/Makefile.am +++ b/src/confd/share/factory.d/Makefile.am @@ -1,3 +1,4 @@ factorydir = $(pkgdatadir)/factory.d -dist_factory_DATA = 10-nacm.json 10-netconf-server.json \ +dist_factory_DATA = 10-keystore.json 10-nacm.json \ + 10-netconf-server.json \ 10-infix-services.json 10-system.json diff --git a/src/confd/share/failure.d/10-infix-services.json b/src/confd/share/failure.d/10-infix-services.json index a5ef51029..c34a89de0 100644 --- a/src/confd/share/failure.d/10-infix-services.json +++ b/src/confd/share/failure.d/10-infix-services.json @@ -6,6 +6,7 @@ "enabled": true }, "infix-services:web": { + "certificate": "gencert", "enabled": true, "restconf": { "enabled": true diff --git a/src/confd/share/failure.d/10-keystore.json b/src/confd/share/failure.d/10-keystore.json new file mode 120000 index 000000000..ae64a9eec --- /dev/null +++ b/src/confd/share/failure.d/10-keystore.json @@ -0,0 +1 @@ +../factory.d/10-keystore.json \ No newline at end of file diff --git a/src/confd/share/failure.d/Makefile.am b/src/confd/share/failure.d/Makefile.am index ae981ac3d..3b1f5f7da 100644 --- a/src/confd/share/failure.d/Makefile.am +++ b/src/confd/share/failure.d/Makefile.am @@ -1,4 +1,5 @@ failuredir = $(pkgdatadir)/failure.d -dist_failure_DATA = 10-nacm.json 10-netconf-server.json \ +dist_failure_DATA = 10-keystore.json 10-nacm.json \ + 10-netconf-server.json \ 10-infix-services.json 10-system.json diff --git a/src/confd/share/migrate/1.8/10-keystore-add-gencert.sh b/src/confd/share/migrate/1.8/10-keystore-add-gencert.sh new file mode 100755 index 000000000..7afea14fe --- /dev/null +++ b/src/confd/share/migrate/1.8/10-keystore-add-gencert.sh @@ -0,0 +1,89 @@ +#!/bin/sh +# Migrate self-signed HTTPS certificate from /cfg/ssl/ files into the +# ietf-keystore in startup-config. Previously mkcert generated cert +# and key files on disk; now they are managed as a keystore entry +# called "gencert" alongside the SSH "genkey" entry. +# +# Also adds the "certificate": "gencert" leaf to the web container +# so nginx knows which keystore entry to use for TLS. +# +# After migration, /cfg/ssl/ is removed since cert/key are now stored +# in the keystore and written to /etc/ssl/ by confd at runtime. + +file=$1 +temp=${file}.tmp + +LEGACY_DIR=/cfg/ssl +LEGACY_KEY=$LEGACY_DIR/private/self-signed.key +LEGACY_CRT=$LEGACY_DIR/certs/self-signed.crt + +MKCERT_DIR=/tmp/ssl +MKCERT_KEY=$MKCERT_DIR/self-signed.key +MKCERT_CRT=$MKCERT_DIR/self-signed.crt + +# Read PEM files, strip markers and newlines to get raw base64 +read_pem() { + grep -v -- '-----' "$1" | tr -d '\n' +} + +if [ -f "$LEGACY_KEY" ] && [ -f "$LEGACY_CRT" ]; then + priv_key=$(read_pem "$LEGACY_KEY") + cert_data=$(read_pem "$LEGACY_CRT") +fi + +# Fallback: generate a fresh certificate if legacy files were missing +# or unreadable, same as keystore.c does on first boot. +if [ -z "$priv_key" ] || [ -z "$cert_data" ]; then + /usr/libexec/infix/mkcert + if [ -f "$MKCERT_KEY" ] && [ -f "$MKCERT_CRT" ]; then + priv_key=$(read_pem "$MKCERT_KEY") + cert_data=$(read_pem "$MKCERT_CRT") + rm -rf "$MKCERT_DIR" + fi +fi + +# If we still have no cert data, leave keys empty and let confd +# generate on boot via keystore_update(). +if [ -z "$priv_key" ]; then + priv_key="" + cert_data="" +fi + +jq --arg priv "$priv_key" --arg cert "$cert_data" ' +# Add gencert entry to keystore if not already present +if .["ietf-keystore:keystore"]?."asymmetric-keys"?."asymmetric-key" then + if (.["ietf-keystore:keystore"]."asymmetric-keys"."asymmetric-key" | map(select(.name == "gencert")) | length) == 0 then + .["ietf-keystore:keystore"]."asymmetric-keys"."asymmetric-key" += [{ + "name": "gencert", + "public-key-format": "infix-crypto-types:x509-public-key-format", + "public-key": $cert, + "private-key-format": "infix-crypto-types:rsa-private-key-format", + "cleartext-private-key": $priv, + "certificates": { + "certificate": [{ + "name": "self-signed", + "cert-data": $cert + }] + } + }] + else + . + end +else + . +end | + +# Add certificate reference to web container +if .["infix-services:web"] then + if .["infix-services:web"].certificate then + . + else + .["infix-services:web"].certificate = "gencert" + end +else + . +end +' "$file" > "$temp" && mv "$temp" "$file" + +# Cert/key now live in the keystore, wipe the legacy on-disk copy +rm -rf "$LEGACY_DIR" diff --git a/src/confd/share/migrate/1.8/Makefile.am b/src/confd/share/migrate/1.8/Makefile.am new file mode 100644 index 000000000..5586bcebc --- /dev/null +++ b/src/confd/share/migrate/1.8/Makefile.am @@ -0,0 +1,2 @@ +migratedir = $(pkgdatadir)/migrate/1.8 +dist_migrate_DATA = 10-keystore-add-gencert.sh diff --git a/src/confd/share/migrate/Makefile.am b/src/confd/share/migrate/Makefile.am index 0a7c2c82f..0a5c71ddd 100644 --- a/src/confd/share/migrate/Makefile.am +++ b/src/confd/share/migrate/Makefile.am @@ -1,2 +1,2 @@ -SUBDIRS = 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 +SUBDIRS = 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 migratedir = $(pkgdatadir)/migrate diff --git a/src/confd/share/test.d/10-keystore.json b/src/confd/share/test.d/10-keystore.json new file mode 120000 index 000000000..ae64a9eec --- /dev/null +++ b/src/confd/share/test.d/10-keystore.json @@ -0,0 +1 @@ +../factory.d/10-keystore.json \ No newline at end of file diff --git a/src/confd/share/test.d/Makefile.am b/src/confd/share/test.d/Makefile.am index 89447af7d..66ac915fc 100644 --- a/src/confd/share/test.d/Makefile.am +++ b/src/confd/share/test.d/Makefile.am @@ -1,4 +1,4 @@ testdir = $(pkgdatadir)/test.d -dist_test_DATA = 10-nacm.json 10-netconf-server.json \ - 10-infix-services.json 10-system.json +dist_test_DATA = 10-keystore.json 10-nacm.json 10-netconf-server.json \ + 10-infix-services.json 10-system.json diff --git a/src/confd/src/Makefile.am b/src/confd/src/Makefile.am index 08bf1cb73..f457f1ad5 100644 --- a/src/confd/src/Makefile.am +++ b/src/confd/src/Makefile.am @@ -1,8 +1,15 @@ AM_CPPFLAGS = -D_DEFAULT_SOURCE -D_XOPEN_SOURCE -D_GNU_SOURCE -DYANG_PATH_=\"$(YANGDIR)\" +AM_CPPFLAGS += -DCONFD_VERSION=\"$(PACKAGE_VERSION)\" CLEANFILES = $(rauc_installer_sources) plugindir = $(srpdplugindir) plugin_LTLIBRARIES = confd-plugin.la +sbin_PROGRAMS = confd + +confd_CFLAGS = $(sysrepo_CFLAGS) $(libyang_CFLAGS) $(jansson_CFLAGS) $(libite_CFLAGS) $(libsrx_CFLAGS) +confd_LDADD = $(sysrepo_LIBS) $(libyang_LIBS) $(jansson_LIBS) $(libite_LIBS) $(libsrx_LIBS) $(EV_LIBS) -ldl +confd_SOURCES = main.c + confd_plugin_la_LDFLAGS = -module -avoid-version -shared confd_plugin_la_CFLAGS = \ diff --git a/src/confd/src/core.c b/src/confd/src/core.c index 4acce329a..dd035a3a5 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -99,6 +99,7 @@ static confd_dependency_t handle_dependencies(struct lyd_node **diff, struct lyd dkeys = lydx_get_descendant(*diff, "keystore", "asymmetric-keys", "asymmetric-key", NULL); LYX_LIST_FOR_EACH(dkeys, dkey, "asymmetric-key") { struct ly_set *hostkeys; + struct lyd_node *webcert; uint32_t i; key_name = lydx_get_cattr(dkey, "name"); @@ -116,6 +117,15 @@ static confd_dependency_t handle_dependencies(struct lyd_node **diff, struct lyd } ly_set_free(hostkeys, NULL); } + + webcert = lydx_get_xpathf(config, "/infix-services:web/certificate[.='%s']", key_name); + if (webcert) { + result = add_dependencies(diff, "/infix-services:web/certificate", key_name); + if (result == CONFD_DEP_ERROR) { + ERROR("Failed to add web certificate to diff for key %s", key_name); + return result; + } + } } hostname = lydx_get_xpathf(*diff, "/ietf-system:system/hostname"); @@ -431,11 +441,8 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod if (event == SR_EV_DONE) { /* skip reload in bootstrap, implicit reload in runlevel change */ - if (systemf("runlevel >/dev/null 2>&1")) { - /* trigger any tasks waiting for confd to have applied *-config */ - system("initctl -nbq cond set bootstrap"); + if (systemf("runlevel >/dev/null 2>&1")) return SR_ERR_OK; - } if (systemf("initctl -b reload")) { EMERG("initctl reload: failed applying new configuration!"); @@ -454,10 +461,10 @@ static inline int subscribe_model(char *model, struct confd *confd, int flags) { return sr_module_change_subscribe(confd->session, model, "//.", change_cb, confd, CB_PRIO_PRIMARY, SR_SUBSCR_CHANGE_ALL_MODULES | - SR_SUBSCR_DEFAULT | flags, &confd->sub) || + SR_SUBSCR_NO_THREAD | flags, &confd->sub) || sr_module_change_subscribe(confd->startup, model, "//.", startup_save, NULL, CB_PRIO_PASSIVE, SR_SUBSCR_CHANGE_ALL_MODULES | - SR_SUBSCR_PASSIVE, &confd->sub); + SR_SUBSCR_PASSIVE | SR_SUBSCR_NO_THREAD, &confd->sub); } int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv) @@ -638,6 +645,15 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv) return rc; } +void confd_get_subscriptions(void *priv, sr_subscription_ctx_t **out_sub, + sr_subscription_ctx_t **out_fsub) +{ + struct confd *c = (struct confd *)priv; + + *out_sub = c->sub; + *out_fsub = c->fsub; +} + void sr_plugin_cleanup_cb(sr_session_ctx_t *session, void *priv) { struct confd *ptr = (struct confd *)priv; diff --git a/src/confd/src/core.h b/src/confd/src/core.h index e65f27053..9262fde6a 100644 --- a/src/confd/src/core.h +++ b/src/confd/src/core.h @@ -31,6 +31,11 @@ #include "dagger.h" +#define SSH_HOSTKEYS "/etc/ssh/hostkeys" +#define SSH_HOSTKEYS_NEXT SSH_HOSTKEYS"+" +#define SSL_CERT_DIR "/etc/ssl/certs" +#define SSL_KEY_DIR "/etc/ssl/private" + #define CB_PRIO_PRIMARY 65535 #define CB_PRIO_PASSIVE 65000 @@ -140,7 +145,7 @@ static inline int register_change(sr_session_ctx_t *session, const char *module, int flags, sr_module_change_cb cb, void *arg, sr_subscription_ctx_t **sub) { int rc = sr_module_change_subscribe(session, module, xpath, cb, arg, - CB_PRIO_PRIMARY, flags | SR_SUBSCR_DEFAULT, sub); + CB_PRIO_PRIMARY, flags | SR_SUBSCR_NO_THREAD, sub); if (rc) { ERROR("failed subscribing to changes of %s: %s", xpath, sr_strerror(rc)); return rc; @@ -154,7 +159,7 @@ static inline int register_monitor(sr_session_ctx_t *session, const char *module int flags, sr_module_change_cb cb, void *arg, sr_subscription_ctx_t **sub) { int rc = sr_module_change_subscribe(session, module, xpath, cb, arg, - 0, flags | SR_SUBSCR_PASSIVE, sub); + 0, flags | SR_SUBSCR_PASSIVE | SR_SUBSCR_NO_THREAD, sub); if (rc) { ERROR("failed subscribing to monitor %s: %s", xpath, sr_strerror(rc)); return rc; @@ -167,7 +172,7 @@ static inline int register_oper(sr_session_ctx_t *session, const char *module, c sr_oper_get_items_cb cb, void *arg, int flags, sr_subscription_ctx_t **sub) { int rc = sr_oper_get_subscribe(session, module, xpath, cb, arg, - flags | SR_SUBSCR_DEFAULT, sub); + flags | SR_SUBSCR_NO_THREAD, sub); if (rc) ERROR("failed subscribing to %s oper: %s", xpath, sr_strerror(rc)); return rc; @@ -176,7 +181,7 @@ static inline int register_oper(sr_session_ctx_t *session, const char *module, c static inline int register_rpc(sr_session_ctx_t *session, const char *xpath, sr_rpc_cb cb, void *arg, sr_subscription_ctx_t **sub) { - int rc = sr_rpc_subscribe(session, xpath, cb, arg, 0, SR_SUBSCR_DEFAULT, sub); + int rc = sr_rpc_subscribe(session, xpath, cb, arg, 0, SR_SUBSCR_NO_THREAD, sub); if (rc) ERROR("failed subscribing to %s rpc: %s", xpath, sr_strerror(rc)); return rc; @@ -238,8 +243,6 @@ int hardware_candidate_init(struct confd *confd); int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd); /* keystore.c */ -#define SSH_HOSTKEYS "/etc/ssh/hostkeys" -#define SSH_HOSTKEYS_NEXT SSH_HOSTKEYS"+" int keystore_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd); /* firewall.c */ diff --git a/src/confd/src/keystore.c b/src/confd/src/keystore.c index 6f493e5c0..7cfa93bbf 100644 --- a/src/confd/src/keystore.c +++ b/src/confd/src/keystore.c @@ -12,6 +12,11 @@ #define XPATH_KEYSTORE_ASYM "/ietf-keystore:keystore/asymmetric-keys" #define XPATH_KEYSTORE_SYM "/ietf-keystore:keystore/symmetric-keys" +#define SSH_PRIVATE_KEY "/tmp/ssh.key" +#define SSH_PUBLIC_KEY "/tmp/ssh.pub" +#define TLS_TMPDIR "/tmp/ssl" +#define TLS_PRIVATE_KEY TLS_TMPDIR "/self-signed.key" +#define TLS_CERTIFICATE TLS_TMPDIR "/self-signed.crt" /* return file size */ static size_t filesz(const char *fn) @@ -68,6 +73,7 @@ static int gen_hostkey(const char *name, struct lyd_node *change) rc = SR_ERR_INTERNAL; } + AUDIT("Installing SSH host key \"%s\".", name); if (systemf("/usr/libexec/infix/mksshkey %s %s %s %s", name, SSH_HOSTKEYS_NEXT, public_key, private_key)) rc = SR_ERR_INTERNAL; @@ -75,6 +81,63 @@ static int gen_hostkey(const char *name, struct lyd_node *change) return rc; } +static int gen_webcert(const char *name, struct lyd_node *change) +{ + const char *private_key, *cert_data, *certname; + struct lyd_node *certs, *cert; + FILE *fp; + + erase("/run/finit/cond/usr/mkcert"); + + private_key = lydx_get_cattr(change, "cleartext-private-key"); + if (!private_key || !*private_key) { + ERROR("Cannot find private key for \"%s\"", name); + return SR_ERR_OK; + } + + certs = lydx_get_descendant(lyd_child(change), "certificates", "certificate", NULL); + if (!certs) { + ERROR("Cannot find any certificates for \"%s\"", name); + return SR_ERR_OK; + } + + cert = certs; /* Use first certificate */ + + certname = lydx_get_cattr(cert, "name"); + if (!certname || !*certname) { + ERROR("Cannot find certificate name for \"%s\"", name); + return SR_ERR_OK; + } + + cert_data = lydx_get_cattr(cert, "cert-data"); + if (!cert_data || !*cert_data) { + ERROR("Cannot find certificate data \"%s\"", name); + return SR_ERR_OK; + } + + AUDIT("Installing HTTPS %s certificate \"%s\"", name, certname); + fp = fopenf("w", "%s/%s.key", SSL_KEY_DIR, certname); + if (!fp) { + ERRNO("Failed creating key file for \"%s\"", certname); + return SR_ERR_INTERNAL; + } + fprintf(fp, "-----BEGIN RSA PRIVATE KEY-----\n%s\n-----END RSA PRIVATE KEY-----\n", private_key); + fclose(fp); + systemf("chmod 600 %s/%s.key", SSL_KEY_DIR, certname); + + fp = fopenf("w", "%s/%s.crt", SSL_CERT_DIR, certname); + if (!fp) { + ERRNO("Failed creating crt file for \"%s\"", certname); + return SR_ERR_INTERNAL; + } + fprintf(fp, "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n", cert_data); + fclose(fp); + + symlink("/run/finit/cond/reconf", "/run/finit/cond/usr/mkcert"); + + return SR_ERR_OK; +} + static int keystore_update(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff) { const char *xpath = "/ietf-keystore:keystore/asymmetric-keys/asymmetric-key"; @@ -163,6 +226,84 @@ static int keystore_update(sr_session_ctx_t *session, struct lyd_node *config, s free(private_key_format); } + if (list) + sr_free_values(list, count); + + /* Second pass: generate X.509 certificates for TLS */ + list = NULL; + count = 0; + rc = sr_get_items(session, xpath, 0, 0, &list, &count); + if (rc != SR_ERR_OK) + return 0; + + for (size_t i = 0; i < count; i++) { + char *name = srx_get_str(session, "%s/name", list[i].xpath); + char *public_key_format = NULL, *private_key_format = NULL; + char *pub_key = NULL, *priv_key = NULL, *cert = NULL; + sr_val_t *entry = &list[i]; + + if (srx_isset(session, "%s/cleartext-private-key", entry->xpath) || + srx_isset(session, "%s/public-key", entry->xpath)) + goto next_x509; + + public_key_format = srx_get_str(session, "%s/public-key-format", entry->xpath); + if (!public_key_format) + goto next_x509; + + private_key_format = srx_get_str(session, "%s/private-key-format", entry->xpath); + if (!private_key_format) + goto next_x509; + + if (strcmp(private_key_format, "infix-crypto-types:rsa-private-key-format") || + strcmp(public_key_format, "infix-crypto-types:x509-public-key-format")) + goto next_x509; + + NOTE("X.509 certificate (%s) does not exist, generating...", name); + if (systemf("/usr/libexec/infix/mkcert")) { + ERROR("Failed generating X.509 certificate for %s", name); + goto next_x509; + } + + priv_key = filerd(TLS_PRIVATE_KEY, filesz(TLS_PRIVATE_KEY)); + if (!priv_key) + goto next_x509; + + pub_key = filerd(TLS_CERTIFICATE, filesz(TLS_CERTIFICATE)); + if (!pub_key) + goto next_x509; + + /* Use cert data also for public-key (X.509 SubjectPublicKeyInfo) */ + rc = srx_set_str(session, priv_key, 0, "%s/cleartext-private-key", entry->xpath); + if (rc) { + ERROR("Failed setting private key for %s... rc: %d", name, rc); + goto next_x509; + } + + rc = srx_set_str(session, pub_key, 0, "%s/public-key", entry->xpath); + if (rc != SR_ERR_OK) { + ERROR("Failed setting public key for %s... rc: %d", name, rc); + goto next_x509; + } + + cert = filerd(TLS_CERTIFICATE, filesz(TLS_CERTIFICATE)); + if (cert) { + rc = srx_set_str(session, cert, 0, + "%s/certificates/certificate[name='self-signed']/cert-data", + entry->xpath); + if (rc) + ERROR("Failed setting cert-data for %s... rc: %d", name, rc); + } + + next_x509: + rmrf(TLS_TMPDIR); + free(public_key_format); + free(private_key_format); + free(priv_key); + free(pub_key); + free(cert); + free(name); + } + if (list) sr_free_values(list, count); @@ -181,8 +322,7 @@ int keystore_change(sr_session_ctx_t *session, struct lyd_node *config, struct l switch (event) { case SR_EV_UPDATE: - rc = keystore_update(session, config, diff); - break; + return keystore_update(session, config, diff); case SR_EV_CHANGE: if (diff && lydx_find_xpathf(diff, XPATH_KEYSTORE_SYM)) rc = interfaces_validate_keys(session, config); @@ -209,21 +349,13 @@ int keystore_change(sr_session_ctx_t *session, struct lyd_node *config, struct l changes = lydx_get_descendant(config, "keystore", "asymmetric-keys", "asymmetric-key", NULL); LYX_LIST_FOR_EACH(changes, change, "asymmetric-key") { const char *name = lydx_get_cattr(change, "name"); - const char *type; - - type = lydx_get_cattr(change, "private-key-format"); - if (strcmp(type, "infix-crypto-types:rsa-private-key-format")) { - INFO("Private key %s is not of SSH type (%s)", name, type); - continue; - } - - type = lydx_get_cattr(change, "public-key-format"); - if (strcmp(type, "infix-crypto-types:ssh-public-key-format")) { - INFO("Public key %s is not of SSH type (%s)", name, type); - continue; - } + const char *pubfmt; - gen_hostkey(name, change); + pubfmt = lydx_get_cattr(change, "public-key-format"); + if (!strcmp(pubfmt, "infix-crypto-types:ssh-public-key-format")) + gen_hostkey(name, change); + else if (!strcmp(pubfmt, "infix-crypto-types:x509-public-key-format")) + gen_webcert(name, change); } return rc; diff --git a/src/confd/src/main.c b/src/confd/src/main.c new file mode 100644 index 000000000..26566f5f7 --- /dev/null +++ b/src/confd/src/main.c @@ -0,0 +1,856 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/* + * confd - Infix configuration daemon + * + * Replaces sysrepo-plugind + bootstrap + load with a single binary. + * One sr_connect(), all datastore operations in-process, then load + * plugins and enter the event loop. + * + * Copyright (c) 2018 - 2021 Deutsche Telekom AG. + * Copyright (c) 2018 - 2021 CESNET, z.s.p.o. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +/* Maximum number of sysrepo event pipe file descriptors across all plugins */ +#define MAX_EVENT_FDS 64 + +/* Callback type names from sysrepo plugin API */ +#define SRP_INIT_CB "sr_plugin_init_cb" +#define SRP_CLEANUP_CB "sr_plugin_cleanup_cb" + +#ifndef CONFD_VERSION +#define CONFD_VERSION PACKAGE_VERSION +#endif + +#ifndef SRPD_PLUGINS_PATH +#define SRPD_PLUGINS_PATH "/usr/lib/sysrepo-plugind/plugins" +#endif + +struct plugin { + void *handle; + char *name; + int (*init_cb)(sr_session_ctx_t *session, void **private_data); + void (*cleanup_cb)(sr_session_ctx_t *session, void *private_data); + void (*get_subs)(void *priv, sr_subscription_ctx_t **sub, sr_subscription_ctx_t **fsub); + void *private_data; + sr_subscription_ctx_t *sub; + sr_subscription_ctx_t *fsub; + int initialized; +}; + +static sig_atomic_t pump_running = 1; +int debug = 0; + + +/* Finit style progress output on console */ +static void conout(int rc, const char *fmt, ...) +{ + const char *sta = "%s\e[1m[\e[1;%dm%s\e[0m\e[1m]\e[0m %s"; + const char *msg[] = { " OK ", "FAIL", "WARN", " ⋯ " }; + const char *cr = rc == 3 ? "" : "\r"; + const int col[] = { 32, 31, 33, 33 }; + char buf[80]; + va_list ap; + + snprintf(buf, sizeof(buf), sta, cr, col[rc], msg[rc], fmt); + va_start(ap, fmt); + vfprintf(stderr, buf, ap); + va_end(ap); +} + +static void version_print(void) +{ + printf("confd - Infix configuration daemon v%s, compiled with libsysrepo v%s\n\n", + CONFD_VERSION, SR_VERSION); +} + +static void help_print(void) +{ + printf("Usage:\n" + " confd [-h] [-V] [-v ] [-f]\n" + " [-F factory-config] [-S startup-config] [-E failure-config]\n" + " [-t timeout]\n" + "\n" + "Options:\n" + " -h, --help Prints usage help.\n" + " -V, --version Prints version information.\n" + " -v, --verbosity \n" + " Change verbosity to a level (none, error, warning, info, debug).\n" + " -f, --fatal-plugin-fail\n" + " Terminate if any plugin initialization fails.\n" + " -F, --factory-config \n" + " Factory default config file (default: /etc/factory-config.cfg).\n" + " -S, --startup-config \n" + " Startup config file (default: /cfg/startup-config.cfg).\n" + " -E, --failure-config \n" + " Failure fallback config file (default: /etc/failure-config.cfg).\n" + " -t, --timeout Sysrepo operation timeout in seconds (default: 60).\n" + "\n" + "Environment variable $SRPD_PLUGINS_PATH overwrites the default plugins directory.\n" + "\n"); +} + +/* libev callbacks for steady-state operation */ +static void signal_cb(struct ev_loop *loop, struct ev_signal *w, int revents) +{ + (void)revents; + (void)w; + ev_break(loop, EVBREAK_ALL); +} + +static void sr_event_cb(struct ev_loop *loop, struct ev_io *w, int revents) +{ + (void)loop; + (void)revents; + sr_subscription_process_events(w->data, NULL, NULL); +} + +/* + * Temporary event pump process for bootstrap. + * + * With SR_SUBSCR_NO_THREAD, sysrepo writes events to a pipe and waits + * for the application to call sr_subscription_process_events(). During + * bootstrap, sr_replace_config() blocks waiting for callbacks — this + * child process ensures those callbacks get dispatched. + */ +static void pump_sigterm(int sig) +{ + (void)sig; + pump_running = 0; +} + +static void event_pump(struct plugin *plugins, int plugin_count) +{ + sr_subscription_ctx_t *subs[MAX_EVENT_FDS]; + struct pollfd fds[MAX_EVENT_FDS]; + int nfds = 0; + + for (int i = 0; i < plugin_count; i++) { + struct plugin *p = &plugins[i]; + + if (p->sub && sr_get_event_pipe(p->sub, &fds[nfds].fd) == SR_ERR_OK) { + fds[nfds].events = POLLIN; + subs[nfds] = p->sub; + nfds++; + } + if (p->fsub && sr_get_event_pipe(p->fsub, &fds[nfds].fd) == SR_ERR_OK) { + fds[nfds].events = POLLIN; + subs[nfds] = p->fsub; + nfds++; + } + } + + signal(SIGTERM, pump_sigterm); + + while (pump_running) { + if (poll(fds, nfds, 100) > 0) { + for (int i = 0; i < nfds; i++) + if (fds[i].revents & POLLIN) + sr_subscription_process_events(subs[i], NULL, NULL); + } + } + + _exit(0); +} + +static void quiet_now(void) +{ + int fd; + + fd = open("/dev/null", O_RDWR, 0); + if (fd != -1) { + dup2(fd, STDIN_FILENO); + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + close(fd); + } +} + +/* + * Plugin loading -- external .so files only (no internal plugins) + */ +static size_t path_len_no_ext(const char *path) +{ + const char *dot; + + dot = strrchr(path, '.'); + if (!dot || dot == path) + return 0; + + return dot - path; +} + +static int load_plugins(struct plugin **plugins, int *plugin_count) +{ + const char *plugins_dir; + struct dirent *ent; + struct plugin *plugin; + void *mem, *handle; + size_t name_len; + int rc = 0; + char *path; + DIR *dir; + + *plugins = NULL; + *plugin_count = 0; + + plugins_dir = getenv("SRPD_PLUGINS_PATH"); + if (!plugins_dir) + plugins_dir = SRPD_PLUGINS_PATH; + + dir = opendir(plugins_dir); + if (!dir) { + ERRNO("Opening \"%s\" directory failed", plugins_dir); + return -1; + } + + while ((ent = readdir(dir))) { + if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) + continue; + + if (asprintf(&path, "%s/%s", plugins_dir, ent->d_name) == -1) { + ERRNO("asprintf() failed"); + rc = -1; + break; + } + handle = dlopen(path, RTLD_LAZY); + if (!handle) { + ERROR("Opening plugin \"%s\" failed: %s", path, dlerror()); + free(path); + rc = -1; + break; + } + free(path); + + mem = realloc(*plugins, (*plugin_count + 1) * sizeof(**plugins)); + if (!mem) { + ERRNO("realloc() failed"); + dlclose(handle); + rc = -1; + break; + } + *plugins = mem; + plugin = &(*plugins)[*plugin_count]; + memset(plugin, 0, sizeof(*plugin)); + + *(void **)&plugin->init_cb = dlsym(handle, SRP_INIT_CB); + if (!plugin->init_cb) { + ERROR("Failed to find \"%s\" in plugin \"%s\".", SRP_INIT_CB, ent->d_name); + dlclose(handle); + rc = -1; + break; + } + + *(void **)&plugin->cleanup_cb = dlsym(handle, SRP_CLEANUP_CB); + if (!plugin->cleanup_cb) { + ERROR("Failed to find \"%s\" in plugin \"%s\".", SRP_CLEANUP_CB, ent->d_name); + dlclose(handle); + rc = -1; + break; + } + + /* Optional: allows main to collect subscription contexts */ + *(void **)&plugin->get_subs = dlsym(handle, "confd_get_subscriptions"); + + plugin->handle = handle; + + name_len = path_len_no_ext(ent->d_name); + if (name_len == 0) { + ERROR("Wrong filename \"%s\".", ent->d_name); + dlclose(handle); + rc = -1; + break; + } + + plugin->name = strndup(ent->d_name, name_len); + if (!plugin->name) { + ERRNO("strndup() failed"); + dlclose(handle); + rc = -1; + break; + } + + ++(*plugin_count); + } + + closedir(dir); + return rc; +} + +/* + * Wipe stale sysrepo SHM files for a clean slate every boot. + */ +static void wipe_sysrepo_shm(void) +{ + glob_t gl; + + if (glob("/dev/shm/sr_*", 0, NULL, &gl) == 0) { + for (size_t i = 0; i < gl.gl_pathc; i++) + unlink(gl.gl_pathv[i]); + globfree(&gl); + } +} + +const char *basenm(const char *path) +{ + const char *slash; + + if (!path) + return NULL; + + slash = strrchr(path, '/'); + if (slash) + return slash[1] ? slash + 1 : NULL; + + return path; +} + +/* + * Append error message to login banners. + */ +static void banner_append(const char *msg) +{ + const char *files[] = { + "/etc/banner", + "/etc/issue", + "/etc/issue.net", + }; + + for (size_t i = 0; i < sizeof(files) / sizeof(files[0]); i++) { + FILE *fp = fopen(files[i], "a"); + + if (fp) { + fprintf(fp, "\n%s\n", msg); + fclose(fp); + } + } +} + +/* + * Smart migration: only fork+exec the migrate script if the version + * in the config file doesn't match the current confd version. + */ +static int maybe_migrate(const char *path) +{ + const char *backup_dir = "/cfg/backup"; + json_t *root, *meta, *ver; + const char *file_ver; + char backup[256]; + int rc; + + root = json_load_file(path, 0, NULL); + if (!root) + return -1; + + meta = json_object_get(root, "infix-meta:meta"); + ver = meta ? json_object_get(meta, "version") : NULL; + file_ver = ver ? json_string_value(ver) : "0.0"; + + if (!strcmp(file_ver, CONFD_VERSION)) { + json_decref(root); + return 0; + } + json_decref(root); + + NOTE("%s config version %s vs confd %s, migrating ...", path, file_ver, CONFD_VERSION); + + mkpath(backup_dir, 0770); + chown(backup_dir, 0, 10); /* root:wheel */ + + snprintf(backup, sizeof(backup), "%s/%s", backup_dir, basenm(path)); + rc = systemf("migrate -i -b \"%s\" \"%s\"", backup, path); + if (rc) + ERROR("Migration of %s failed (rc=%d)", path, rc); + + return rc; +} + +/* + * Load a JSON config file into the running datastore. + * Mirrors what sysrepocfg -I does: lyd_parse_data() + sr_replace_config(). + */ +static int load_config(sr_conn_ctx_t *conn, sr_session_ctx_t *sess, + const char *path, uint32_t timeout_ms) +{ + const struct ly_ctx *ly_ctx; + struct lyd_node *data = NULL; + struct ly_in *in = NULL; + LY_ERR lyrc; + int r; + + ly_ctx = sr_acquire_context(conn); + + lyrc = ly_in_new_filepath(path, 0, &in); + if (lyrc == LY_EINVAL) { + /* empty file */ + char *empty = strdup(""); + + ly_in_new_memory(empty, &in); + } else if (lyrc) { + ERROR("Failed to open \"%s\" for reading", path); + sr_release_context(conn); + return -1; + } + + lyrc = lyd_parse_data(ly_ctx, NULL, in, LYD_JSON, + LYD_PARSE_NO_STATE | LYD_PARSE_ONLY | LYD_PARSE_STRICT, 0, &data); + ly_in_free(in, 1); + + if (lyrc) { + ERROR("Parsing %s failed", path); + sr_release_context(conn); + return -1; + } + + sr_release_context(conn); + + r = sr_replace_config(sess, NULL, data, timeout_ms); + if (r != SR_ERR_OK) { + ERROR("sr_replace_config failed: %s", sr_strerror(r)); + return -1; + } + + return 0; +} + +/* + * Export running datastore to a JSON file. + */ +static int export_running(sr_session_ctx_t *sess, const char *path, uint32_t timeout_ms) +{ + sr_data_t *data = NULL; + FILE *fp; + int r; + + r = sr_get_data(sess, "/*", 0, timeout_ms, 0, &data); + if (r != SR_ERR_OK) { + ERROR("sr_get_data failed: %s", sr_strerror(r)); + return -1; + } + + umask(0006); + fp = fopen(path, "w"); + if (!fp) { + ERRNO("Failed to open %s for writing", path); + sr_release_data(data); + return -1; + } + + lyd_print_file(fp, data ? data->tree : NULL, LYD_JSON, LYD_PRINT_SIBLINGS); + fclose(fp); + sr_release_data(data); + + chown(path, 0, 10); /* root:wheel for admin group access */ + + return 0; +} + +/* + * Handle startup-config load failure: revert to factory-default, + * then load failure-config, set error banners. + */ +static void handle_startup_failure(sr_session_ctx_t *sess, const char *failure_path, + sr_conn_ctx_t *conn, uint32_t timeout_ms) +{ + int r; + + ERROR("Failed loading startup-config, reverting to Fail Secure mode!"); + + /* Reset to factory-default */ + r = sr_copy_config(sess, NULL, SR_DS_FACTORY_DEFAULT, timeout_ms); + if (r != SR_ERR_OK) { + ERROR("sr_copy_config(factory-default) failed: %s", sr_strerror(r)); + /* Nuclear option: wipe everything */ + systemf("rm -f /etc/sysrepo/data/*startup* /etc/sysrepo/data/*running* /dev/shm/sr_*"); + return; + } + + /* Load failure-config on top */ + if (fexist(failure_path)) { + if (load_config(conn, sess, failure_path, timeout_ms)) { + ERROR("Failed loading failure-config, aborting!"); + banner_append("CRITICAL ERROR: Logins are disabled, no credentials available"); + systemf("initctl -nbq runlevel 9"); + return; + } + } + + banner_append("ERROR: Corrupt startup-config, system has reverted to default login credentials"); +} + +/* + * Enable test-mode if the test-mode marker exists. + */ +static void maybe_enable_test_mode(void) +{ + if (fexist("/mnt/aux/test-mode")) { + int rc; + + conout(3, "Enabling test mode"); + rc = systemf("sysrepoctl -c infix-test -e test-mode-enable"); + conout(rc ? 1 : 0, "\n"); + } +} + +/* + * Determine which config to load: + * - test-mode (unless override exists) + * - startup-config + * - first-boot from factory + */ +static int bootstrap_config(sr_conn_ctx_t *conn, sr_session_ctx_t *sess, + const char *factory_path, const char *startup_path, + const char *failure_path, const char *test_path, + uint32_t timeout_ms) +{ + const char *config_path; + int r; + + /* Test mode support */ + if (fexist("/mnt/aux/test-mode")) { + if (fexist("/mnt/aux/test-override-startup")) { + unlink("/mnt/aux/test-override-startup"); + config_path = startup_path; + } else { + NOTE("Test mode detected, switching to test-config"); + config_path = test_path; + } + } else { + config_path = startup_path; + } + + if (fexist(config_path)) { + /* Run migration if needed */ + maybe_migrate(config_path); + + /* Load startup (or test) config */ + NOTE("Loading %s ...", config_path); + if (load_config(conn, sess, config_path, timeout_ms)) { + handle_startup_failure(sess, failure_path, conn, timeout_ms); + return 0; /* continue running even in fail-secure */ + } + + NOTE("Loaded %s successfully, syncing startup datastore.", config_path); + sr_session_switch_ds(sess, SR_DS_STARTUP); + r = sr_copy_config(sess, NULL, SR_DS_RUNNING, timeout_ms); + sr_session_switch_ds(sess, SR_DS_RUNNING); + if (r != SR_ERR_OK) + WARN("Failed to sync startup datastore: %s", sr_strerror(r)); + + return 0; + } + + /* First boot: no startup-config, initialize from factory */ + NOTE("startup-config missing, initializing from factory-config"); + + r = sr_copy_config(sess, NULL, SR_DS_FACTORY_DEFAULT, timeout_ms); + if (r != SR_ERR_OK) { + ERROR("sr_copy_config(factory-default) failed: %s", sr_strerror(r)); + return -1; + } + + /* Export running → startup file */ + if (export_running(sess, startup_path, timeout_ms)) + WARN("Failed to export running to %s", startup_path); + + return 0; +} + +int main(int argc, char **argv) +{ + const char *failure_path = "/etc/failure-config.cfg"; + const char *startup_path = "/cfg/startup-config.cfg"; + const char *factory_path = "/etc/factory-config.cfg"; + const char *test_path = "/etc/test-config.cfg"; + int log_opts = LOG_PID | LOG_NDELAY; + int rc = EXIT_FAILURE, opt, i, r; + sr_session_ctx_t *sess = NULL; + struct plugin *plugins = NULL; + sr_conn_ctx_t *conn = NULL; + int log_level = LOG_ERR; + pid_t gen_pid, pump_pid; + uint32_t timeout_s = 60; + int plugin_count = 0; + int fatal_fail = 0; + uint32_t timeout_ms; + int status; + + struct option options[] = { + {"help", no_argument, NULL, 'h'}, + {"version", no_argument, NULL, 'V'}, + {"verbosity", required_argument, NULL, 'v'}, + {"fatal-plugin-fail", no_argument, NULL, 'f'}, + {"factory-config", required_argument, NULL, 'F'}, + {"startup-config", required_argument, NULL, 'S'}, + {"failure-config", required_argument, NULL, 'E'}, + {"timeout", required_argument, NULL, 't'}, + {NULL, 0, NULL, 0}, + }; + + opterr = 0; + while ((opt = getopt_long(argc, argv, "hVv:fF:S:E:t:", options, NULL)) != -1) { + switch (opt) { + case 'h': + version_print(); + help_print(); + return EXIT_SUCCESS; + case 'V': + version_print(); + return EXIT_SUCCESS; + case 'v': + if (!strcmp(optarg, "none")) + log_level = LOG_EMERG; + else if (!strcmp(optarg, "error")) + log_level = LOG_ERR; + else if (!strcmp(optarg, "warning")) + log_level = LOG_WARNING; + else if (!strcmp(optarg, "info")) + log_level = LOG_NOTICE; + else if (!strcmp(optarg, "debug")) + log_level = LOG_DEBUG; + else { + fprintf(stderr, "confd error: Invalid verbosity \"%s\"\n", optarg); + return EXIT_FAILURE; + } + break; + case 'f': + fatal_fail = 1; + break; + case 'F': + factory_path = optarg; + break; + case 'S': + startup_path = optarg; + break; + case 'E': + failure_path = optarg; + break; + case 't': + timeout_s = (uint32_t)atoi(optarg); + break; + default: + fprintf(stderr, "confd error: Invalid option or missing argument: -%c\n", optopt); + return EXIT_FAILURE; + } + } + + if (optind < argc) { + fprintf(stderr, "confd error: Redundant parameters\n"); + return EXIT_FAILURE; + } + + timeout_ms = timeout_s * 1000; + + nice(-20); + signal(SIGPIPE, SIG_IGN); + + if (getenv("DEBUG")) { + log_opts |= LOG_PERROR; + debug = 1; + } + openlog("confd", log_opts, LOG_DAEMON); + setlogmask(LOG_UPTO(log_level)); + + pidfile(NULL); + + /* Load plugins from disk (dlopen) */ + if (load_plugins(&plugins, &plugin_count)) + ERROR("load_plugins failed (continuing)"); + + /* Start gen-config in parallel — child is reaped before we need the result */ + conout(3, "Generating factory-config and failure-config"); + gen_pid = fork(); + if (gen_pid < 0) { + ERRNO("Failed to fork gen-config"); + conout(1, "\n"); + goto cleanup; + } + if (gen_pid == 0) + _exit(systemf("/usr/libexec/confd/gen-config")); + + /* Phase 1: Wipe stale SHM for a clean slate */ + wipe_sysrepo_shm(); + + /* Phase 2: Connect to sysrepo (rebuilds SHM from installed YANG modules) */ + r = sr_connect(0, &conn); + if (r != SR_ERR_OK) { + ERROR("Failed to connect: %s", sr_strerror(r)); + goto cleanup; + } + + /* Phase 3: Wait for gen-config to finish */ + waitpid(gen_pid, &status, 0); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + ERROR("gen-config failed (status=%d)", status); + conout(1, "\n"); + goto cleanup; + } + conout(0, "\n"); + + /* Phase 4: Install factory defaults into all datastores */ + NOTE("Loading factory-default datastore from %s ...", factory_path); + conout(3, "Loading factory-default datastore"); + r = sr_install_factory_config(conn, factory_path); + if (r != SR_ERR_OK) { + ERROR("sr_install_factory_config failed: %s", sr_strerror(r)); + conout(1, "\n"); + goto cleanup; + } + conout(0, "\n"); + + /* Phase 5: Start running-datastore session */ + r = sr_session_start(conn, SR_DS_RUNNING, &sess); + if (r != SR_ERR_OK) { + ERROR("Failed to start new session: %s", sr_strerror(r)); + goto cleanup; + } + + /* Phase 6: Clear running datastore so plugin init sees an empty + * tree. This matches the original bootstrap flow where running + * was cleared with '{}' before sysrepo-plugind started. When we + * later load startup-config, the diff will be all-create which is + * what the plugin callbacks expect. */ + r = sr_replace_config(sess, NULL, NULL, timeout_ms); + if (r != SR_ERR_OK) { + ERROR("Failed to clear running datastore: %s", sr_strerror(r)); + goto cleanup; + } + + /* Enable test-mode YANG feature if needed */ + maybe_enable_test_mode(); + + /* Phase 7: Initialize plugins (subscribe to YANG module changes) */ + conout(3, "Loading confd plugins"); + for (i = 0; i < plugin_count; i++) { + r = plugins[i].init_cb(sess, &plugins[i].private_data); + if (r) { + ERROR("Plugin \"%s\" initialization failed (%s).", plugins[i].name, sr_strerror(r)); + if (fatal_fail) { + conout(1, "\n"); + goto cleanup; + } + } else { + NOTE("Plugin \"%s\" initialized.", plugins[i].name); + plugins[i].initialized = 1; + } + } + conout(0, "\n"); + + /* Phase 8: Collect subscription contexts from plugins */ + for (i = 0; i < plugin_count; i++) { + if (plugins[i].initialized && plugins[i].get_subs) + plugins[i].get_subs(plugins[i].private_data, &plugins[i].sub, &plugins[i].fsub); + } + + /* Phase 9: Fork event pump process for bootstrap. + * With SR_SUBSCR_NO_THREAD, sr_replace_config() blocks waiting + * for callbacks. The pump process processes those events. */ + pump_pid = fork(); + if (pump_pid < 0) { + ERRNO("Failed to fork event pump"); + goto cleanup; + } + if (pump_pid == 0) + event_pump(plugins, plugin_count); + + /* Phase 10: Load startup config -- plugins are now subscribed, so + * sr_replace_config() will trigger their change callbacks. + * The event pump process processes those callbacks. */ + conout(3, "Loading startup-config"); + if (bootstrap_config(conn, sess, factory_path, startup_path, + failure_path, test_path, timeout_ms)) { + kill(pump_pid, SIGTERM); + waitpid(pump_pid, NULL, 0); + conout(1, "\n"); + goto cleanup; + } + conout(0, "\n"); + + /* Phase 11: Stop event pump — bootstrap is done */ + kill(pump_pid, SIGTERM); + waitpid(pump_pid, NULL, 0); + + /* No more progress to show, go to quiet daemon mode */ + quiet_now(); + + /* Signal that bootstrap is complete (dbus, resolvconf depend on this) */ + symlink("/run/finit/cond/reconf", "/run/finit/cond/usr/bootstrap"); + + /* Phase 12: Steady-state — libev event loop */ + { + struct ev_signal sigterm_w, sigint_w, sighup_w, sigquit_w; + struct ev_io io_watchers[MAX_EVENT_FDS]; + struct ev_loop *loop = EV_DEFAULT; + int nio = 0; + + ev_signal_init(&sigterm_w, signal_cb, SIGTERM); + ev_signal_init(&sigint_w, signal_cb, SIGINT); + ev_signal_init(&sighup_w, signal_cb, SIGHUP); + ev_signal_init(&sigquit_w, signal_cb, SIGQUIT); + ev_signal_start(loop, &sigterm_w); + ev_signal_start(loop, &sigint_w); + ev_signal_start(loop, &sighup_w); + ev_signal_start(loop, &sigquit_w); + + for (i = 0; i < plugin_count; i++) { + int fd; + + if (plugins[i].sub && sr_get_event_pipe(plugins[i].sub, &fd) == SR_ERR_OK) { + ev_io_init(&io_watchers[nio], sr_event_cb, fd, EV_READ); + io_watchers[nio].data = plugins[i].sub; + ev_io_start(loop, &io_watchers[nio]); + nio++; + } + if (plugins[i].fsub && sr_get_event_pipe(plugins[i].fsub, &fd) == SR_ERR_OK) { + ev_io_init(&io_watchers[nio], sr_event_cb, fd, EV_READ); + io_watchers[nio].data = plugins[i].fsub; + ev_io_start(loop, &io_watchers[nio]); + nio++; + } + } + + ev_run(loop, 0); + ev_loop_destroy(loop); + } + + rc = EXIT_SUCCESS; + +cleanup: + while (plugin_count > 0) { + if (plugins[plugin_count - 1].initialized) + plugins[plugin_count - 1].cleanup_cb(sess, plugins[plugin_count - 1].private_data); + if (plugins[plugin_count - 1].handle) + dlclose(plugins[plugin_count - 1].handle); + free(plugins[plugin_count - 1].name); + --plugin_count; + } + free(plugins); + + sr_disconnect(conn); + return rc; +} diff --git a/src/confd/src/services.c b/src/confd/src/services.c index 97e126997..173238e6c 100644 --- a/src/confd/src/services.c +++ b/src/confd/src/services.c @@ -19,9 +19,14 @@ #define GENERATE_ENUM(ENUM) ENUM, #define GENERATE_STRING(STRING) #STRING, +#define NGINX_SSL_CONF "/etc/nginx/ssl.conf" +#define AVAHI_SVC_PATH "/etc/avahi/services" + #define LLDP_CONFIG "/etc/lldpd.d/confd.conf" #define LLDP_CONFIG_NEXT LLDP_CONFIG"+" +enum mdns_cmd { MDNS_ADD, MDNS_DELETE, MDNS_UPDATE }; + #define FOREACH_SVC(SVC) \ SVC(none) \ SVC(ssh) \ @@ -68,6 +73,13 @@ struct mdns_svc { { ssh, "ssh", "_ssh._tcp", 22, "Secure shell command line interface (CLI)", NULL }, }; +static const char *jgets(json_t *obj, const char *key) +{ + json_t *val = json_object_get(obj, key); + + return val ? json_string_value(val) : NULL; +} + /* * On hostname changes we need to update the mDNS records, in particular * the ones advertising an adminurl (standarized by Apple), because they @@ -77,27 +89,78 @@ struct mdns_svc { * adminurl to include 'admin@%s.local' to pre-populate the default * username in the login dialog. */ -static int mdns_records(const char *cmd, svc type) +static int mdns_records(int cmd, svc type) { char hostname[MAXHOSTNAMELEN + 1]; + const char *vendor, *product, *serial, *mac; + const char *vn, *on, *ov; if (gethostname(hostname, sizeof(hostname))) { ERRNO("failed getting system hostname"); return SR_ERR_SYS; } + vendor = jgets(confd.root, "vendor"); + product = jgets(confd.root, "product-name"); + serial = jgets(confd.root, "serial-number"); + mac = jgets(confd.root, "mac-address"); + + vn = fgetkey("/etc/os-release", "VENDOR_NAME"); + on = fgetkey("/etc/os-release", "NAME"); + ov = fgetkey("/etc/os-release", "VERSION_ID"); + for (size_t i = 0; i < NELEMS(services); i++) { struct mdns_svc *srv = &services[i]; - char buf[256] = ""; + FILE *fp; if (type != all && srv->svc != type) continue; - if (srv->text) - snprintf(buf, sizeof(buf), srv->text, hostname); + if (cmd == MDNS_DELETE) { + erasef(AVAHI_SVC_PATH "/%s.service", srv->name); + continue; + } + + if (cmd == MDNS_UPDATE && !fexistf(AVAHI_SVC_PATH "/%s.service", srv->name)) + continue; + + fp = fopenf("w", AVAHI_SVC_PATH "/%s.service", srv->name); + if (!fp) { + ERRNO("failed creating %s.service", srv->name); + continue; + } + + fprintf(fp, + "\n" + "\n" + "\n" + " %s\n" + " \n" + " %s\n" + " %d\n" + " %s.local\n" + " vv=1\n" + " vendor=%s\n" + " product=%s\n" + " serial=%s\n" + " deviceid=%s\n" + " vn=%s\n" + " on=%s\n" + " ov=%s\n", + srv->desc, srv->type, srv->port, hostname, + vendor ?: "", product ?: "", serial ?: "", mac ?: "", + vn ?: "", on ?: "", ov ?: ""); + + if (srv->text) { + fprintf(fp, " "); + fprintf(fp, srv->text, hostname); + fprintf(fp, "\n"); + } - systemf("/usr/libexec/confd/gen-service %s %s %s %s %d \"%s\" %s", cmd, - hostname, srv->name, srv->type, srv->port, srv->desc, buf); + fprintf(fp, + " \n" + "\n"); + fclose(fp); } return SR_ERR_OK; @@ -182,7 +245,7 @@ static void svc_enadis(int ena, svc type, const char *svc) } if (type != none) - mdns_records(ena ? "add" : "delete", type); + mdns_records(ena ? MDNS_ADD : MDNS_DELETE, type); systemf("initctl -nbq touch avahi"); systemf("initctl -nbq touch nginx"); @@ -295,7 +358,7 @@ static int mdns_change(sr_session_ctx_t *session, struct lyd_node *config, struc mdns_conf(srv); /* Generate/update basic mDNS service records */ - mdns_records("update", all); + mdns_records(MDNS_UPDATE, all); } svc_enadis(ena, none, "avahi"); @@ -485,6 +548,48 @@ static int ssh_change(sr_session_ctx_t *session, struct lyd_node *config, struct } +static void web_ssl_conf(struct lyd_node *srv, struct lyd_node *config) +{ + const char *keyref, *certname = "self-signed"; + struct lyd_node *key, *certs; + FILE *fp; + + keyref = lydx_get_cattr(srv, "certificate"); + if (!keyref) + keyref = "gencert"; + + key = lydx_get_xpathf(config, "/ietf-keystore:keystore/asymmetric-keys" + "/asymmetric-key[name='%s']", keyref); + if (key) { + certs = lydx_get_descendant(lyd_child(key), "certificates", "certificate", NULL); + if (certs) { + const char *name = lydx_get_cattr(certs, "name"); + + if (name && *name) + certname = name; + } + } + + fp = fopen(NGINX_SSL_CONF, "w"); + if (!fp) { + ERRNO("failed creating %s", NGINX_SSL_CONF); + return; + } + + fprintf(fp, + "ssl_certificate %s/%s.crt;\n" + "ssl_certificate_key %s/%s.key;\n" + "\n" + "ssl_protocols TLSv1.3 TLSv1.2;\n" + "ssl_ciphers HIGH:!aNULL:!MD5;\n" + "ssl_prefer_server_ciphers on;\n" + "\n" + "ssl_session_cache shared:SSL:1m;\n" + "ssl_session_timeout 5m;\n", + SSL_CERT_DIR, certname, SSL_KEY_DIR, certname); + fclose(fp); +} + static int web_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd) { struct lyd_node *srv = NULL; @@ -498,6 +603,8 @@ static int web_change(sr_session_ctx_t *session, struct lyd_node *config, struct if (!cfg) return SR_ERR_OK; + web_ssl_conf(srv, config); + ena = lydx_is_enabled(srv, "enabled"); if (ena) { svc_enadis(srx_enabled(session, "%s/enabled", WEB_CONSOLE_XPATH), ttyd, "ttyd"); diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 8efe9e3e1..a2e5426ac 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -43,13 +43,13 @@ MODULES=( "infix-firewall-icmp-types@2025-04-26.yang" "infix-meta@2025-12-10.yang" "infix-system@2025-12-02.yang" - "infix-services@2025-12-10.yang" + "infix-services@2026-02-14.yang" "ieee802-ethernet-interface@2019-06-21.yang" "infix-ethernet-interface@2024-02-27.yang" "infix-factory-default@2023-06-28.yang" "infix-interfaces@2025-11-06.yang -e vlan-filtering" "ietf-crypto-types -e cleartext-symmetric-keys" - "infix-crypto-types@2025-11-09.yang" + "infix-crypto-types@2026-02-14.yang" "ietf-keystore -e symmetric-keys" "infix-ntp@2026-02-08.yang" "infix-keystore@2025-12-17.yang" diff --git a/src/confd/yang/confd/infix-crypto-types.yang b/src/confd/yang/confd/infix-crypto-types.yang index ab7ab37ab..ada63afa1 100644 --- a/src/confd/yang/confd/infix-crypto-types.yang +++ b/src/confd/yang/confd/infix-crypto-types.yang @@ -6,6 +6,9 @@ module infix-crypto-types { prefix ct; } + revision 2026-02-14 { + description "Add X.509 public key format for TLS certificates."; + } revision 2025-11-09 { description "Add Wireguard public/private key and sha"; } @@ -44,6 +47,13 @@ module infix-crypto-types { Used for SSH host keys."; } + identity x509-public-key-format { + base public-key-format; + base ct:subject-public-key-info-format; + description + "X.509 SubjectPublicKeyInfo format. Used for TLS certificates."; + } + identity symmetric-key-format { base ct:symmetric-key-format; description diff --git a/src/confd/yang/confd/infix-crypto-types@2025-11-09.yang b/src/confd/yang/confd/infix-crypto-types@2026-02-14.yang similarity index 100% rename from src/confd/yang/confd/infix-crypto-types@2025-11-09.yang rename to src/confd/yang/confd/infix-crypto-types@2026-02-14.yang diff --git a/src/confd/yang/confd/infix-services.yang b/src/confd/yang/confd/infix-services.yang index a08135acd..6eec46b72 100644 --- a/src/confd/yang/confd/infix-services.yang +++ b/src/confd/yang/confd/infix-services.yang @@ -25,6 +25,10 @@ module infix-services { contact "kernelkit@googlegroups.com"; description "Infix services, generic."; + revision 2026-02-14 { + description "Add certificate leaf to web container for TLS keystore reference."; + reference "internal"; + } revision 2025-12-10 { description "Adapt to changes in final version of ietf-keystore"; reference "internal"; @@ -188,6 +192,12 @@ module infix-services { container web { description "Web services"; + leaf certificate { + description "Reference to asymmetric key in central keystore with an + associated certificate. By default 'gencert' is used."; + type ks:central-asymmetric-key-ref; + } + leaf enabled { description "Enable or disable on all web services. diff --git a/src/confd/yang/confd/infix-services@2025-12-10.yang b/src/confd/yang/confd/infix-services@2026-02-14.yang similarity index 100% rename from src/confd/yang/confd/infix-services@2025-12-10.yang rename to src/confd/yang/confd/infix-services@2026-02-14.yang diff --git a/src/statd/python/yanger/__main__.py b/src/statd/python/yanger/__main__.py index e156174c4..f7097c078 100644 --- a/src/statd/python/yanger/__main__.py +++ b/src/statd/python/yanger/__main__.py @@ -1,100 +1,130 @@ -import logging -import logging.handlers import json -import sys # (built-in module) import os -import argparse +import sys from . import common from . import host -def main(): - def dirpath(path): - if not os.path.isdir(path): - raise argparse.ArgumentTypeError(f"'{path}' is not a valid directory") - return path +USAGE = """\ +usage: yanger [-p PARAM] [-x PREFIX] [-r DIR | -c DIR] model - parser = argparse.ArgumentParser(description="YANG data creator") - parser.add_argument("model", help="YANG Model") - parser.add_argument("-p", "--param", - help="Model dependent parameter, e.g. interface name") - parser.add_argument("-x", "--cmd-prefix", metavar="PREFIX", - help="Use this prefix for all system commands, e.g. " + - "'ssh user@remotehost sudo'") +YANG data creator - rrparser = parser.add_mutually_exclusive_group() - rrparser.add_argument("-r", "--replay", type=dirpath, metavar="DIR", - help="Generate output based on recorded system commands from DIR, " + - "rather than querying the local system") - rrparser.add_argument("-c", "--capture", metavar="DIR", - help="Capture system command output in DIR, such that the current system " + - "state can be recreated offline (with --replay) for testing purposes") +positional arguments: + model YANG Model - args = parser.parse_args() - if args.replay and args.cmd_prefix: - parser.error("--cmd-prefix cannot be used with --replay") +options: + -p, --param PARAM Model dependent parameter, e.g. interface name + -x, --cmd-prefix PREFIX + Use this prefix for all system commands, e.g. + 'ssh user@remotehost sudo' + -r, --replay DIR Generate output based on recorded system commands + from DIR, rather than querying the local system + -c, --capture DIR Capture system command output in DIR, such that the + current system state can be recreated offline (with + --replay) for testing purposes +""" - # Set up syslog output for critical errors to aid debugging - common.LOG = logging.getLogger('yanger') - if os.path.exists('/dev/log'): - log = logging.handlers.SysLogHandler(address='/dev/log') - else: - # Use /dev/null as a fallback for unit tests - log = logging.FileHandler('/dev/null') +def _parse_args(argv): + model = None + param = None + cmd_prefix = None + replay = None + capture = None + + i = 1 + while i < len(argv): + arg = argv[i] + if arg in ('-h', '--help'): + sys.stdout.write(USAGE) + sys.exit(0) + elif arg in ('-p', '--param'): + i += 1 + if i >= len(argv): + sys.exit(f"error: {arg} requires an argument") + param = argv[i] + elif arg in ('-x', '--cmd-prefix'): + i += 1 + if i >= len(argv): + sys.exit(f"error: {arg} requires an argument") + cmd_prefix = argv[i] + elif arg in ('-r', '--replay'): + i += 1 + if i >= len(argv): + sys.exit(f"error: {arg} requires an argument") + replay = argv[i] + if not os.path.isdir(replay): + sys.exit(f"error: '{replay}' is not a valid directory") + elif arg in ('-c', '--capture'): + i += 1 + if i >= len(argv): + sys.exit(f"error: {arg} requires an argument") + capture = argv[i] + elif arg.startswith('-'): + sys.exit(f"error: unknown option: {arg}") + elif model is None: + model = arg + else: + sys.exit(f"error: unexpected argument: {arg}") + i += 1 - fmt = logging.Formatter('%(name)s[%(process)d]: %(message)s') - log.setFormatter(fmt) - common.LOG.setLevel(logging.INFO) - common.LOG.addHandler(log) + if model is None: + sys.exit("error: missing required argument: model") + if replay and cmd_prefix: + sys.exit("error: --cmd-prefix cannot be used with --replay") + if replay and capture: + sys.exit("error: --replay cannot be used with --capture") + + return model, param, cmd_prefix, replay, capture + +def main(): + model, param, cmd_prefix, replay, capture = _parse_args(sys.argv) - if args.cmd_prefix or args.capture: - host.HOST = host.Remotehost(args.cmd_prefix, args.capture) - elif args.replay: - host.HOST = host.Replayhost(args.replay) + if cmd_prefix or capture: + host.HOST = host.Remotehost(cmd_prefix, capture) + elif replay: + host.HOST = host.Replayhost(replay) else: host.HOST = host.Localhost() - if args.model == 'ietf-interfaces': + if model == 'ietf-interfaces': from . import ietf_interfaces - yang_data = ietf_interfaces.operational(args.param) - elif args.model == 'ietf-routing': + yang_data = ietf_interfaces.operational(param) + elif model == 'ietf-routing': from . import ietf_routing yang_data = ietf_routing.operational() - elif args.model == 'ietf-ospf': + elif model == 'ietf-ospf': from . import ietf_ospf yang_data = ietf_ospf.operational() - elif args.model == 'ietf-rip': + elif model == 'ietf-rip': from . import ietf_rip yang_data = ietf_rip.operational() - elif args.model == 'ietf-hardware': + elif model == 'ietf-hardware': from . import ietf_hardware yang_data = ietf_hardware.operational() - elif args.model == 'infix-containers': + elif model == 'infix-containers': from . import infix_containers yang_data = infix_containers.operational() - elif args.model == 'infix-dhcp-server': + elif model == 'infix-dhcp-server': from . import infix_dhcp_server yang_data = infix_dhcp_server.operational() - elif args.model == 'ietf-system': + elif model == 'ietf-system': from . import ietf_system yang_data = ietf_system.operational() - elif args.model == 'ietf-ntp': + elif model == 'ietf-ntp': from . import ietf_ntp yang_data = ietf_ntp.operational() - elif args.model == 'ieee802-dot1ab-lldp': - from . import infix_lldp + elif model == 'ieee802-dot1ab-lldp': + from . import infix_lldp yang_data = infix_lldp.operational() - elif args.model == 'infix-firewall': + elif model == 'infix-firewall': from . import infix_firewall yang_data = infix_firewall.operational() - elif args.model == 'ietf-bfd-ip-sh': + elif model == 'ietf-bfd-ip-sh': from . import ietf_bfd_ip_sh yang_data = ietf_bfd_ip_sh.operational() - elif args.model == 'infix-wifi-radio': - from . import infix_wifi_radio - yang_data = infix_wifi_radio.operational() else: - common.LOG.warning("Unsupported model %s", args.model) + common.LOG.warning("Unsupported model %s", model) sys.exit(1) print(json.dumps(yang_data, indent=2, ensure_ascii=False)) diff --git a/src/statd/python/yanger/common.py b/src/statd/python/yanger/common.py index be0c3ef90..a6cf8e506 100644 --- a/src/statd/python/yanger/common.py +++ b/src/statd/python/yanger/common.py @@ -1,8 +1,50 @@ +import syslog from datetime import timedelta from . import host -LOG = None + +class SysLog: + """Lightweight syslog wrapper replacing the logging module. + + Provides the same .error()/.warning()/.info()/.debug() interface + used throughout yanger, but uses the C syslog facility directly, + avoiding the ~374ms import overhead of logging + logging.handlers. + """ + + DEBUG = syslog.LOG_DEBUG + INFO = syslog.LOG_INFO + WARNING = syslog.LOG_WARNING + ERROR = syslog.LOG_ERR + + def __init__(self, name): + syslog.openlog(name, syslog.LOG_PID) + self._level = self.INFO + + def setLevel(self, level): + self._level = level + + def _log(self, level, msg, *args): + if level > self._level: + return + if args: + msg = msg % args + syslog.syslog(level, msg) + + def debug(self, msg, *args): + self._log(self.DEBUG, msg, *args) + + def info(self, msg, *args): + self._log(self.INFO, msg, *args) + + def warning(self, msg, *args): + self._log(self.WARNING, msg, *args) + + def error(self, msg, *args): + self._log(self.ERROR, msg, *args) + + +LOG = SysLog("yanger") class YangDate: def __init__(self, dt=None): diff --git a/src/statd/python/yanger/ietf_hardware.py b/src/statd/python/yanger/ietf_hardware.py index b68493c4e..1f210a7f4 100644 --- a/src/statd/python/yanger/ietf_hardware.py +++ b/src/statd/python/yanger/ietf_hardware.py @@ -265,8 +265,11 @@ def create_sensor(sensor_name, value, value_type, value_scale, label=None): component["description"] = desc return component + # List hwmon directory once, reuse for all sensor types + all_entries = HOST.run(("ls", hwmon_path), default="").split() + # Temperature sensors - temp_entries = HOST.run(("ls", hwmon_path), default="").split() + temp_entries = all_entries temp_files = [os.path.join(hwmon_path, e) for e in temp_entries if e.startswith("temp") and e.endswith("_input")] for temp_file in temp_files: try: @@ -285,7 +288,7 @@ def create_sensor(sensor_name, value, value_type, value_scale, label=None): continue # Fan sensors (RPM from tachometer) - fan_entries = HOST.run(("ls", hwmon_path), default="").split() + fan_entries = all_entries fan_files = [os.path.join(hwmon_path, e) for e in fan_entries if e.startswith("fan") and e.endswith("_input")] for fan_file in fan_files: try: @@ -307,7 +310,7 @@ def create_sensor(sensor_name, value, value_type, value_scale, label=None): # Only add if no fan*_input exists for this device (avoid duplicates) has_rpm_sensor = bool(fan_files) if not has_rpm_sensor: - pwm_entries = HOST.run(("ls", hwmon_path), default="").split() + pwm_entries = all_entries pwm_files = [os.path.join(hwmon_path, e) for e in pwm_entries if e.startswith("pwm") and e[3:].replace('_', '').isdigit() if len(e) > 3] for pwm_file in pwm_files: # Skip pwm*_enable, pwm*_mode, etc. - only process pwm1, pwm2, etc. @@ -336,7 +339,7 @@ def create_sensor(sensor_name, value, value_type, value_scale, label=None): continue # Voltage sensors - voltage_entries = HOST.run(("ls", hwmon_path), default="").split() + voltage_entries = all_entries voltage_files = [os.path.join(hwmon_path, e) for e in voltage_entries if e.startswith("in") and e.endswith("_input")] for voltage_file in voltage_files: try: @@ -356,7 +359,7 @@ def create_sensor(sensor_name, value, value_type, value_scale, label=None): continue # Current sensors - current_entries = HOST.run(("ls", hwmon_path), default="").split() + current_entries = all_entries current_files = [os.path.join(hwmon_path, e) for e in current_entries if e.startswith("curr") and e.endswith("_input")] for current_file in current_files: try: @@ -376,7 +379,7 @@ def create_sensor(sensor_name, value, value_type, value_scale, label=None): continue # Power sensors - power_entries = HOST.run(("ls", hwmon_path), default="").split() + power_entries = all_entries power_files = [os.path.join(hwmon_path, e) for e in power_entries if e.startswith("power") and e.endswith("_input")] for power_file in power_files: try: diff --git a/src/statd/python/yanger/ietf_interfaces/bridge.py b/src/statd/python/yanger/ietf_interfaces/bridge.py index b57536b4a..3c3bbd8c7 100644 --- a/src/statd/python/yanger/ietf_interfaces/bridge.py +++ b/src/statd/python/yanger/ietf_interfaces/bridge.py @@ -204,10 +204,13 @@ def mctlq2yang_mode(mctlq): return "off" -def mctl(ifname, vid): - mctl = HOST.run_json(["mctl", "-p", "show", "igmp", "json"], default={}) +def mctl_queriers(): + """Fetch all IGMP multicast querier data in one call""" + return HOST.run_json(["mctl", "-p", "show", "igmp", "json"], default={}) - for q in mctl.get("multicast-queriers", []): + +def mctl(ifname, vid, mctldata): + for q in mctldata.get("multicast-queriers", []): # TODO: Also need to match against VLAN uppers (e.g. br0.1337) if q.get("interface") == ifname and q.get("vid") == vid: return q @@ -239,8 +242,8 @@ def multicast_filters(iplink, vid): return { "multicast-filter": list(mdb.values()) } -def multicast(iplink, info): - mctlq = mctl(iplink["ifname"], info.get("vlan")) +def multicast(iplink, info, mctldata): + mctlq = mctl(iplink["ifname"], info.get("vlan"), mctldata) mcast = { "snooping": bool(info.get("mcast_snooping")), @@ -276,13 +279,15 @@ def vlans(iplink): if not (brgvlans := HOST.run_json(f"bridge -j vlan global show dev {iplink['ifname']}".split())): return [] + mctldata = mctl_queriers() + vlans = { v["vlan"]: { "vid": v["vlan"], "untagged": [], "tagged": [], - "multicast": multicast(iplink, v), + "multicast": multicast(iplink, v, mctldata), "multicast-filters": multicast_filters(iplink, v["vlan"]), } for v in brgvlans[0]["vlans"] @@ -307,7 +312,7 @@ def dbridge(iplink): info = iplink["linkinfo"]["info_data"] return { - "multicast": multicast(iplink, info), + "multicast": multicast(iplink, info, mctl_queriers()), "multicast-filters": multicast_filters(iplink, None), } diff --git a/src/statd/python/yanger/ietf_routing.py b/src/statd/python/yanger/ietf_routing.py index 9d7b1d9b9..da6fd1572 100644 --- a/src/statd/python/yanger/ietf_routing.py +++ b/src/statd/python/yanger/ietf_routing.py @@ -132,19 +132,34 @@ def get_routing_interfaces(): links_json = HOST.run(tuple(['ip', '-j', 'link', 'show']), default="[]") links = json.loads(links_json) + # Fetch all forwarding sysctls in two calls instead of 2 per interface + ipv4_sysctls = HOST.run(tuple(['sysctl', 'net.ipv4.conf']), default="") + ipv6_sysctls = HOST.run(tuple(['sysctl', 'net.ipv6.conf']), default="") + + # Parse "net.ipv4.conf..forwarding = 1" lines into a set + ipv4_fwd = set() + ipv6_fwd = set() + for line in ipv4_sysctls.splitlines(): + if '.forwarding = 1' in line: + # net.ipv4.conf.IFNAME.forwarding = 1 + parts = line.split('.') + if len(parts) >= 5: + ipv4_fwd.add(parts[3]) + + for line in ipv6_sysctls.splitlines(): + if '.force_forwarding = 1' in line: + # net.ipv6.conf.IFNAME.force_forwarding = 1 + parts = line.split('.') + if len(parts) >= 5: + ipv6_fwd.add(parts[3]) + routing_ifaces = [] for link in links: ifname = link.get('ifname') if not ifname: continue - # Check if IPv4 forwarding is enabled - ipv4_fwd = HOST.run(tuple(['sysctl', '-n', f'net.ipv4.conf.{ifname}.forwarding']), default="0").strip() - - # Check if IPv6 force_forwarding is enabled (available since Linux 6.17) - ipv6_fwd = HOST.run(tuple(['sysctl', '-n', f'net.ipv6.conf.{ifname}.force_forwarding']), default="0").strip() - - if ipv4_fwd == "1" or ipv6_fwd == "1": + if ifname in ipv4_fwd or ifname in ipv6_fwd: routing_ifaces.append(ifname) return routing_ifaces diff --git a/test/case/interfaces/iface_phys_address/test.py b/test/case/interfaces/iface_phys_address/test.py index c6a7d27a6..0a11a82c7 100755 --- a/test/case/interfaces/iface_phys_address/test.py +++ b/test/case/interfaces/iface_phys_address/test.py @@ -35,15 +35,14 @@ def reset_mac(tgt, port): with infamy.Test() as test: - CMD = "jq -r '.[\"mac-address\"]' /run/system.json" - with test.step("Set up topology and attach to target DUT"): env = infamy.Env() target = env.attach("target", "mgmt") - tgtssh = env.attach("target", "mgmt", "ssh") _, tport = env.ltop.xlate("target", "data") pmac = iface.get_phys_address(target, tport) - cmac = tgtssh.runsh(CMD).stdout.strip() + data = target.get_data("/ietf-hardware:hardware/component[name='mainboard']") + cmac = data.get("hardware", {}).get("component", {}) \ + .get("mainboard", {}).get("infix-hardware:phys-address", "") STATIC = "02:01:00:c0:ff:ee" OFFSET = "00:00:00:00:ff:aa" @@ -88,9 +87,7 @@ def reset_mac(tgt, port): target.put_config_dict("ietf-interfaces", config) with test.step("Verify target:data has chassis MAC"): - mac = iface.get_phys_address(target, tport) - print(f"Current MAC: {mac}, should be: {cmac}") - assert mac == cmac + until(lambda: iface.get_phys_address(target, tport) == cmac) with test.step("Set target:data to chassis MAC + offset"): print(f"Setting chassis MAC {cmac} + offset {OFFSET}") @@ -109,10 +106,8 @@ def reset_mac(tgt, port): target.put_config_dict("ietf-interfaces", config) with test.step("Verify target:data has chassis MAC + offset"): - mac = iface.get_phys_address(target, tport) BMAC = calc_mac(cmac, OFFSET) - print(f"Current MAC: {mac}, should be: {BMAC} (calculated)") - assert mac == BMAC + until(lambda: iface.get_phys_address(target, tport) == BMAC) with test.step("Reset target:data MAC address to default"): reset_mac(target, tport) diff --git a/test/infamy/tap.py b/test/infamy/tap.py index b19383265..42b7e10b0 100644 --- a/test/infamy/tap.py +++ b/test/infamy/tap.py @@ -56,6 +56,8 @@ def __exit__(self, t, e, tb): elif len(e.args) and type(e.args[0]) is subprocess.CompletedProcess: print("Failing subprocess stdout:\n", e.args[0].stdout) + self.out.write(f"1..{self.steps}\n") + self.out.flush() raise SystemExit(1) @contextlib.contextmanager