diff --git a/.github/ci/targets.yml b/.github/ci/targets.yml new file mode 100644 index 000000000..11cdbdec7 --- /dev/null +++ b/.github/ci/targets.yml @@ -0,0 +1,114 @@ +# CI targets for the build-firmware workflow. +openwrt_releases: + - "24.10.6" + - "25.12.2" +feed_branches: + "24.10.6": "openwrt-24.10" + "25.12.2": "openwrt-25.12" +default_physical_releases: + - "24.10.6" + +packages: >- + lime-system lime-proto-babeld lime-proto-batadv lime-proto-anygw + lime-hwd-openwrt-wan lime-hwd-ground-routing lime-app + lime-debug lime-docs lime-docs-minimal + shared-state-babeld_hosts shared-state-bat_hosts + shared-state-dnsmasq_hosts shared-state-nodes_and_links + babeld-auto-gw-mode check-date-http batctl-default + -dnsmasq -odhcpd-ipv6only + +targets: + - device: linksys_e8450 + imagebuilder: mediatek-mt7622 + # The `linksys_e8450-ubi` profile (UBI-on-NAND) defines KERNEL_INITRAMFS + # and ships the kernel.bin + DTB build_image.sh needs to repack the FIT; + # the plain `linksys_e8450` profile does not. + profile: linksys_e8450-ubi + arch: aarch64_cortex-a53 + sdk_arch: aarch64_cortex-a53-openwrt-24.10 + index_imagebuilder: mediatek-filogic + build_initramfs: true + fit_arch: arm64 + fit_kernel_loadaddr: "0x44000000" + fit_dts: mt7622-linksys-e8450-ubi + test_firmware: true + test_places: + - belkin_rt3200_1 + - belkin_rt3200_2 + - belkin_rt3200_3 + # MT7622 BL2/BL31 take ~12s before U-Boot starts its 3s autoboot + # countdown; bump the interrupt-spam window so it covers both. + uboot_interrupt_spam_sec: 25 + # mt7915e PCIe needs `swiotlb=512` to allocate DMA bounce buffers. + fit_bootargs: "console=ttyS0,115200n1 swiotlb=512 pci=pcie_bus_perf" + # OEM MAC lives in a UBI factory volume; mtk_eth_soc races UBI attach + # and gets stuck on -EPROBE_DEFER. Patch DTBs to inject the MAC. + dtb_patch_nvmem_mac: true + # Force the legacy 23.05 SPI-NAND layout so the kernel UBI MTD does + # not overwrite BL2/BL31/factory on units still on layout 1.0. + dtb_force_legacy_partitions: true + + - device: openwrt_one + imagebuilder: mediatek-filogic + profile: openwrt_one + arch: aarch64_cortex-a53 + sdk_arch: aarch64_cortex-a53-openwrt-24.10 + index_imagebuilder: mediatek-filogic + build_initramfs: true + fit_arch: arm64 + fit_kernel_loadaddr: "0x44000000" + fit_dts: mt7981b-openwrt-one + + - device: qemu_x86_64 + imagebuilder: x86-64 + profile: generic + arch: x86_64 + sdk_arch: x86_64-openwrt-24.10 + index_imagebuilder: x86-64 + build_initramfs: false + image_format: x86-combined + # vwifi packaging fork pinned by full SHA (smart-HTTP rejects short prefixes). + packages: >- + {{ packages_default }} kmod-mac80211-hwsim wpad-mesh-mbedtls vwifi + extra_feeds: + - "src-git|vwifi|https://github.com/fcefyn-testbed/vwifi_cli_package.git^8a57be2622053ca75e7e2c8c592cf62b9fad012f" + extra_packages: + - "vwifi" + test_firmware: false + test_qemu: true + + - device: librerouter_v1 + imagebuilder: ath79-generic + profile: librerouter_librerouter-v1 + arch: mips_24kc + sdk_arch: mips_24kc-openwrt-24.10 + index_imagebuilder: ath79-generic + build_initramfs: true + image_format: dual-tftp + test_firmware: true + test_places: + - librerouter_1 + + - device: bananapi_bpi-r4 + imagebuilder: mediatek-filogic + profile: bananapi_bpi-r4 + arch: aarch64_cortex-a53 + sdk_arch: aarch64_cortex-a53-openwrt-24.10 + index_imagebuilder: mediatek-filogic + build_initramfs: true + fit_arch: arm64 + fit_kernel_loadaddr: "0x46000000" + fit_dts: mt7988a-bananapi-bpi-r4 + # mt7996e on 24.10.x panics under hostapd; exclude wifi kmods until the + # mt76 backport lands. The board still mesh-routes over the wired DSA + # ports. Tracking: https://github.com/openwrt/openwrt/issues/21657 + packages: >- + lime-system lime-proto-babeld lime-proto-batadv lime-proto-anygw + lime-hwd-openwrt-wan lime-hwd-ground-routing lime-app + lime-debug lime-docs lime-docs-minimal + shared-state-babeld_hosts shared-state-bat_hosts + shared-state-dnsmasq_hosts shared-state-nodes_and_links + babeld-auto-gw-mode check-date-http batctl-default + -dnsmasq -odhcpd-ipv6only + -kmod-mt7996e -kmod-mt7996-firmware -kmod-mt7996-firmware-common + -kmod-mt7996-233-firmware -mt7988-wo-firmware diff --git a/.github/workflows/build-firmware.yml b/.github/workflows/build-firmware.yml new file mode 100644 index 000000000..fd357379d --- /dev/null +++ b/.github/workflows/build-firmware.yml @@ -0,0 +1,1169 @@ +name: Build firmware + +on: + pull_request: + branches: [master, main] + paths: + - ".github/workflows/build-firmware.yml" + - ".github/ci/targets.yml" + - "tools/ci/**" + - "packages/**" + workflow_dispatch: + inputs: + targets: + description: "Comma-separated devices from .github/ci/targets.yml or 'all'" + required: false + default: "all" + openwrt_releases: + description: "Comma-separated OpenWrt releases (defaults to targets.yml openwrt_releases)" + required: false + default: "" + physical_releases: + description: "Comma-separated releases to run on physical lab (defaults to targets.yml default_physical_releases)" + required: false + default: "" + physical_single: + description: "Run single-node tests on every physical lab DUT" + required: false + type: boolean + default: false + physical_mesh_count: + description: "Number of physical DUTs for the mesh test (0 = skip, 2 or 3)" + required: false + type: choice + options: + - "0" + - "2" + - "3" + default: "0" + physical_mesh_pairs: + description: "Run 2-node mesh pairs sweep (same as daily cron)" + required: false + type: boolean + default: false + schedule: + # Daily lab sweep at 06:00 UTC (03:00 ART). + - cron: "0 6 * * *" + +concurrency: + # PR/push share a per-ref lane; schedule and workflow_dispatch share a single lab lane so lab-bound runs never race on a labgrid place. + group: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && 'physical-lab-shared' || format('build-firmware-{0}', github.ref) }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + prepare-matrix: + runs-on: ubuntu-latest + outputs: + targets_matrix: ${{ steps.prepare.outputs.targets_matrix }} + test_targets_matrix: ${{ steps.prepare.outputs.test_targets_matrix }} + mesh_test_matrix: ${{ steps.prepare.outputs.mesh_test_matrix }} + mesh_pairs_matrix: ${{ steps.prepare.outputs.mesh_pairs_matrix }} + qemu_single_matrix: ${{ steps.prepare.outputs.qemu_single_matrix }} + qemu_mesh_matrix: ${{ steps.prepare.outputs.qemu_mesh_matrix }} + archs_matrix: ${{ steps.prepare.outputs.archs_matrix }} + lime_packages_list: ${{ steps.prepare.outputs.lime_packages_list }} + feed_hash: ${{ steps.prepare.outputs.feed_hash }} + steps: + - uses: actions/checkout@v5 + + - name: Install yq + run: sudo apt-get update && sudo apt-get install -y yq jq + + - name: Prepare matrices + id: prepare + env: + TARGETS_INPUT: ${{ github.event.inputs.targets || 'all' }} + RELEASES_OVERRIDE: ${{ github.event.inputs.openwrt_releases || '' }} + PHYSICAL_RELEASES_OVERRIDE: ${{ github.event.inputs.physical_releases || '' }} + MESH_COUNT_INPUT: ${{ github.event.inputs.physical_mesh_count || '0' }} + EVENT_NAME: ${{ github.event_name }} + run: tools/ci/prepare_matrix.sh + + build-feed: + runs-on: ubuntu-latest + needs: prepare-matrix + timeout-minutes: 180 + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.prepare-matrix.outputs.archs_matrix) }} + steps: + - uses: actions/checkout@v5 + + - name: Free disk + run: sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android + + - name: Restore feed cache + id: cache-feed + uses: actions/cache@v4 + with: + path: feed-artifact/lime_packages + # `extra_hash` is a 12-char sha256 prefix over (extra_feeds, + # extra_packages); restore-keys deliberately omits it so a stale + # extras pin never falls back to a partial match. + key: lime-feed-v3-${{ matrix.arch }}-${{ matrix.openwrt_release }}-${{ needs.prepare-matrix.outputs.feed_hash }}-${{ matrix.extra_hash }} + restore-keys: | + lime-feed-v3-${{ matrix.arch }}-${{ matrix.openwrt_release }}-${{ needs.prepare-matrix.outputs.feed_hash }}- + + - name: Build packages with openwrt/gh-action-sdk + if: steps.cache-feed.outputs.cache-hit != 'true' + uses: openwrt/gh-action-sdk@v9 + env: + ARCH: ${{ matrix.sdk_arch }} + FEEDNAME: lime_packages + PACKAGES: ${{ needs.prepare-matrix.outputs.lime_packages_list }} ${{ matrix.extra_packages }} + # Extra src-git feeds, space-separated `||[^]` + # entries; gh-action-sdk appends each to feeds.conf. Pinning the + # full 40-char SHA is required (smart-HTTP rejects short prefixes). + EXTRA_FEEDS: ${{ matrix.extra_feeds }} + NO_REFRESH_CHECK: "1" + NO_SHFMT_CHECK: "1" + # SDK-side `make package/index` is unreliable in CI; we index the + # feed downstream with ImageBuilder's ipkg-make-index / apk mkndx. + INDEX: "0" + IGNORE_ERRORS: "n m y" + + - name: Diagnose feed build output + if: steps.cache-feed.outputs.cache-hit != 'true' + shell: bash + env: + OPENWRT_RELEASE: ${{ matrix.openwrt_release }} + run: | + set -euo pipefail + shopt -s nullglob + case "${OPENWRT_RELEASE}" in + 24.10.*) PKG_FORMAT=ipk ;; + *) PKG_FORMAT=apk ;; + esac + echo "Package format for ${OPENWRT_RELEASE}: ${PKG_FORMAT}" + echo "=== bin/packages tree ===" + find bin/packages -maxdepth 4 -type d 2>/dev/null | sort || true + echo "=== bin/targets tree (where target-binary extras land) ===" + find bin/targets -maxdepth 4 -type d 2>/dev/null | sort || true + echo "=== bin/packages/${{ matrix.arch }}/lime_packages/ ===" + ls -la "bin/packages/${{ matrix.arch }}/lime_packages/" 2>/dev/null || echo "(missing)" + echo "=== bin/packages/all/lime_packages/ ===" + ls -la "bin/packages/all/lime_packages/" 2>/dev/null || echo "(missing)" + arch_files=( + "bin/packages/${{ matrix.arch }}/lime_packages/"*.ipk + "bin/packages/${{ matrix.arch }}/lime_packages/"*.apk + ) + all_files=( + "bin/packages/all/lime_packages/"*.ipk + "bin/packages/all/lime_packages/"*.apk + ) + echo "Arch packages: ${#arch_files[@]} | All packages: ${#all_files[@]}" + if (( ${#arch_files[@]} == 0 && ${#all_files[@]} == 0 )); then + echo "ERROR: feed produced no .ipk or .apk packages" >&2 + exit 1 + fi + # Best-effort listing of every extra feed output dir; missing + # dirs are tolerated since target-binary packages land under + # bin/targets/ instead. + extra_feeds_str="${{ matrix.extra_feeds }}" + if [[ -n "$extra_feeds_str" ]]; then + echo "=== Extra src-git feeds output ===" + for entry in $extra_feeds_str; do + feed_name="$(echo "$entry" | cut -d'|' -f2)" + extra_dir="bin/packages/${{ matrix.arch }}/${feed_name}" + echo "--- ${extra_dir} ---" + ls -la "$extra_dir" 2>/dev/null || echo "(missing - target-binary extras live under bin/targets/)" + done + echo "=== bin/targets/**/packages/ (target-built packages) ===" + find bin/targets \( -name "*.ipk" -o -name "*.apk" \) -path "*/packages/*" 2>/dev/null \ + | sort || echo "(none)" + fi + + - name: Assemble lime_packages feed artifact (index with ImageBuilder) + if: steps.cache-feed.outputs.cache-hit != 'true' + env: + OPENWRT_RELEASE: ${{ matrix.openwrt_release }} + run: | + set -euo pipefail + # 24.10.x emits .ipk indexed by Packages/Packages.gz (opkg); + # 25.12.x emits .apk indexed by packages.adb (apk-tools). + case "${OPENWRT_RELEASE}" in + 24.10.*) PKG_FORMAT=ipk ;; + *) PKG_FORMAT=apk ;; + esac + echo "Assembling feed for ${OPENWRT_RELEASE} (PKG_FORMAT=${PKG_FORMAT})" + + rm -rf feed-artifact + mkdir -p feed-artifact/lime_packages + + arch_dir="bin/packages/${{ matrix.arch }}/lime_packages" + all_dir="bin/packages/all/lime_packages" + + if [[ -d "$arch_dir" ]]; then + cp -a "$arch_dir"/. feed-artifact/lime_packages/ + fi + if [[ -d "$all_dir" ]]; then + cp -a "$all_dir"/. feed-artifact/lime_packages/ + fi + + # Merge per-feed packages from the extra src-git feeds. PKGARCH:=all + # entries land under bin/packages///; target-binary + # entries (e.g. vwifi) land under bin/targets///packages/. + extra_feeds_str="${{ matrix.extra_feeds }}" + extra_packages_str="${{ matrix.extra_packages }}" + if [[ -n "$extra_feeds_str" ]]; then + for entry in $extra_feeds_str; do + feed_name="$(echo "$entry" | cut -d'|' -f2)" + for src in \ + "bin/packages/${{ matrix.arch }}/${feed_name}" \ + "bin/packages/all/${feed_name}" + do + if [[ -d "$src" ]]; then + echo "Merging $src into feed-artifact/lime_packages/" + shopt -s nullglob + for f in "$src"/*.ipk "$src"/*.apk; do + cp -a "$f" feed-artifact/lime_packages/ + done + fi + done + done + # Pull each declared extra_packages entry from bin/targets/ + # if not already present (handles target-binary feeds like + # vwifi where the SDK lands the package under bin/targets/). + if [[ -n "$extra_packages_str" && -d bin/targets ]]; then + for pkg in $extra_packages_str; do + shopt -s nullglob + already=( + feed-artifact/lime_packages/${pkg}_*.ipk + feed-artifact/lime_packages/${pkg}-*.ipk + feed-artifact/lime_packages/${pkg}-*.apk + ) + if [[ ${#already[@]} -gt 0 ]]; then + continue + fi + while IFS= read -r -d '' f; do + echo "Merging $f (from bin/targets) into feed-artifact/lime_packages/" + cp -a "$f" feed-artifact/lime_packages/ + done < <(find bin/targets -path '*/packages/*' \ + \( -name "${pkg}_*.ipk" -o -name "${pkg}-*.ipk" -o -name "${pkg}-*.apk" \) \ + -print0 2>/dev/null) + done + fi + fi + + # Each declared extra_packages entry must have produced at least + # one .ipk/.apk; gh-action-sdk exits 0 when an arch is unsupported. + if [[ -n "$extra_packages_str" ]]; then + for pkg in $extra_packages_str; do + shopt -s nullglob + matches=( + feed-artifact/lime_packages/${pkg}_*.ipk + feed-artifact/lime_packages/${pkg}-*.ipk + feed-artifact/lime_packages/${pkg}-*.apk + ) + if [[ ${#matches[@]} -eq 0 ]]; then + echo "ERROR: extra_packages entry '${pkg}' produced no .ipk or .apk" >&2 + echo "Searched feed-artifact/lime_packages/${pkg}{_,-}*.{ipk,apk}" >&2 + echo "Also searched bin/targets/**/packages/${pkg}*:" >&2 + find bin/targets -path '*/packages/*' \ + \( -name "${pkg}_*.ipk" -o -name "${pkg}-*.ipk" -o -name "${pkg}-*.apk" \) \ + 2>/dev/null >&2 || true + exit 1 + fi + done + fi + + shopt -s nullglob + all_pkgs=(feed-artifact/lime_packages/*.ipk feed-artifact/lime_packages/*.apk) + if [[ ${#all_pkgs[@]} -eq 0 ]]; then + echo "ERROR: No .ipk or .apk files found under feed-artifact/lime_packages" >&2 + echo "Expected sources: $arch_dir and/or $all_dir" >&2 + exit 1 + fi + + docker pull "ghcr.io/openwrt/imagebuilder:${{ matrix.index_imagebuilder }}-v${OPENWRT_RELEASE}" + chmod -R a+rwX feed-artifact/lime_packages + + if [[ "$PKG_FORMAT" == "ipk" ]]; then + # ipkg-make-index puts the path it receives straight into the + # `Filename:` field after stripping a leading `./`. Pass `.` + # so the index ships bare basenames; `/work` would emit + # `Filename: /work/...` which opkg cannot resolve. + docker run --rm \ + --user root \ + -e MKHASH=/builder/staging_dir/host/bin/mkhash \ + -e PATH=/builder/staging_dir/host/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ + -v "$(pwd)/feed-artifact/lime_packages:/work" \ + "ghcr.io/openwrt/imagebuilder:${{ matrix.index_imagebuilder }}-v${OPENWRT_RELEASE}" \ + sh -lc 'cd /work && /builder/scripts/ipkg-make-index.sh . > Packages && gzip -9nc Packages > Packages.gz' + + echo "=== Final feed-artifact/lime_packages/ contents (ipk) ===" + ls -la feed-artifact/lime_packages/ + echo "=== Packages index summary (first 60 lines + count) ===" + head -n 60 feed-artifact/lime_packages/Packages || true + pkg_count=$(grep -c '^Package:' feed-artifact/lime_packages/Packages || echo 0) + echo "Total Package: entries in index: ${pkg_count}" + echo "=== Sanity: Filename fields must be bare basenames ===" + bad_fn=$(grep -E '^Filename: (/|\./)' feed-artifact/lime_packages/Packages || true) + if [[ -n "$bad_fn" ]]; then + echo "ERROR: Packages index has non-basename Filename entries:" >&2 + echo "$bad_fn" >&2 + exit 1 + fi + grep -m 3 '^Filename:' feed-artifact/lime_packages/Packages + else + # apk-tools 3.x quirks: `--allow-untrusted` is a global flag + # (must precede the sub-command); `apk --help` exits 1 + # even on valid sub-commands, so probe via the global help. + # The IB ships `apk` under staging_dir/host{,/usr}/bin/apk. + docker run --rm \ + --user root \ + -e PATH=/builder/staging_dir/host/usr/bin:/builder/staging_dir/host/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ + -v "$(pwd)/feed-artifact/lime_packages:/work" \ + "ghcr.io/openwrt/imagebuilder:${{ matrix.index_imagebuilder }}-v${OPENWRT_RELEASE}" \ + sh -lc ' + set -e + cd /work + APK="" + for cand in \ + /builder/staging_dir/host/usr/bin/apk \ + /builder/staging_dir/host/bin/apk \ + /builder/staging_dir/hostpkg/usr/bin/apk \ + /usr/bin/apk \ + $(command -v apk 2>/dev/null || true) + do + if [ -n "$cand" ] && [ -x "$cand" ]; then + APK="$cand"; break + fi + done + if [ -z "$APK" ]; then + echo "ERROR: cannot locate apk binary in the IB" >&2 + echo "=== /builder/staging_dir/host listing ===" >&2 + find /builder/staging_dir/host -maxdepth 4 -name apk 2>/dev/null >&2 || true + echo "=== /builder layout (top 2 levels) ===" >&2 + find /builder -maxdepth 2 -type d 2>/dev/null >&2 || true + exit 1 + fi + echo "=== apk binary: $APK ===" + "$APK" --version 2>&1 || true + help_text=$("$APK" --help 2>&1 || true) + if printf "%s\n" "$help_text" | grep -qw mkndx; then + echo "Using: apk --allow-untrusted mkndx" + "$APK" --allow-untrusted mkndx --output packages.adb -- *.apk + elif printf "%s\n" "$help_text" | grep -qw index; then + echo "Using: apk --allow-untrusted index" + "$APK" --allow-untrusted index --output packages.adb *.apk + else + echo "WARNING: neither mkndx nor index advertised in apk --help; attempting mkndx anyway" >&2 + printf "%s\n" "$help_text" | head -80 >&2 || true + "$APK" --allow-untrusted mkndx --output packages.adb -- *.apk + fi + ' + + echo "=== Final feed-artifact/lime_packages/ contents (apk) ===" + ls -la feed-artifact/lime_packages/ + if [[ ! -f feed-artifact/lime_packages/packages.adb ]]; then + echo "ERROR: packages.adb was not generated" >&2 + exit 1 + fi + adb_size=$(stat -c%s feed-artifact/lime_packages/packages.adb) + apk_count=$(find feed-artifact/lime_packages -maxdepth 1 -name "*.apk" | wc -l) + echo "packages.adb size: ${adb_size} bytes | .apk count: ${apk_count}" + fi + + - name: Diagnose cached feed (cache hit) + if: steps.cache-feed.outputs.cache-hit == 'true' + env: + OPENWRT_RELEASE: ${{ matrix.openwrt_release }} + run: | + set -euo pipefail + case "${OPENWRT_RELEASE}" in + 24.10.*) PKG_FORMAT=ipk ;; + *) PKG_FORMAT=apk ;; + esac + echo "=== Cache hit: feed-artifact/lime_packages/ (${PKG_FORMAT}) ===" + ls -la feed-artifact/lime_packages/ + if [[ "$PKG_FORMAT" == "ipk" ]]; then + pkg_count=$(grep -c '^Package:' feed-artifact/lime_packages/Packages 2>/dev/null || echo 0) + echo "Cached opkg index has ${pkg_count} packages" + else + apk_count=$(find feed-artifact/lime_packages -maxdepth 1 -name "*.apk" | wc -l) + adb_size=$(stat -c%s feed-artifact/lime_packages/packages.adb 2>/dev/null || echo 0) + echo "Cached apk feed: ${apk_count} .apk files, packages.adb=${adb_size} bytes" + fi + + - name: Upload local feed artifact + uses: actions/upload-artifact@v4 + with: + name: lime-feed-${{ matrix.arch }}-${{ matrix.openwrt_release }} + path: feed-artifact/lime_packages/ + retention-days: 7 + + - name: Upload feed debug logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: build-debug-feed-${{ matrix.arch }}-${{ matrix.openwrt_release }} + path: | + bin/packages/${{ matrix.arch }}/ + logs/ + if-no-files-found: ignore + retention-days: 7 + + build-image: + runs-on: ubuntu-latest + needs: [prepare-matrix, build-feed] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.prepare-matrix.outputs.targets_matrix) }} + steps: + - uses: actions/checkout@v5 + + - name: Free disk + run: sudo rm -rf /opt/hostedtoolcache /usr/share/dotnet /usr/local/lib/android + + - name: Download matching local feed + uses: actions/download-artifact@v5 + with: + name: lime-feed-${{ matrix.arch }}-${{ matrix.openwrt_release }} + path: ./feed-in/lime_packages/ + + - name: Diagnose downloaded feed + run: | + set -euo pipefail + echo "=== feed-in/lime_packages/ contents ===" + ls -la ./feed-in/lime_packages/ + echo "=== Packages index head ===" + head -n 80 ./feed-in/lime_packages/Packages || echo "(no Packages file)" + ipk_count=$(find ./feed-in/lime_packages -maxdepth 1 -name '*.ipk' | wc -l) + pkg_idx_count=$(grep -c '^Package:' ./feed-in/lime_packages/Packages 2>/dev/null || echo 0) + echo "IPK files: ${ipk_count} | Packages index entries: ${pkg_idx_count}" + + - name: Pull ImageBuilder image + run: docker pull ghcr.io/openwrt/imagebuilder:${{ matrix.imagebuilder }}-v${{ matrix.openwrt_release }} + + - name: Build firmware image + env: + DEVICE_NAME: ${{ matrix.device }} + ARCH: ${{ matrix.arch }} + FEED_BRANCH: ${{ matrix.feed_branch }} + PACKAGES: ${{ matrix.packages }} + BUILD_INITRAMFS: ${{ matrix.build_initramfs }} + IMAGE_FORMAT: ${{ matrix.image_format }} + FIT_ARCH: ${{ matrix.fit_arch }} + FIT_KERNEL_LOADADDR: ${{ matrix.fit_kernel_loadaddr }} + FIT_DTS: ${{ matrix.fit_dts }} + FIT_CONFIG: ${{ matrix.fit_config }} + FIT_BOOTARGS: ${{ matrix.fit_bootargs }} + DTB_PATCH_NVMEM_MAC: ${{ matrix.dtb_patch_nvmem_mac }} + DTB_FORCE_LEGACY_PARTITIONS: ${{ matrix.dtb_force_legacy_partitions }} + run: | + chmod +x tools/ci/build_image.sh + tools/ci/build_image.sh \ + "${{ matrix.imagebuilder }}" \ + "${{ matrix.profile }}" \ + "${{ matrix.openwrt_release }}" \ + "./feed-in" \ + "./out" + + - name: Upload firmware artifact + uses: actions/upload-artifact@v4 + with: + name: firmware-${{ matrix.device }}-${{ matrix.openwrt_release }} + path: ./out/firmware-${{ matrix.device }}.* + retention-days: 7 + + - name: Upload image debug files + if: failure() + uses: actions/upload-artifact@v4 + with: + name: build-debug-image-${{ matrix.device }}-${{ matrix.openwrt_release }} + path: | + ./out/ + ./feed-in/ + if-no-files-found: ignore + retention-days: 7 + + # Physical single-node test on the self-hosted lab. Runs on schedule + # (full sweep), on workflow_dispatch -f physical_single=true, and on + # pull_request (every place; gated by the physical-lab environment). + # A single approval here covers test-mesh too (test-mesh needs: + # test-firmware, so it waits for this job to finish). + test-firmware: + name: test-firmware (${{ matrix.place }} / ${{ matrix.openwrt_release }}) + needs: [prepare-matrix, build-image] + if: | + ( + github.event_name == 'schedule' + || (github.event_name == 'workflow_dispatch' && github.event.inputs.physical_single == 'true') + || github.event_name == 'pull_request' + ) + && fromJson(needs.prepare-matrix.outputs.test_targets_matrix).include[0] != null + environment: + name: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && 'physical-lab' || '' }} + runs-on: [self-hosted, testbed-fcefyn] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.prepare-matrix.outputs.test_targets_matrix) }} + env: + PYTHONUNBUFFERED: "1" + PYTEST_ADDOPTS: "--color=yes" + LG_CONSOLE: internal + steps: + - uses: actions/checkout@v6 + + - uses: actions/download-artifact@v4 + with: + name: firmware-${{ matrix.device }}-${{ matrix.openwrt_release }} + path: fw + + - uses: actions/checkout@v6 + with: + repository: fcefyn-testbed/libremesh-tests + ref: staging + path: libremesh-tests + + - uses: actions/checkout@v6 + with: + repository: aparcar/openwrt-tests + ref: main + path: openwrt-tests + + - name: Set OPENWRT_TESTS_DIR for pytest + run: echo "OPENWRT_TESTS_DIR=$GITHUB_WORKSPACE/openwrt-tests" >> "$GITHUB_ENV" + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Sync libremesh-tests environment + run: | + echo "$(date -u +'%Y-%m-%dT%H:%M:%SZ') uv sync (libremesh-tests) starting" + cd libremesh-tests && uv sync + echo "$(date -u +'%Y-%m-%dT%H:%M:%SZ') uv sync finished" + + - name: Stage firmware on TFTP + env: + DEVICE: ${{ matrix.device }} + PLACE: ${{ matrix.place }} + OPENWRT_RELEASE: ${{ matrix.openwrt_release }} + RUN_ID: ${{ github.run_id }} + run: tools/ci/lab_stage_firmware.sh + + - name: Lock labgrid place + working-directory: libremesh-tests + run: | + set -euo pipefail + echo "LG_PROXY=labgrid-fcefyn" >> "$GITHUB_ENV" + PLACE="labgrid-fcefyn-${{ matrix.place }}" + LOCK_OUT=$(uv run labgrid-client -v -p "$PLACE" lock 2>&1) || { + echo "$LOCK_OUT" + echo "::error::labgrid lock failed for $PLACE" + exit 1 + } + echo "$LOCK_OUT" + echo "LG_PLACE=$PLACE" >> "$GITHUB_ENV" + echo "LG_ENV=targets/${{ matrix.device }}.yaml" >> "$GITHUB_ENV" + echo "Locked $PLACE for device=${{ matrix.device }}" + + - name: Run single-node LibreMesh tests + working-directory: libremesh-tests + env: + LG_PROXY: labgrid-fcefyn + LG_IMAGE: ${{ env.LG_IMAGE }} + LG_IMAGE_INITRD: ${{ env.LG_IMAGE_INITRD }} + LG_PLACE: ${{ env.LG_PLACE }} + LG_ENV: ${{ env.LG_ENV }} + LG_MESH_UBOOT_INTERRUPT_SPAM_SEC: ${{ matrix.uboot_interrupt_spam_sec }} + run: | + set -euo pipefail + LOG_DIR="$GITHUB_WORKSPACE/logs/${{ matrix.place }}-${{ matrix.openwrt_release }}" + mkdir -p "$LOG_DIR" + uv run pytest tests/test_libremesh.py \ + --lg-log "$LOG_DIR/" \ + --junitxml="$LOG_DIR/report.xml" \ + --lg-colored-steps \ + --log-cli-level=CONSOLE + + - name: Collect lime-report from DUT + id: lime-report + if: always() && env.LG_PLACE != '' + working-directory: libremesh-tests + env: + LG_PLACE: ${{ env.LG_PLACE }} + run: | + set +e + REPORT=$(uv run labgrid-client -p "$LG_PLACE" ssh -- lime-report.sh -m 2>/dev/null | head -c 50000) + if [ -z "$REPORT" ]; then + REPORT="_Could not collect lime-report (device unreachable or command not found)._" + fi + echo "$REPORT" > "$GITHUB_WORKSPACE/lime-report-${{ matrix.place }}.md" + echo "collected=true" >> "$GITHUB_OUTPUT" + + - name: Poweroff and unlock device + if: always() + working-directory: libremesh-tests + env: + LG_PLACE: ${{ env.LG_PLACE }} + run: | + set +e + if [ -n "${LG_PLACE:-}" ]; then + uv run labgrid-client -p "$LG_PLACE" power off || true + uv run labgrid-client -p "$LG_PLACE" unlock || true + else + echo "No labgrid lock acquired by this job; skipping power off / unlock." + fi + rm -rf "/srv/tftp/firmwares/ci/${{ github.run_id }}/${{ matrix.place }}/${{ matrix.openwrt_release }}" 2>/dev/null || true + + - name: Upload test logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.place }}-${{ matrix.openwrt_release }} + path: ${{ github.workspace }}/logs/${{ matrix.place }}-${{ matrix.openwrt_release }}/* + if-no-files-found: ignore + retention-days: 14 + + - name: Open or close issue on test result + if: always() && github.event_name == 'schedule' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const device = "${{ matrix.device }}"; + const place = "${{ matrix.place }}"; + const release = "${{ matrix.openwrt_release }}"; + const issueTitle = `CI healthcheck: ${place} (${release})`; + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const isSuccess = "${{ job.status }}" === "success"; + + let limeReport = "_Not collected._"; + const reportPath = `${process.env.GITHUB_WORKSPACE}/lime-report-${place}.md`; + try { limeReport = fs.readFileSync(reportPath, 'utf8').trim(); } catch {} + + const failureBody = [ + `### CI healthcheck failed`, + ``, + `| Field | Value |`, + `|-------|-------|`, + `| **Place** | \`${place}\` |`, + `| **Device** | \`${device}\` |`, + `| **OpenWrt release** | \`${release}\` |`, + `| **Run** | [#${context.runId}](${runUrl}) |`, + `| **Date** | ${new Date().toISOString().slice(0,10)} |`, + ``, + `### lime-report`, + ``, + `
Click to expand`, + ``, + '```', + limeReport, + '```', + `
`, + ].join('\n'); + + const issues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'all', + labels: 'healthcheck', + per_page: 100, + }); + + let testIssue = issues.data.find(i => i.title === issueTitle); + + if (isSuccess) { + if (testIssue && testIssue.state === 'open') { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: testIssue.number, + body: `Healthcheck passed on [run #${context.runId}](${runUrl}). Closing.`, + }); + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: testIssue.number, + state: 'closed', + }); + console.log(`Closed issue #${testIssue.number} (test passed).`); + } + } else { + if (testIssue) { + if (testIssue.state === 'closed') { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: testIssue.number, + state: 'open', + body: failureBody, + }); + console.log(`Reopened issue #${testIssue.number}.`); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: testIssue.number, + body: failureBody, + }); + console.log(`Commented on open issue #${testIssue.number}.`); + } + } else { + const created = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: issueTitle, + body: failureBody, + labels: ['healthcheck'], + }); + console.log(`Created issue #${created.data.number}.`); + } + } + + # Physical mesh test (N=2 or N=3 places). Runs on workflow_dispatch + # (-f physical_mesh_count=2|3) and on pull_request (forced N=3). + # Daily mesh coverage comes from `test-mesh-pairs`. + test-mesh: + name: test-mesh (N=${{ github.event.inputs.physical_mesh_count || '3 (PR)' }} / ${{ matrix.openwrt_release }}) + # test-firmware is in `needs` only to serialise lab access (both reserve + # the same labgrid places). When test-firmware is skipped via dispatch + # inputs we still want this job to run, so `if:` uses `!cancelled()` and + # checks needs.test-firmware.result != 'failure' instead of relying on + # GitHub's default skip-cascade behaviour. + needs: [prepare-matrix, build-image, test-firmware] + if: | + !cancelled() + && needs.prepare-matrix.result == 'success' + && needs.build-image.result == 'success' + && needs.test-firmware.result != 'failure' + && ( + (github.event_name == 'workflow_dispatch' && github.event.inputs.physical_mesh_count != '0') + || github.event_name == 'pull_request' + ) + && fromJson(needs.prepare-matrix.outputs.mesh_test_matrix).include[0] != null + runs-on: [self-hosted, testbed-fcefyn] + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.prepare-matrix.outputs.mesh_test_matrix) }} + env: + PYTHONUNBUFFERED: "1" + PYTEST_ADDOPTS: "--color=yes" + LG_CONSOLE: internal + steps: + - uses: actions/checkout@v6 + + - uses: actions/checkout@v6 + with: + repository: fcefyn-testbed/libremesh-tests + ref: staging + path: libremesh-tests + + - uses: actions/checkout@v6 + with: + repository: aparcar/openwrt-tests + ref: main + path: openwrt-tests + + - name: Set OPENWRT_TESTS_DIR for pytest + run: echo "OPENWRT_TESTS_DIR=$GITHUB_WORKSPACE/openwrt-tests" >> "$GITHUB_ENV" + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Sync libremesh-tests environment + run: cd libremesh-tests && uv sync + + # Static download steps gated by `contains(matrix.devices, ...)`. + # N=2 fires only openwrt_one + bananapi_bpi-r4; N=3 also pulls + # linksys_e8450 (consumed by belkin_rt3200_2). + - name: Download firmware (openwrt_one) + if: contains(matrix.devices, 'openwrt_one') + uses: actions/download-artifact@v4 + with: + name: firmware-openwrt_one-${{ matrix.openwrt_release }} + path: fw-mesh/openwrt_one + + - name: Download firmware (bananapi_bpi-r4) + if: contains(matrix.devices, 'bananapi_bpi-r4') + uses: actions/download-artifact@v4 + with: + name: firmware-bananapi_bpi-r4-${{ matrix.openwrt_release }} + path: fw-mesh/bananapi_bpi-r4 + + - name: Download firmware (linksys_e8450) + if: contains(matrix.devices, 'linksys_e8450') + uses: actions/download-artifact@v4 + with: + name: firmware-linksys_e8450-${{ matrix.openwrt_release }} + path: fw-mesh/linksys_e8450 + + - name: Stage firmware and run mesh tests + env: + MODE: full + OPENWRT_RELEASE: ${{ matrix.openwrt_release }} + RUN_ID: ${{ github.run_id }} + MESH_PLACES_JSON: ${{ toJson(matrix.places) }} + MESH_DEVICES_JSON: ${{ toJson(matrix.devices) }} + SRC_DIR: fw-mesh + LOGS_DIR: mesh-logs + run: tools/ci/lab_stage_mesh.sh + + - name: Cleanup TFTP on failure + if: failure() + run: rm -rf "/srv/tftp/firmwares/ci/${{ github.run_id }}/mesh/${{ matrix.openwrt_release }}" || true + + - name: Upload mesh test logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-mesh-${{ matrix.openwrt_release }} + path: mesh-logs/* + if-no-files-found: ignore + retention-days: 14 + + # Walking-chain mesh smoke. Three sequential 2-node pairs share devices + # on purpose; max-parallel:1 serialises lab access. Runs on daily cron + # and on workflow_dispatch with physical_mesh_pairs=true. + # Place layout is computed in `prepare_matrix.sh::mesh_pairs_matrix`. + test-mesh-pairs: + name: test-mesh-pairs (#${{ matrix.pair }} ${{ matrix.place_a }}+${{ matrix.place_b }} / ${{ matrix.openwrt_release }}) + # test-firmware is in `needs` only to serialise lab access (both reserve + # the same labgrid places). When test-firmware is skipped via dispatch + # inputs we still want this job to run, so `if:` uses `!cancelled()` and + # checks needs.test-firmware.result != 'failure' instead of relying on + # GitHub's default skip-cascade behaviour. + needs: [prepare-matrix, build-image, test-firmware] + if: | + !cancelled() + && needs.prepare-matrix.result == 'success' + && needs.build-image.result == 'success' + && needs.test-firmware.result != 'failure' + && ( + github.event_name == 'schedule' + || (github.event_name == 'workflow_dispatch' && github.event.inputs.physical_mesh_pairs == 'true') + ) + && fromJson(needs.prepare-matrix.outputs.mesh_pairs_matrix).include[0] != null + runs-on: [self-hosted, testbed-fcefyn] + strategy: + fail-fast: false + max-parallel: 1 + matrix: ${{ fromJson(needs.prepare-matrix.outputs.mesh_pairs_matrix) }} + env: + PYTHONUNBUFFERED: "1" + PYTEST_ADDOPTS: "--color=yes" + LG_CONSOLE: internal + steps: + - uses: actions/checkout@v6 + + - uses: actions/checkout@v6 + with: + repository: fcefyn-testbed/libremesh-tests + ref: staging + path: libremesh-tests + + - uses: actions/checkout@v6 + with: + repository: aparcar/openwrt-tests + ref: main + path: openwrt-tests + + - name: Set OPENWRT_TESTS_DIR for pytest + run: echo "OPENWRT_TESTS_DIR=$GITHUB_WORKSPACE/openwrt-tests" >> "$GITHUB_ENV" + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Sync libremesh-tests environment + run: cd libremesh-tests && uv sync + + - name: Download firmware (${{ matrix.device_a }}) + uses: actions/download-artifact@v4 + with: + name: firmware-${{ matrix.device_a }}-${{ matrix.openwrt_release }} + path: fw-pair/${{ matrix.device_a }} + + # Skip the second download when both endpoints share the same + # device artifact (download-artifact errors on a duplicate path). + - name: Download firmware (${{ matrix.device_b }}) + if: matrix.device_a != matrix.device_b + uses: actions/download-artifact@v4 + with: + name: firmware-${{ matrix.device_b }}-${{ matrix.openwrt_release }} + path: fw-pair/${{ matrix.device_b }} + + - name: Stage firmware and run mesh tests + env: + MODE: pair + OPENWRT_RELEASE: ${{ matrix.openwrt_release }} + RUN_ID: ${{ github.run_id }} + PAIR: ${{ matrix.pair }} + PLACE_A: ${{ matrix.place_a }} + DEVICE_A: ${{ matrix.device_a }} + PLACE_B: ${{ matrix.place_b }} + DEVICE_B: ${{ matrix.device_b }} + SRC_DIR: fw-pair + LOGS_DIR: mesh-pairs-logs + run: tools/ci/lab_stage_mesh.sh + + - name: Cleanup TFTP on failure + if: failure() + run: rm -rf "/srv/tftp/firmwares/ci/${{ github.run_id }}/mesh-pairs/${{ matrix.pair }}/${{ matrix.openwrt_release }}" || true + + - name: Upload mesh-pairs test logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-mesh-pairs-${{ matrix.pair }}-${{ matrix.openwrt_release }} + path: mesh-pairs-logs/* + if-no-files-found: ignore + retention-days: 14 + + # QEMU single-node smoke test on a GitHub-hosted runner. One job per + # release, for every target with `test_qemu: true`. Runs in TCG mode + # (KVM only available on the multi-node mesh job below). + test-firmware-qemu-single: + name: test-firmware-qemu-single (${{ matrix.device }} / ${{ matrix.openwrt_release }}) + needs: [prepare-matrix, build-image] + if: | + fromJson(needs.prepare-matrix.outputs.qemu_single_matrix).include[0] != null + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.prepare-matrix.outputs.qemu_single_matrix) }} + env: + PYTHONUNBUFFERED: "1" + PYTEST_ADDOPTS: "--color=yes" + steps: + - uses: actions/checkout@v6 + + - uses: actions/checkout@v6 + with: + repository: fcefyn-testbed/libremesh-tests + ref: staging + path: libremesh-tests + + - uses: actions/checkout@v6 + with: + repository: aparcar/openwrt-tests + ref: main + path: openwrt-tests + + - name: Set OPENWRT_TESTS_DIR for pytest + run: echo "OPENWRT_TESTS_DIR=$GITHUB_WORKSPACE/openwrt-tests" >> "$GITHUB_ENV" + + - name: Download firmware artifact + uses: actions/download-artifact@v5 + with: + name: firmware-${{ matrix.device }}-${{ matrix.openwrt_release }} + path: fw + + - name: Install QEMU + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends qemu-system-x86 ovmf + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Sync libremesh-tests environment + run: | + cd libremesh-tests + uv sync + + - name: Locate firmware image + id: firmware + run: | + set -euo pipefail + shopt -s nullglob + candidates=(fw/firmware-${{ matrix.device }}.img) + if [[ ${#candidates[@]} -eq 0 ]]; then + echo "::error::No firmware-${{ matrix.device }}.img under fw/" >&2 + ls -la fw/ || true + exit 1 + fi + echo "path=${candidates[0]}" >> "$GITHUB_OUTPUT" + file "${candidates[0]}" || true + ls -la "${candidates[0]}" + sha256sum "${candidates[0]}" + + - name: Run single-node QEMU LibreMesh tests + working-directory: libremesh-tests + env: + # The qemu_x86-64_libremesh.yaml env in libremesh-tests + # configures a QEMUDriver + QEMUNetworkStrategyLibreMesh + # that boots the supplied disk image, waits for dropbear + # (~30s after lime-config completes), and forwards SSH + # to the guest's anygw 10.13.0.1:22. This is the + # canonical LibreMesh-on-x86_64 test path. + LG_ENV: targets/qemu_x86-64_libremesh.yaml + run: | + set -euo pipefail + LOG_DIR="$GITHUB_WORKSPACE/qemu-single-logs/${{ matrix.device }}-${{ matrix.openwrt_release }}" + mkdir -p "$LOG_DIR" + # Single-node validation: lime-* runtime, base shell, LAN. + # Multi-node coverage lives in `test-mesh-qemu` below. + uv run pytest \ + tests/test_libremesh.py \ + tests/test_base.py \ + tests/test_lan.py \ + --lg-env "${LG_ENV}" \ + --firmware "$GITHUB_WORKSPACE/${{ steps.firmware.outputs.path }}" \ + --lg-log "$LOG_DIR/" \ + --junitxml="$LOG_DIR/report.xml" \ + --log-cli-level=INFO \ + -v + + - name: Upload QEMU single-node test logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-qemu-single-${{ matrix.device }}-${{ matrix.openwrt_release }} + path: ${{ github.workspace }}/qemu-single-logs/${{ matrix.device }}-${{ matrix.openwrt_release }}/* + if-no-files-found: ignore + retention-days: 14 + + # QEMU multi-node mesh test. Spawns N=3 QEMU instances of the + # x86-combined image tied through vwifi-server (mac80211_hwsim ↔ + # 802.11s). Requires KVM acceleration (TCG can't fit three VMs in + # the runner budget) and a vwifi-server binary on PATH. + test-mesh-qemu: + name: test-mesh-qemu (${{ matrix.device }} / ${{ matrix.openwrt_release }}) + needs: [prepare-matrix, build-image, test-firmware-qemu-single] + if: | + fromJson(needs.prepare-matrix.outputs.qemu_mesh_matrix).include[0] != null + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.prepare-matrix.outputs.qemu_mesh_matrix) }} + env: + PYTHONUNBUFFERED: "1" + PYTEST_ADDOPTS: "--color=yes" + steps: + - uses: actions/checkout@v6 + + - uses: actions/checkout@v6 + with: + repository: fcefyn-testbed/libremesh-tests + ref: staging + path: libremesh-tests + + - uses: actions/checkout@v6 + with: + repository: aparcar/openwrt-tests + ref: main + path: openwrt-tests + + - name: Set OPENWRT_TESTS_DIR for pytest + run: echo "OPENWRT_TESTS_DIR=$GITHUB_WORKSPACE/openwrt-tests" >> "$GITHUB_ENV" + + - name: Install QEMU and build deps + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + qemu-system-x86 ovmf cmake build-essential libnl-3-dev libnl-genl-3-dev pkg-config + + - name: Enable KVM access for runner user + run: tools/ci/enable_kvm.sh + + # vwifi-server pinned at Raizo62/vwifi@4a9842e6 (release v7.0). + # The in-guest vwifi-client comes from packages/vwifi/. + - name: Cache vwifi-server build + id: cache-vwifi-server + uses: actions/cache@v4 + with: + path: ~/.vwifi-server-bin + key: vwifi-server-4a9842e6-${{ runner.os }} + + - name: Build vwifi-server from source + if: steps.cache-vwifi-server.outputs.cache-hit != 'true' + run: | + set -euo pipefail + mkdir -p ~/.vwifi-server-bin + tmp=$(mktemp -d) + git clone https://github.com/Raizo62/vwifi.git "$tmp/vwifi" + git -C "$tmp/vwifi" checkout 4a9842e6 + cmake -S "$tmp/vwifi" -B "$tmp/build" + cmake --build "$tmp/build" -j"$(nproc)" + for bin in vwifi-server vwifi-client vwifi-ctrl; do + cp "$tmp/build/$bin" ~/.vwifi-server-bin/ + done + rm -rf "$tmp" + + - name: Install vwifi-server + run: | + sudo install -m 0755 ~/.vwifi-server-bin/vwifi-server /usr/local/bin/ + vwifi-server --help || vwifi-server -h || true + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Sync libremesh-tests environment + run: | + cd libremesh-tests + uv sync + + - name: Download firmware artifact + uses: actions/download-artifact@v5 + with: + name: firmware-${{ matrix.device }}-${{ matrix.openwrt_release }} + path: fw + + - name: Run multi-node QEMU mesh tests + working-directory: libremesh-tests + env: + # `LG_VIRTUAL_MESH=1` switches the `mesh_nodes` fixture from the + # labgrid path to virtual_mesh_launcher.launch_virtual_mesh(). + LG_VIRTUAL_MESH: "1" + VIRTUAL_MESH_NODES: "3" + VIRTUAL_MESH_MAX_NODES: "3" + VIRTUAL_MESH_IMAGE: ${{ github.workspace }}/fw/firmware-${{ matrix.device }}.img + run: | + set -euo pipefail + LOG_DIR="$GITHUB_WORKSPACE/qemu-mesh-logs/${{ matrix.device }}-${{ matrix.openwrt_release }}" + mkdir -p "$LOG_DIR" + vwifi-server -u >"$LOG_DIR/vwifi-server.log" 2>&1 & + echo $! > "$LOG_DIR/vwifi-server.pid" + trap 'kill "$(cat "$LOG_DIR/vwifi-server.pid")" 2>/dev/null || true' EXIT + sleep 1 + uv run pytest tests/test_mesh.py \ + --lg-log "$LOG_DIR/" \ + --junitxml="$LOG_DIR/report.xml" \ + --log-cli-level=INFO \ + -v + + - name: Upload QEMU mesh test logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-qemu-mesh-${{ matrix.device }}-${{ matrix.openwrt_release }} + path: ${{ github.workspace }}/qemu-mesh-logs/${{ matrix.device }}-${{ matrix.openwrt_release }}/* + if-no-files-found: ignore + retention-days: 14 + + summary: + runs-on: ubuntu-latest + if: always() + needs: + - prepare-matrix + - build-feed + - build-image + - test-firmware + - test-mesh + - test-mesh-pairs + - test-firmware-qemu-single + - test-mesh-qemu + steps: + - uses: actions/checkout@v6 + + - name: Build summary + env: + TRIGGER: ${{ github.event_name }} + TARGETS_MATRIX: ${{ needs.prepare-matrix.outputs.targets_matrix }} + ARCHS_MATRIX: ${{ needs.prepare-matrix.outputs.archs_matrix }} + TEST_TARGETS_MATRIX: ${{ needs.prepare-matrix.outputs.test_targets_matrix }} + MESH_TEST_MATRIX: ${{ needs.prepare-matrix.outputs.mesh_test_matrix }} + MESH_PAIRS_MATRIX: ${{ needs.prepare-matrix.outputs.mesh_pairs_matrix }} + QEMU_SINGLE_MATRIX: ${{ needs.prepare-matrix.outputs.qemu_single_matrix }} + QEMU_MESH_MATRIX: ${{ needs.prepare-matrix.outputs.qemu_mesh_matrix }} + BUILD_FEED_RESULT: ${{ needs.build-feed.result }} + BUILD_IMAGE_RESULT: ${{ needs.build-image.result }} + TEST_FIRMWARE_RESULT: ${{ needs.test-firmware.result }} + TEST_MESH_RESULT: ${{ needs.test-mesh.result }} + TEST_MESH_PAIRS_RESULT: ${{ needs.test-mesh-pairs.result }} + TEST_FIRMWARE_QEMU_SINGLE_RESULT: ${{ needs.test-firmware-qemu-single.result }} + TEST_MESH_QEMU_RESULT: ${{ needs.test-mesh-qemu.result }} + run: tools/ci/build_summary.sh diff --git a/luacov.report.out b/luacov.report.out new file mode 100644 index 000000000..9320e25c6 --- /dev/null +++ b/luacov.report.out @@ -0,0 +1,9551 @@ +============================================================================== +./packages/pirania/tests/pirania_test_utils.lua +============================================================================== + + + 3 local utils = {} + + 3 function utils.fake_for_tests() + 29 local hooks = require('voucher.hooks') + 29 local config = require('voucher.config') + + 29 config.db_path = '/tmp/pirania_vouchers' + 29 os.execute("mkdir -p " .. config.db_path) + 29 config.prune_expired_for_days = '30' + + 57 hooks.run = function(action) end + end + + 3 return utils + + +============================================================================== +./tests/utils.lua +============================================================================== + 43 local limeutils = require 'lime.utils' + 43 local config = require 'lime.config' + 43 local libuci = require 'uci' + 43 local stub = require 'luassert.stub' + + 43 local utils = {} + + 43 utils.assert = assert + + 43 UCI_CONFIG_FILES = { + 43 "6relayd", "babeld", "batman-adv", "check-date", "dhcp", "dropbear", "fstab", "firewall", + 43 "libremap", "lime", "lime-app", "location", + 43 "luci", "network", "pirania", "rpcd", "shared-state", "system", "ucitrack", + 43 "uhttpd", "wireless", "deferrable-reboot", config.UCI_AUTOGEN_NAME, config.UCI_NODE_NAME, + 43 config.UCI_COMMUNITY_NAME, config.UCI_DEFAULTS_NAME + 43 } + + 43 function utils.disable_asserts() + 1 _G['assert'] = function(expresion, message) return expresion end + end + + 43 function utils.enable_asserts() + 1 _G['assert'] = utils.assert + end + + 43 function utils.lua_path_from_pkgname(pkgname) + 4 return 'packages/' .. pkgname .. '/files/usr/lib/lua/?.lua;' + end + + 43 function utils.enable_package(pkgname) + 2 path = utils.lua_path_from_pkgname(pkgname) + 2 if string.find(package.path, path) == nil then + 2 package.path = path .. package.path + end + end + + 43 function utils.disable_package(pkgname, modulename) + -- remove pkg from LUA search path + 2 path = utils.lua_path_from_pkgname(pkgname) + 2 package.path = string.gsub(package.path, limeutils.literalize(path), '') + -- remove module from preload table + 2 package.preload[modulename] = nil + 2 package.loaded[modulename] = nil + 2 _G[modulename] = nil + end + + -- Creates a custom empty uci environment to be used in unittesting. + -- Should be called in a before_each block and must be followed by a call to + -- teardown_test_uci in an after_each block. + 43 function utils.setup_test_uci() + 211 local uci = libuci:cursor() + 211 config.set_uci_cursor(uci) + 211 local tmpdir = io.popen("mktemp -d"):read('*l') + 211 uci:set_confdir(tmpdir) + -- If the uci files does not exists then doing uci add fails + -- so here we create empty config files + 5697 for _, cfgname in ipairs(UCI_CONFIG_FILES) do + 5486 local f = io.open(tmpdir .. '/' .. cfgname, "w"):close() + end + 211 return uci + end + + 43 function utils.teardown_test_uci(uci) + 211 local confdir = uci:get_confdir() + 211 if(string.find(confdir, '^/tmp') ~= nil) then + 211 local out = io.popen("rm -rf " .. confdir .. " " .. uci:get_savedir()) + 211 out:read('*all') -- this allows waiting for popen completion + 211 out:close() + 211 io.popen("rm -rf " .. confdir .. " " .. uci:get_savedir()) + end + 211 uci:close() + end + + -- Create a temporal empty directory and return its path with a trailin '/' + -- eg: '/tmp/tmp.occigb/' + -- utils.teardown_test_dir() must be called for cleanup + 43 function utils.setup_test_dir() + 50 utils._tmpdir = io.popen("mktemp -d"):read('*l') .. "/" + 50 return utils._tmpdir + end + + 43 function utils.teardown_test_dir() + 71 if(utils._tmpdir ~= nil and string.find(utils._tmpdir, '^/tmp') ~= nil) then + 49 local out = io.popen("rm -rf " .. utils._tmpdir) + 49 out:read('*all') -- this allows waiting for popen completion + 49 out:close() + 49 utils._tmpdir = nil + end + end + + 43 function utils.get_board(name) + 1 local board_path = 'tests/devices/' .. name .. '/board.json' + 1 return limeutils.getBoardAsTable(board_path) + end + + 43 function utils.write_uci_file(uci, filename, content) + 59 local confdir = uci:get_confdir() + 59 local f = io.open(confdir .. '/' .. filename, "w") + 59 f:write(content) + 59 f:close() + end + + 43 function utils.read_uci_file(uci, filename) + 1 local confdir = uci:get_confdir() + 1 local f = io.open(confdir .. '/' .. filename, "r") + 1 local content = nil + 1 if f ~= nil then + 1 content = f:read('*all') + 1 f:close() + end + 1 return content + end + + 43 function utils.load_lua_file_as_function(filename) + 15 local f = io.open(filename) + + 15 local content = "" + + -- removes the shebang if it is the first line + 15 local first_line = f:read("*l") + 15 if not first_line:find("^#!") then +****0 content = first_line .. content + end + + 15 content = content .. f:read("*a") + 15 f:close() + + -- mimic lua executable arguments handling injecting arg variable from varargs + 15 content = 'local arg = {...}\n' .. content + 15 return loadstring(content, filename) + end + + -- Use this function to test libexec/rpcd "json API" calls + 43 function utils.rpcd_call(f, script_args, call_args) + local response = nil + + 132 stub(limeutils, "printJson", function (x) response = x end) + 132 stub(limeutils, "rpcd_readline", function () return call_args end) + + 69 f(unpack(script_args)) + + -- revert the stub + 69 limeutils.printJson:revert() + 69 limeutils.rpcd_readline:revert() + 69 return response + end + + 43 return utils + +============================================================================== +packages/check-internet/files/usr/libexec/rpcd/check-internet +============================================================================== + #!/usr/bin/env lua + --[[ + Copyright (C) 2021 LibreMesh.org + This is free software, licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + + Copyright 2021 Santiago Piccinini + ]]-- + + 3 local ubus = require "ubus" + 3 local json = require "luci.jsonc" + 3 local utils = require "lime.utils" + + 3 local conn = ubus.connect() + 3 if not conn then +****0 error("Failed to connect to ubus") + end + + local function is_connected() + 2 local exit_status = os.execute('check-internet') + 2 local connected = false + 2 if exit_status == 0 then + 1 connected = true + end + 2 return utils.printJson({status = 'ok', connected = connected}) + end + + 3 local methods = { + 3 is_connected = { no_params = 0 } + } + + 3 if arg[1] == 'list' then + 1 utils.printJson(methods) + end + + 3 if arg[1] == 'call' then + 2 local msg = utils.rpcd_readline() + 2 msg = json.parse(msg) + 2 if arg[2] == 'is_connected' then is_connected(msg) +****0 else utils.printJson({ error = "Method not found" }) + end + end + +============================================================================== +packages/deferrable-reboot/files/usr/lib/lua/deferrable_reboot.lua +============================================================================== + 1 local utils = require "lime.utils" + 1 local config = require "lime.config" + + 1 local defreboot = {} + + 1 defreboot.DEFAULT_REBOOT_UPTIME = 60*60*27 + 1 defreboot.SLEEP_BEFORE_REBOOT_S = 30 + + 1 defreboot.POSTPONE_FILE_PATH = '/tmp/deferrable-reboot.defer' + + 1 function defreboot.config(min_uptime) + 11 if min_uptime == nil then + 4 local uci = config.get_uci_cursor() + 4 local lime_min_uptime = config.get("system", "deferrable_reboot_uptime_s", false) + 4 local general_min_uptime = uci:get("deferrable-reboot", "options", "deferrable_reboot_uptime_s") + 4 min_uptime = tonumber(lime_min_uptime or general_min_uptime or defreboot.DEFAULT_REBOOT_UPTIME) + end + 11 assert(type(min_uptime) == "number", "min_uptime must be a number") + 11 defreboot.min_uptime = min_uptime + end + + 1 function defreboot.should_reboot() + 7 local uptime_s = utils.uptime_s() + 7 local postpone_until_s = defreboot.read_postpone_file() + 7 local min_uptime = defreboot.min_uptime + + 7 if (postpone_until_s ~= nil) and (postpone_until_s > min_uptime) then + 3 min_uptime = postpone_until_s + end + 7 return uptime_s > min_uptime + end + + 1 function defreboot.postpone_util_s(uptime) + 3 assert(type(uptime) == "number", "uptime must be a number") + 3 local f = io.open(defreboot.POSTPONE_FILE_PATH, 'w') + 3 f:write(tostring(uptime)) + 3 f:close() + end + + + --! use this function to postpone the reboot, also the following command can be used + --! replacing SECONDS: # awk '{print $1 + SECONDS}' /proc/uptime > /tmp/deferrable-reboot.defer + 1 function defreboot.read_postpone_file() + 7 local f = io.open(defreboot.POSTPONE_FILE_PATH) + 7 if f ~= nil then + 3 return tonumber(f:read("*l")) + end + 4 return nil + end + + 1 function defreboot.reboot() + --! give time to sysupgrade to kill us +****0 nixio.nanosleep(defreboot.SLEEP_BEFORE_REBOOT_S) +****0 os.execute("reboot") + end + + 1 return defreboot + +============================================================================== +packages/eupgrade/files/usr/lib/lua/eupgrade.lua +============================================================================== + 1 local utils = require "lime.utils" + 1 local json = require "luci.jsonc" + 1 local libuci = require "uci" + 1 local fs = require("nixio.fs") + + 1 local eup = {} + + 1 eup.STATUS_DEFAULT = 'not-initiated' + 1 eup.STATUS_DOWNLOADING = 'downloading' + 1 eup.STATUS_DOWNLOADED = 'downloaded' + 1 eup.STATUS_DOWNLOAD_FAILED = 'download-failed' + + + 1 local uci = libuci.cursor() + + 1 function eup.set_workdir(workdir) + 9 if not utils.file_exists(workdir) then + 1 os.execute('mkdir -p ' .. workdir) + end + 9 if fs.stat(workdir, "type") ~= "dir" then +****0 error("Can't configure workdir " .. workdir) + end + 9 eup.WORKDIR = workdir + 9 eup.DOWNLOAD_INFO_CACHE_FILE = eup.WORKDIR .. '/download_status' + 9 eup.FIRMWARE_LATEST_JSON = eup.WORKDIR .. "/firmware_latest.json" + 9 eup.FIRMWARE_LATEST_JSON_SIGNATURE = eup.FIRMWARE_LATEST_JSON .. '.sig' + end + + 1 eup.set_workdir("/tmp/eupgrades") + + 1 function eup.is_enabled() +****0 return uci:get('eupgrade', 'main', 'enabled') == '1' + end + + 1 function eup.get_upgrade_api_url() + 6 return uci:get('eupgrade', 'main', 'api_url') or '' + end + + 1 function eup._check_signature(file_path, signature_path) +****0 local cmd = string.format("usign -q -V -P /etc/opkg/keys -x %s -m %s", +****0 signature_path, file_path) +****0 local exit_status = os.execute(cmd) +****0 return exit_status == 0 + end + + 1 function eup._get_board_name() +****0 return utils.read_file("/tmp/sysinfo/board_name"):gsub("\n","") + end + + 1 function eup._get_current_fw_version() +****0 return utils.release_info()["DISTRIB_DESCRIPTION"] + end + + 1 function eup._file_sha256(path) +****0 return utils.unsafe_shell(string.format("sha256sum %s", path)):match("^([^%s]+)") + end + + + --! check if a new firmware is available for download, returning the information of the version + --! when cached_only is true it will not hit the network (only checking the local cache) + 1 function eup.is_new_version_available(cached_only) + --! if 'latest' files are present is because there is a new version + 4 if utils.file_exists(eup.FIRMWARE_LATEST_JSON) and utils.file_exists(eup.FIRMWARE_LATEST_JSON_SIGNATURE) then +****0 if eup._check_signature(eup.FIRMWARE_LATEST_JSON, eup.FIRMWARE_LATEST_JSON_SIGNATURE) then +****0 return json.parse(utils.read_file(eup.FIRMWARE_LATEST_JSON)) + end + end + 4 if cached_only then +****0 return false + end + local message + 4 local board_name = eup._get_board_name() + 4 local current_firmware_version = eup._get_current_fw_version() + 4 local url = string.format("%slatest/%s.json", eup.get_upgrade_api_url(), utils.slugify(board_name)) + 4 local latest_json = utils.http_client_get(url, 10) + 4 if not latest_json then + 1 message = "Can't download latest info from: " .. url + else + 3 local latest_data = json.parse(latest_json) + 3 local version = latest_data['version'] + + 3 if version and current_firmware_version ~= version then + 2 utils.write_file(eup.FIRMWARE_LATEST_JSON, latest_json) + 2 local sig_url = url .. ".sig" + 2 if not utils.http_client_get(sig_url, 10, eup.FIRMWARE_LATEST_JSON_SIGNATURE) then +****0 message = "Can't download signature " .. sig_url +****0 utils.log(message) + end + + 2 if eup._check_signature(eup.FIRMWARE_LATEST_JSON, eup.FIRMWARE_LATEST_JSON_SIGNATURE) then + 1 utils.log("Good signature of firmware_latest.json") + 1 return latest_data + else + 1 message = "Bad signature of firmware_latest.json" + 1 utils.log(message) + end + end + end + --! remove the 'latest' files. + 3 utils.unsafe_shell(string.format('rm -f %s %s', eup.FIRMWARE_LATEST_JSON, eup.FIRMWARE_LATEST_JSON_SIGNATURE)) + 3 return false, message + end + + 1 function eup.get_latest_info() +****0 if utils.file_exists(eup.FIRMWARE_LATEST_JSON) then +****0 return json.parse(utils.read_file(eup.FIRMWARE_LATEST_JSON)) + end + end + + 1 function eup.get_downloaded_info() +****0 local latest_data = eup.get_latest_info() +****0 if latest_data then +****0 for _, image in pairs(latest_data['images']) do +****0 local fw_type = image['type'] +****0 local firmware_path = eup.WORKDIR .. "/" .. image['name'] +****0 if utils.file_exists(firmware_path) then +****0 return firmware_path, fw_type + end + end + end + end + + 1 function eup.set_download_status(status) + 6 return utils.write_obj_store_var(eup.DOWNLOAD_INFO_CACHE_FILE, 'status', status) + end + + 1 function eup.get_download_status() + 8 local data = utils.read_obj_store(eup.DOWNLOAD_INFO_CACHE_FILE) + 8 if data.status == nil then + 3 return eup.STATUS_DEFAULT + else + 5 return data.status + end + end + + 1 function eup.download_firmware(latest_data) + 3 if eup.get_download_status() == eup.STATUS_DOWNLOADING then + 1 return nil, "Already downloading" + end + + local image, message + + -- Select the image type, discarding unknown types. Prefer image installer over sysupgrade + 2 for _, im in pairs(latest_data['images']) do + 2 if im['type'] == 'installer' then + 2 image = im + 2 break +****0 elseif im['type'] == 'sysupgrade' then +****0 image = im + end + end + + 2 if image then + 2 eup.set_download_status(eup.STATUS_DOWNLOADING) + 3 for _, url in pairs(image['download-urls']) do + 2 if not string.match(url, "://") then + 2 url = eup.get_upgrade_api_url() .. url + end + 2 utils.log("Downloading the firmware from " .. url) + + 2 local firmware_path = eup.WORKDIR .. "/" .. image['name'] + 2 local download_status = utils.http_client_get(url, 10, firmware_path) + 2 if download_status then + 2 if image['sha256'] ~= eup._file_sha256(firmware_path) then + 1 message = "Error: the sha256 does not match" + 1 utils.log(message) + 1 utils.unsafe_shell('rm -f ' .. firmware_path) + else + 1 utils.log("Firmware downloaded ok") + 1 eup.set_download_status(eup.STATUS_DOWNLOADED) + 1 return image + end + end + end + end + + 1 eup.set_download_status(eup.STATUS_DOWNLOAD_FAILED) + 1 return nil, message + end + + 1 return eup + +============================================================================== +packages/first-boot-wizard/files/usr/lib/lua/firstbootwizard.lua +============================================================================== + #!/usr/bin/lua + + -- FIRSTBOOTWIZARD + -- get_all_networks: Perform scan and fetch configurations + -- apply_file_config: Set lime-community and apply configurations + -- apply_user_configs: Set a new mesh network + -- check_scan_file: Return /tmp/scanning status + -- is_configured: returns true if FBW has already configured the node + -- mark_as_configured: save the status of FBW as configured (is_configured will return true) + -- read_configs: Return scan results + + 1 local json = require 'luci.jsonc' + 1 local ft = require('firstbootwizard.functools') + 1 local utils = require('firstbootwizard.utils') + 1 local iwinfo = require("iwinfo") + 1 local wireless = require("lime.wireless") + 1 local fs = require("nixio.fs") + 1 local config = require("lime.config") + 1 local lutils = require("lime.utils") + 1 local nixio = require "nixio" + 1 local uci = require "uci" + + 1 local fbw = {} + + 1 fbw.WORKDIR = '/tmp/fbw/' + 1 fbw.COMMUNITY_HOST_CONFIG_PREFIX = 'lime-community__host__' + 1 fbw.COMMUNITY_ASSETS_TMPL = 'lime-community_assets__host__%s.tar.gz' + 1 fbw.SCAN_RESULTS_FILE = 'lime-scan-results.json' + + 1 fbw.FETCH_CONFIG_STATUS = { + 1 downloaded_config = { + 1 retval = true, code = "downloaded_config" + 1 }, + 1 downloading_config = { + 1 retval = true, code = "downloading_config" + 1 }, + 1 error_download_lime_community = { + 1 retval = false, code = "error_download_lime_community" + 1 }, + 1 error_not_configured = { + 1 retval = false, code = "error_not_configured" + 1 }, + 1 error_download_lime_assets = { + 1 retval = false, code = "error_download_lime_assets" + 1 }, + 1 } + + + 1 utils.execute('mkdir -p ' .. fbw.WORKDIR) + + 1 function fbw.log(text) + 12 nixio.syslog('info', '[FBW] ' .. text) + end + + 1 function fbw.lime_community_assets_name(hostname) + 3 return fbw.WORKDIR .. string.format(fbw.COMMUNITY_ASSETS_TMPL, hostname) + end + + 1 function fbw.get_lime_communty_fname(hostname, bssid) + 4 return fbw.WORKDIR .. fbw.COMMUNITY_HOST_CONFIG_PREFIX .. hostname .. "__" .. bssid + end + + -- Write lock file at begin + 1 function fbw.start_scan_file() + 4 local file = io.open("/tmp/scanning", "w") + 4 file:write("true") + 4 file:close() + end + + -- Remove old results + 1 function fbw.clean_tmp() +****0 utils.execute('rm -f ' .. fbw.WORKDIR .. '*') + end + + -- Save working copy of wireless + 1 function fbw.backup_wifi_config() +****0 utils.execute("cp /etc/config/wireless /tmp/wireless-temp") + end + + -- Get networks in 5ghz radios + 1 function fbw.get_networks() + -- Get all radios + 1 local radios = ft.map(utils.extract_prop(".name"), wireless.scandevices()) + -- Get only 5ghz radios + 1 local radios_5ghz = ft.filter(wireless.is5Ghz, radios) + -- Convert radios to phys (get a list of phys from radio devices) + 1 local phys = ft.map(utils.extract_phys_from_radios, radios_5ghz) + -- Scan networks in phys and format result + 2 local networks = ft.map( + function(phy) +****0 local nets = iwinfo.nl80211.scanlist(phy) +****0 return ft.map(utils.add_prop("phy_idx", utils.phy_to_idx(phy)), nets) + 1 end, phys) + -- Merge results + 1 networks = ft.reduce(ft.flatTable, networks, {}) + -- Filter only remote mesh and ad-hoc networks + 1 networks = ft.filter(utils.filter_mesh, networks) + -- Sort by signal + 1 networks = ft.sortBy('signal', true)(networks) + -- Remove dupicated results in multiradios devices + 1 networks = utils.only_best(networks) + 1 return networks + end + + -- Get macs from 5ghz radios + 1 function fbw.get_own_macs() +****0 local radios = ft.map(utils.extract_prop(".name"), wireless.scandevices()) +****0 local radios_5ghz = ft.filter(wireless.is5Ghz, radios) +****0 local phys = ft.map(utils.extract_phys_from_radios, radios_5ghz) +****0 return ft.map(function(phy) return table.concat(wireless.get_phy_mac(phy),":") end, phys) + end + + -- Calc link local address and download lime-community + 1 function fbw.get_config(results, mesh_network) +****0 fbw.log('Calc link local address and download lime-community - '.. json.stringify(mesh_network)) +****0 local mode = mesh_network.mode == "Mesh Point" and 'mesh' or 'adhoc' +****0 local dev_id = 'wlan'..mesh_network['phy_idx']..'-'..mode +****0 local stations = {} +****0 local linksLocalIpv6 = {} + -- Setup wireless interface +****0 fbw.setup_wireless(mesh_network) + -- Check if connected if not sleep some more until connected or ignore if 10s passed +****0 utils.is_connected(dev_id) + -- Get associated stations +****0 stations = utils.get_stations_macs(dev_id) + -- Remove own wifi networks +****0 local own_macs = fbw.get_own_macs() +****0 stations = ft.filter(utils.not_own_network(own_macs), stations) + -- Calc ipv6 +****0 local linksLocalIpv6 = ft.map(utils.eui64, stations) +****0 local hosts = ft.map(utils.append_network(dev_id), linksLocalIpv6) + -- Add aditional info +****0 local data = ft.map(function(host) +****0 return { host = host, signal = mesh_network.signal, ssid = mesh_network.ssid, bssid = mesh_network.bssid } +****0 end, hosts) +****0 data = utils.filter_alredy_scanned(data, results) + -- Try to fetch remote config file +****0 local configs = ft.map(fbw.fetch_config, data) + -- Return only valid configs +****0 for _, config in pairs(configs) do +****0 results[config.host] = config + end +****0 return results + end + + -- Setup wireless + 1 function fbw.setup_wireless(mesh_network) +****0 local phy_idx = mesh_network["phy_idx"] +****0 local mode = mesh_network.mode == "Mesh Point" and 'mesh' or 'adhoc' +****0 local device_name = "lm_wlan"..phy_idx.."_"..mode.."_radio"..phy_idx +****0 local uci_cursor = config.get_uci_cursor() + -- Get acutal settings +****0 local current_channel = uci_cursor:get("wireless", 'radio'..phy_idx, "channel") +****0 local current_mode = uci_cursor:get("wireless", device_name, "mode") + -- Avoid unnecessary configuration changes +****0 if ( tonumber(current_channel) == tonumber(mesh_network.channel) and current_mode == mode ) then +****0 return + end + -- Remove current wireless setup +****0 uci_cursor:foreach("wireless", "wifi-iface", function(entry) +****0 if entry['.name'] == device_name then +****0 uci_cursor:delete("wireless", entry['.name']) + end + end) + -- Set new wireless configuration +****0 uci_cursor:set("wireless", 'radio'..phy_idx, "channel", mesh_network.channel) +****0 uci_cursor:set("wireless", device_name, "wifi-iface") +****0 uci_cursor:set("wireless", device_name, "device", 'radio'..phy_idx) +****0 uci_cursor:set("wireless", device_name, "ifname", 'wlan'..phy_idx..'-'..mode) +****0 uci_cursor:set("wireless", device_name, "network", 'lm_net_wlan'..phy_idx..'_'..mode) +****0 uci_cursor:set("wireless", device_name, "distance", '10000') +****0 uci_cursor:set("wireless", device_name, "mode", mode) +****0 uci_cursor:set("wireless", device_name, "mesh_id", 'LiMe') +****0 uci_cursor:set("wireless", device_name, "ssid", 'LiMe') +****0 uci_cursor:set("wireless", device_name, "mesh_fwding", '0') +****0 uci_cursor:set("wireless", device_name, "bssid", 'ca:fe:00:c0:ff:ee') +****0 uci_cursor:set("wireless", device_name, "mcast_rate", '6000') +****0 uci_cursor:commit("wireless") +****0 utils.execute("wifi down radio"..phy_idx.."; wifi up radio"..phy_idx) +****0 os.execute("sleep 10s") + end + + 1 function fbw.fetch_lime_community(host, lime_community_fname) + 1 local res = lutils.http_client_get("http://[" .. host .. "]/cgi-bin/lime/lime-community", 10, lime_community_fname) + 1 if res == nil or utils.file_not_exists_or_empty(lime_community_fname) then + 1 res = lutils.http_client_get("http://[" .. host .. "]/lime-community", 10, lime_community_fname) + end + 1 return res + end + + -- Return true if download success, false otherwise + 1 function fbw.fetch_lime_community_assets(host, fname) + 3 local res = lutils.http_client_get("http://[" .. host .. "]/cgi-bin/lime/lime-community-assets", 10, lime_community_fname) + 3 return res + end + + -- Fetch remote configuration and save result + 1 function fbw.fetch_config(data) + 4 fbw.log('Fetch config from '.. json.stringify(data)) + 4 fbw.set_status_to_scanned_bbsid(data.bssid, fbw.FETCH_CONFIG_STATUS.downloading_config) + 4 local success = true + 4 local host = data.host + + 4 local hostname = utils.execute("wget --no-check-certificate http://["..data.host.."]/cgi-bin/hostname -qO - "):gsub("\n", "") + 4 fbw.log('Hostname found: '.. hostname) + 4 if (hostname == '') then hostname = host end + + 4 local lime_community_fname = fbw.get_lime_communty_fname(hostname, data.bssid) + + 4 local res = fbw.fetch_lime_community(data.host, lime_community_fname) + + -- Remove lime-community files that are not yet configured. + -- For this we asume that no ap_ssid options equals not configured. + 4 if res == true and not utils.file_not_exists_or_empty(lime_community_fname) then + 3 local f = io.open(lime_community_fname) + 3 local content = f:read("*a") + 3 f:close() + 3 if not content:match("ap_ssid") then + 1 fbw.set_status_to_scanned_bbsid(data.bssid, fbw.FETCH_CONFIG_STATUS.error_not_configured) + 1 utils.execute("rm " .. lime_community_fname) + 1 success = false + else + 2 local fname = fbw.lime_community_assets_name(hostname) + 2 success = fbw.fetch_lime_community_assets(data.host, fname) + 2 if success == nil then + -- Error downloading lime community assets + 1 success = false + 1 fbw.set_status_to_scanned_bbsid(data.bssid, fbw.FETCH_CONFIG_STATUS.error_download_lime_assets) + end + end + else + -- Error downloading lime community + 1 fbw.set_status_to_scanned_bbsid(data.bssid, fbw.FETCH_CONFIG_STATUS.error_download_lime_community) + 1 success = false + end + + 4 if success then + 1 fbw.set_status_to_scanned_bbsid(data.bssid, fbw.FETCH_CONFIG_STATUS.downloaded_config) + end + + 4 return { host = host, filename = lime_community_fname, success = success} + end + + -- Restore previus wireless configuration + 1 function fbw.restore_wifi_config() + 1 utils.execute("cp /tmp/wireless-temp /etc/config/wireless") + 1 local allRadios = wireless.scandevices() + 1 for _, radio in pairs (allRadios) do +****0 if wireless.is5Ghz(radio[".name"]) then +****0 local phyIndex = radio[".name"].sub(radio[".name"], -1) +****0 utils.execute("wifi down radio"..phyIndex.."; wifi up radio"..phyIndex) + end + end + end + + -- Apply configuration permanenty + -- TODO: check if config is valid + -- TODO: use safe-reboot + 1 function fbw.apply_file_config(file, hostname) +****0 fbw.log('apply_file_config(file=' .. file .. ', hostname=' .. hostname .. ')') +****0 local uci_cursor = config.get_uci_cursor() + --Check if lime-community exist +****0 local filePath = fbw.WORKDIR .. file +****0 utils.file_exists(filePath) + -- Format hostname +****0 hostname = hostname or config.get("system", "hostname") + -- Clean previus lime configuration and replace lime-community +****0 config.reset_node_config() +****0 utils.execute("cp " .. filePath .. " /etc/config/" .. config.UCI_COMMUNITY_NAME) + + -- Setup the shared lime-assets +****0 local remote_hostname = string.sub(file, #fbw.COMMUNITY_HOST_CONFIG_PREFIX + 1) +****0 local lime_community_assets_fname = fbw.lime_community_assets_name(remote_hostname) +****0 if utils.file_exists(lime_community_assets_fname) then +****0 utils.execute(string.format("tar xfz %s -C /etc/lime-assets/", lime_community_assets_fname)) + end + + -- Run lime-config as first boot and setup new hostname +****0 uci_cursor:set(config.UCI_NODE_NAME, "system", "hostname", hostname) +****0 fbw.end_config() + end + + -- Remove scan lock file + 1 function fbw.end_scan() + 3 local file = io.open("/tmp/scanning", "w") + 3 file:write("false") + 3 file:close() + end + + -- Read scan status + 1 function fbw.check_scan_file() + 9 local file = io.open("/tmp/scanning", "r") + 9 if(file == nil) then + 2 return nil + end + 7 return assert(file:read("*a"), nil) + end + + 1 function fbw.is_configured() + 5 return config.get_bool('system', 'firstbootwizard_configured', false) + end + + 1 function fbw.mark_as_configured() + 1 local uci_cursor = config.get_uci_cursor() + 1 uci_cursor:set(config.UCI_NODE_NAME, 'system', 'firstbootwizard_configured', 'true') + end + + 1 function fbw.is_dismissed() + 5 return config.get_bool('system', 'firstbootwizard_dismissed', false) + end + + 1 function fbw.dismiss() + 1 local uci_cursor = config.get_uci_cursor() + 1 uci_cursor:set(config.UCI_NODE_NAME, 'system', 'firstbootwizard_dismissed', 'true') + 1 uci_cursor:commit(config.UCI_NODE_NAME) + 1 config.uci_autogen() + end + + -- Get config from lime-default file + local function getConfig(path) + 3 local uci_cursor = uci.cursor(fbw.WORKDIR) + 3 local config = uci_cursor:get_all(path) + + 3 if config ~= nil then + 3 return config + end +****0 return {} + end + + -- List downloaded lime-community + 1 function fbw.read_configs() + 7 local tempFiles = fs.dir(fbw.WORKDIR) + 7 local result = {} + 13 for file in tempFiles do + 6 if (file ~= nil and file:match("^" .. lutils.literalize(fbw.COMMUNITY_HOST_CONFIG_PREFIX))) then + 3 local config = getConfig(file) + 3 local trimedConfig = {} + 3 trimedConfig.wifi = config['wifi'] + 6 table.insert(result, { + 3 config = trimedConfig, + 3 file = file + }) + end + end + + 7 return result + end + + -- Apply configuration for a new network ( used in ubus daemon) + 1 function fbw.create_network(ssid, hostname, password, country) + 1 fbw.log('apply_file_config(ssid=' .. ssid .. ', hostname=' .. hostname .. ')') + 1 local uci_cursor = config.get_uci_cursor() + + -- Save changes in lime-community + 1 if password ~= nil and password ~= '' then + 1 lutils.set_shared_root_password(password) + end + 1 if country then +****0 uci_cursor:set("lime-community", 'wifi', 'country', country) + end + 1 uci_cursor:set("lime-community", 'wifi', 'ap_ssid', ssid) + 1 uci_cursor:set("lime-community", 'wifi', 'apname_ssid', ssid..'/%H') + 1 uci_cursor:commit("lime-community") + + -- Apply new configuration and setup hostname + 1 config.reset_node_config() + 1 uci_cursor:set("lime-node", 'system', 'hostname', hostname or config.get("system", "hostname")) + 1 fbw.end_config() + end + + 1 function fbw.end_config() +****0 local uci_cursor = config.get_uci_cursor() +****0 fbw.mark_as_configured() +****0 fbw.log('commiting lime-node') +****0 uci_cursor:commit(config.UCI_NODE_NAME) + -- Apply new configuration + +****0 os.execute("/usr/bin/lime-config") + +****0 os.execute("reboot") + end + + 1 function fbw.save_scan_results(networks) + 17 return lutils.write_obj_store(fbw.WORKDIR .. fbw.SCAN_RESULTS_FILE, networks) + end + + 1 function fbw.read_scan_results( ) + 18 return lutils.read_obj_store(fbw.WORKDIR .. fbw.SCAN_RESULTS_FILE) + end + + -- Used to add "status" to an entry on the scanresults file + 1 function fbw.set_status_to_scanned_bbsid(destBssid, status) + -- Open scan_results.json + 10 local results = fbw.read_scan_results() + -- Search ssid + 12 for k, v in pairs(results) do + 12 if(v['bssid'] == destBssid) then + -- Add status message + 10 v["status"] = status + 10 break + end + end + -- Store it again + 10 fbw.save_scan_results(results) + end + + -- Apply file config for specific file, hostname and stop scanning if running + 1 function fbw.set_network(file, hostname) +****0 fbw.stop_search_networks() -- Stop firstbootwizard service if running +****0 fbw.apply_file_config(file, hostname) + end + + -- Scan for networks and fetch configurations files + 1 function fbw.get_all_networks() +****0 local networks = {} +****0 local configs = {} +****0 fbw.log("Starting search networks") + +****0 fbw.log('Add lock file') +****0 fbw.start_scan_file() +****0 fbw.log('Clear previus scans') +****0 fbw.clean_tmp() +****0 fbw.log('Set wireless backup') +****0 fbw.backup_wifi_config() +****0 fbw.log('Get mesh networks') +****0 networks = fbw.get_networks() +****0 fbw.log('Saving mesh scan results') +****0 fbw.save_scan_results(networks) +****0 fbw.log('Get configs files') +****0 configs = ft.reduce(fbw.get_config, networks, {}) +****0 fbw.log('Restore previous wireless configuration') +****0 fbw.restore_wifi_config() +****0 fbw.log('Remove lock file') +****0 fbw.end_scan() +****0 fbw.log('Return configs files names') +****0 return configs + end + + -- Run daemonized /bin/firstbootwizard execution that start get_all_networks + -- Return false if already runing + 1 function fbw.start_search_networks() + 1 local scan_file = fbw.check_scan_file() + 1 if(scan_file == nil) or (scan_file == "false") then + 1 os.execute("rm -f /tmp/scanning") + 1 lutils.execute_daemonized("/bin/firstbootwizard") + 1 return true + end +****0 return false + end + + -- Return object with status, read_configs() and read_scan_results() + 1 function fbw.status_search_networks() + 3 local scan_file = fbw.check_scan_file() + local status + 3 if (scan_file == nil) then + 1 status = 'idle' + 2 elseif(scan_file == "true") then + 2 status = 'scanning' + else +****0 status = 'scanned' + end + 3 lock = not fbw.is_configured() and not fbw.is_dismissed() + 3 return { lock = lock, status = status, networks = fbw.read_configs(), scanned = fbw.read_scan_results()} + end + + -- todo(kon): check this work properly + 1 function fbw.kill_fbw() + 1 os.execute("killall firstbootwizard") + end + + -- Function that stop get_all_networks function if running + 1 function fbw.stop_search_networks() + 1 local scan_file = fbw.check_scan_file() + 1 if (scan_file == "true") then + 1 fbw.log('Stopping firstbootwizard service') + 1 fbw.kill_fbw() + 1 fbw.log('Restore previus wireless configuration') + 1 fbw.restore_wifi_config() + 1 fbw.log('Remove lock file') + 1 fbw.end_scan() + 1 return true + else +****0 return true + end +****0 return false + end + + -- Return false if can't perform the restart + 1 function fbw.restart_search_networks() +****0 if fbw.stop_search_networks() then +****0 return fbw.start_search_networks() + end +****0 return false + end + + 1 return fbw + +============================================================================== +packages/first-boot-wizard/files/usr/lib/lua/firstbootwizard/functools.lua +============================================================================== + 1 local functools = {} + + -- Lua implementation of the curry function + -- Developed by tinylittlelife.org + -- released under the WTFPL (http://sam.zoy.org/wtfpl/) + + -- curry(func, num_args) : take a function requiring a tuple for num_args arguments + -- and turn it into a series of 1-argument functions + -- e.g.: you have a function dosomething(a, b, c) + -- curried_dosomething = curry(dosomething, 3) -- we want to curry 3 arguments + -- curried_dosomething (a1) (b1) (c1) -- returns the result of dosomething(a1, b1, c1) + -- partial_dosomething1 = curried_dosomething (a_value) -- returns a function + -- partial_dosomething2 = partial_dosomething1 (b_value) -- returns a function + -- partial_dosomething2 (c_value) -- returns the result of dosomething(a_value, b_value, c_value) + 1 function functools.curry(func, num_args) + + -- currying 2-argument functions seems to be the most popular application +****0 num_args = num_args or 2 + + -- helper + local function curry_h(argtrace, n) +****0 if 0 == n then + -- reverse argument list and call function +****0 return func(functools.reverse(argtrace())) + else + -- "push" argument (by building a wrapper function) and decrement n + return function (onearg) +****0 return curry_h(function () return onearg, argtrace() end, n - 1) + end + end + end + + -- no sense currying for 1 arg or less +****0 if num_args > 1 then +****0 return curry_h(function () return end, num_args) + else +****0 return func + end + end + + -- reverse(...) : take some tuple and return a tuple of elements in reverse order + -- + -- e.g. "reverse(1,2,3)" returns 3,2,1 + 1 function functools.reverse(...) + + --reverse args by building a function to do it, similar to the unpack() example + local function reverse_h(acc, v, ...) +****0 if 0 == select('#', ...) then +****0 return v, acc() + else +****0 return reverse_h(function () return v, acc() end, ...) + end + end + + -- initial acc is the end of the list +****0 return reverse_h(function () return end, ...) + end + + + 1 function functools.map(func, tbl) + 3 local newtbl = {} + 3 for i,v in pairs(tbl) do +****0 newtbl[i] = func(v) + end + 3 return newtbl + end + + 1 function functools.filter(func, tbl) + 2 local newtbl= {} + 2 local index=1; + 2 for i,v in pairs(tbl) do +****0 if func(v, i) then +****0 newtbl[index]=v +****0 index = index + 1 + end + end + 2 return newtbl + end + + 1 function functools.search(func, tbl) +****0 for i,v in pairs(tbl) do +****0 if func(v, i) then +****0 return i + end + end + +****0 return 0 + end + + 1 function functools.print_r ( t ) +****0 local print_r_cache={} + local function sub_print_r(t,indent) +****0 if (print_r_cache[tostring(t)]) then +****0 print(indent.."*"..tostring(t)) + else +****0 print_r_cache[tostring(t)]=true +****0 if (type(t)=="table") then +****0 for pos,val in pairs(t) do +****0 if (type(val)=="table") then +****0 print(indent.."["..pos.."] => "..tostring(t).." {") +****0 sub_print_r(val,indent..string.rep(" ",string.len(pos)+8)) +****0 print(indent..string.rep(" ",string.len(pos)+6).."}") +****0 elseif (type(val)=="string") then +****0 print(indent.."["..pos..'] => "'..val..'"') + else +****0 print(indent.."["..pos.."] => "..tostring(val)) + end + end + else +****0 print(indent..tostring(t)) + end + end + end +****0 if (type(t)=="table") then +****0 print(tostring(t).." {") +****0 sub_print_r(t," ") +****0 print("}") + else +****0 sub_print_r(t," ") + end +****0 print() + end + + 1 function functools.reduce(cb, tab, default) + 2 local result = default + 2 for k, act in pairs(tab) do +****0 result = cb(result,act) + end + 2 return result + end + + 1 function functools.splitBy(option) + return function(tab) +****0 local result = {} +****0 for k, obj in pairs(tab) do +****0 if result[obj[option]] == nil then +****0 result[obj[option]] = {} + end +****0 table.insert(result[obj.mode], obj) + end +****0 return result + end + end + + 1 function functools.sortBy(option, reverse) + return function(tab) + 2 table.sort(tab, function (left, right) +****0 order = left[option] < right[option] +****0 return (not reverse and order) or not order + end) + 1 return tab + end + end + + 1 function functools.flatTable(prev, act) +****0 for i=1,#act do +****0 prev[#prev+1] = act[i] + end +****0 return prev + end + + 1 return functools + +============================================================================== +packages/first-boot-wizard/files/usr/lib/lua/firstbootwizard/utils.lua +============================================================================== + 1 local utils = {} + + 1 local ft = require('firstbootwizard.functools') + 1 local fs = require("nixio.fs") + 1 local iwinfo = require("iwinfo") + 1 local json = require("luci.jsonc") + 1 local limeutils = require("lime.utils") + + 1 function execute(cmd) + 45 local f = assert(io.popen(cmd, 'r')) + 45 local s = assert(f:read('*a')) + 45 f:close() + 45 return s + end + + 1 function utils.execute (cmd) + 45 return execute(cmd) + end + + 1 function utils.eui64(mac) + local cmd = [[ + function eui64 { + mac="$(echo "$1" | tr -d : | tr A-Z a-z)" + mac="$(echo "$mac" | head -c 6)fffe$(echo "$mac" | tail -c +7)" + let "b = 0x$(echo "$mac" | head -c 2)" + let "b ^= 2" + printf "%02x" "$b" + echo "$mac" | tail -c +3 | head -c 2 + echo -n : + echo "$mac" | tail -c +5 | head -c 4 + echo -n : + echo "$mac" | tail -c +9 | head -c 4 + echo -n : + echo "$mac" | tail -c +13 + } +****0 echo -n `eui64 ]]..mac..'`' +****0 return 'fe80::'..execute(cmd) + end + + 1 function utils.file_exists(filename) +****0 return fs.stat(filename, "type") == "reg" + end + + 1 function utils.file_not_exists_or_empty(filename) + 3 local f=io.open(filename,"r") + 3 if not f then return true end + 3 local size = f:seek("end") + 3 if f~=nil then io.close(f) end + 3 if size == 0 then return true else return false end + end + + 1 function split(str, sep) +****0 local sep, fields = sep or ":", {} +****0 local pattern = string.format("([^%s]+)", sep) +****0 str:gsub(pattern, function(c) fields[#fields+1] = c end) +****0 return fields + end + + 1 function utils.split(str, sep) +****0 return split(str, sep) + end + + -- splits a multiline string in a list of strings, one per line + 1 function lsplit(mlstring) +****0 return split(mlstring, "\n") + end + + 1 function utils.lsplit(mlstring) +****0 return lsplit(mlstring) + end + + 1 function utils.phy_to_idx(phy) +****0 local substr = string.gsub(phy, "phy", "") +****0 return tonumber(substr) + end + + 1 function utils.extract_phys_from_radios(radio) +****0 return "phy"..radio.sub(radio, -1) + end + + 1 function utils.not_own_network(own_macs) + return function(remote_mac) +****0 return limeutils.has_value(own_macs, remote_mac) ~= true + end + end + + 1 function utils.add_prop(option, value) + return function(tab) +****0 tab[option] = value +****0 return tab + end + end + + 1 function utils.extract_prop(prop) + return function(tab) +****0 return tab[prop] + end + end + + 1 function utils.read_file(file) +****0 local lines = utils.lines_from("/tmp/"..file) +****0 return lines + end + + + 1 function tableEmpty(self) +****0 for _, _ in pairs(self) do +****0 return false + end +****0 return true + end + + 1 function utils.tableEmpty(self) +****0 return tableEmpty(self) + end + + 1 function utils.hash_file(file) +****0 return execute("md5sum "..file.." | awk '{print $1}'") + end + + 1 function utils.are_files_different(file1, file2) +****0 return hash_file(file1) ~= hash_file(file2) + end + + 1 function utils.unpack_table(t) +****0 local unpacked = {} +****0 for k,v in ipairs(t) do +****0 for sk, sv in ipairs(v) do +****0 unpacked[#unpacked+1] = sv + end + end +****0 return unpacked + end + + 1 function utils.filter_mesh(n) +****0 return n.mode == "Ad-Hoc" or n.mode == "Mesh Point" + end + + local same_network = function(net_a, net_b) +****0 return net_a.channel == net_b.channel and net_a.ssid == net_b.ssid + end + + local find_network = function(network, data) +****0 return ft.filter(function(net) return same_network(net,network) end, data) + end + + 1 function utils.only_best(networks) + 2 return ft.reduce(function(acc, network) +****0 local existent = find_network(network, acc) +****0 if #existent > 0 then +****0 acc = ft.map(function(net) +****0 if same_network(net, network) and net.signal < network.signal then +****0 return network + end +****0 return net +****0 end,acc) +****0 return acc + end +****0 table.insert(acc, network) +****0 return acc + 1 end, networks, {}) + end + + 1 function utils.is_connected(dev_id) +****0 local isAssociated = {} +****0 local i = 0 +****0 while (tableEmpty(isAssociated)) and i < 5 do +****0 isAssociated = iwinfo.nl80211.assoclist(dev_id) +****0 if tableEmpty(isAssociated) == false then break end +****0 i = i + 1 +****0 os.execute("sleep 2s") + end + end + + 1 function utils.get_stations_macs(network) +****0 return lsplit(execute('iw dev '..network..' station dump | grep ^Station | cut -d\\ -f 2')) + end + + 1 function utils.append_network(dev) + return function (ipv6) +****0 return ipv6..'%'..dev + end + end + + 1 function utils.filter_alredy_scanned(hosts, results) +****0 local fe80scanned = ft.reduce(function(scanned, host) +****0 local mac = split(host.host, "%%")[1] +****0 scanned[mac] = mac +****0 return scanned +****0 end, results, {}) + +****0 return ft.filter(function(host) +****0 local mac = split(host.host, "%%")[1] +****0 return fe80scanned[mac] == nil +****0 end, hosts) + end + + 1 return utils + +============================================================================== +packages/lime-eth-config/files/usr/lib/lua/lime-eth-config.lua +============================================================================== + -- ! LibreMesh + -- ! Generic hook to be called as a symbolic link for each ref type + -- ! Copyright (C) 2025 Javier Jorge + -- ! Copyright (C) 2025 Instituto Nacional de Tecnología Industrial (INTI) + -- ! Copyright (C) 2025 Asociación Civil Altermundi + -- ! SPDX-License-Identifier: AGPL-3.0- + 1 local JSON = require("luci.jsonc") + 1 local utils = require("lime.utils") + 1 local config = require("lime.config") + 1 local libuci = require("uci") + 1 local node_status = require 'lime.node_status' + + 1 local luci_config = libuci:cursor() + 1 local eht_config = {} + + 1 function eht_config.get_eth_config() + 3 limenode_interfaces = {} + --get configurations from lime-node + 3 local uci = config.get_uci_cursor() + 6 uci:foreach("lime-autogen", "net", function(entry) +****0 if entry.eth_role ~= nil then +****0 local interface = {} +****0 interface.name = entry.linux_name +****0 interface.eth_role = entry.eth_role +****0 table.insert(limenode_interfaces, interface) + end + end) + --get default settings + 3 local switch_status = node_status.switch_status() + 3 local interfaces = {} + 3 if switch_status ~= nil then + 15 for _, status in ipairs(switch_status) do + 12 local interface = { + 12 device = status.device, + 12 num = status.num, + 12 role = status.role, + 12 link = status.link, + 12 eth_role = "default" -- default value + } + 12 for _, limenode_interface in ipairs(limenode_interfaces) do +****0 if limenode_interface.name == status.device then +****0 interface.eth_role = limenode_interface.eth_role + break + end + end + 12 table.insert(interfaces, interface) + end + end + 3 return interfaces + end + + 1 function eht_config.delete_eth_config(tag_value) + 1 local uci = config.get_uci_cursor() + 1 config.uci:delete("lime-node", tag_value) + 1 config.uci:save("lime-node") + 1 uci:commit("lime-node") + end + + 1 function eht_config.set_eth_config(device, role) + 3 local tag_value = "lime_app_eth_cfg_" .. device:gsub("%.", "_") + 3 local uci = config.get_uci_cursor() + 3 local eth_role = uci:get("lime-node", tag_value, "eth_role") + -- verify previous config + 3 if eth_role ~= nil then +****0 if eth_role == role then + -- No changes needed, the role is already set +****0 return true + else +****0 eht_config.delete_eth_config(tag_value) + end + end + + -- handle lm_hwd_openwrt_wan + 3 if role ~= "wan" then + -- if the port was automatically configured as a wan port, we need to remove the + -- wan protocol. If we returtn to default we need to enable auto detection. + 2 local switch_status = node_status.switch_status() + 2 if switch_status ~= nil then + 10 for _, status in ipairs(switch_status) do + 8 if status.device == device and status.role == "wan" then +****0 if role == "lan" or role == "mesh" then +****0 uci:set("lime-node", "lm_hwd_openwrt_wan", "net") +****0 uci:set("lime-node", "lm_hwd_openwrt_wan", "autogenerated", "false") + +****0 elseif role == "default" then +****0 uci:delete("lime-node", "lm_hwd_openwrt_wan") + end + break + end + end + end + end + + 3 if role == "default" then + 1 eht_config.delete_eth_config(tag_value) + else + 2 uci:set("lime-node", tag_value, "net") + 2 uci:set("lime-node", tag_value, "eth_role", role) + 2 uci:set("lime-node", tag_value, "linux_name", device) + + 2 if role == "wan" then + 1 uci:set("lime-node", tag_value, "protocols", {"wan", "dynamic"}) + 1 elseif role == "lan" then +****0 uci:set("lime-node", tag_value, "protocols", {"lan"}) + 1 elseif role == "mesh" then + 1 uci:set("lime-node", tag_value, "protocols", {"batadv:%N1", "babeld:17"}) + else +****0 return false + end + end + 3 uci:commit("lime-node") + 3 config.uci:save("lime-node") + 3 os.execute("lime-config && /etc/init.d/network restart") + 3 return true + end + + 1 return eht_config + +============================================================================== +packages/lime-eth-config/files/usr/libexec/rpcd/lime-eth-config +============================================================================== + #!/usr/bin/env lua + + --! LibreMesh + --! Generic hook to be called as a symbolic link for each ref type + --! Copyright (C) 2025 Javier Jorge + --! Copyright (C) 2025 Instituto Nacional de Tecnología Industrial (INTI) + --! Copyright (C) 2025 Asociación Civil Altermundi + --! SPDX-License-Identifier: AGPL-3.0- + + + 6 local json = require 'luci.jsonc' + 6 local utils = require 'lime.utils' + 6 local eth_config = require("lime-eth-config") + + + local function get_eth_config(msg) + + 3 interfaces = eth_config.get_eth_config() + + 3 result = {} + 3 if interfaces and #interfaces > 0 then + 3 result.status = "ok" + 3 result.interfaces = interfaces + else +****0 result.status = "error" +****0 result.interfaces = {} +****0 result.error = "No interface configuration found" + end + 3 utils.printJson(result) + end + + 6 function set_eth_config(msg) + 3 local device, role = msg.device, msg.role + + 3 if not device or not role then +****0 return utils.printJson({ status = "error", error = "Missing parameters" }) + end + + 3 local success = eth_config.set_eth_config(device, role) + 3 if success then + 3 utils.printJson({ status = "ok" }) + else +****0 utils.printJson({ status = "error", error = "Failed to set eth ".. msg.device .. " as " .. msg.role }) + end + + end + + 6 local methods = { + 6 get_eth_config = { no_params = 0 }, + 6 set_eth_config = { device = 'value', role = 'value'} + } + + + 6 if arg[1] == 'list' then +****0 utils.printJson(methods) + end + + 6 if arg[1] == 'call' then + 6 local msg = utils.rpcd_readline() or '{}' + 6 msg = json.parse(msg) + 6 if arg[2] == 'get_eth_config' then get_eth_config(msg) + 3 elseif arg[2] == 'set_eth_config' then set_eth_config(msg) +****0 else utils.printJson({ error = "Method not found" }) + end + end + +============================================================================== +packages/lime-hwd-ground-routing/files/usr/lib/lua/lime/hwd/ground_routing.lua +============================================================================== + #!/usr/bin/lua + + 1 local hardware_detection = require("lime.hardware_detection") + 1 local config = require("lime.config") + 1 local utils = require("lime.utils") + + 1 local ground_routing = {} + + 1 ground_routing.sectionNamePrefix = hardware_detection.sectionNamePrefix.."ground_routing_" + + 1 function ground_routing.delete_all_switch_vlan_sections() +****0 local uci = config.get_uci_cursor() +****0 uci:foreach("network", "switch_vlan", function(s) uci:delete("network", s[".name"]) end ) +****0 uci:save("network") + end + + 1 function ground_routing.clean() + 1 local uci = config.get_uci_cursor() + + 1 function cleanGrSection(section) + 4 if utils.stringStarts(section[".name"], ground_routing.sectionNamePrefix) then +****0 uci:delete("network", section[".name"]) + end + end + + 1 uci:foreach("network", "switch_vlan", cleanGrSection) + 1 uci:foreach("network", "interface", cleanGrSection) + 1 uci:save("network") + end + + 1 function ground_routing.detect_hardware() + 1 function parse_gr(section) +****0 local link_name = section[".name"] +****0 local net_dev = section["net_dev"] +****0 local vlan = section["vlan"] +****0 local uci = config.get_uci_cursor() + +****0 function create_8021q_dev(vlan_p) +****0 local dev_secname = ground_routing.sectionNamePrefix..link_name.."_"..net_dev.."_"..vlan_p +****0 uci:set("network", dev_secname, "device") +****0 uci:set("network", dev_secname, "name", net_dev.."."..vlan_p) +****0 uci:set("network", dev_secname, "ifname", net_dev) +****0 uci:set("network", dev_secname, "type", "8021q") +****0 uci:set("network", dev_secname, "vid", vlan_p) + end + +****0 local switch_dev = section["switch_dev"] +****0 if switch_dev then +****0 local switch_cpu_port = section["switch_cpu_port"] +****0 function tag_cpu_port(section) +****0 if (section["device"] ~= switch_dev) then return end + +****0 local patterns = { "^"..switch_cpu_port.." ", " "..switch_cpu_port.."$", " "..switch_cpu_port.." " } +****0 local substits = { switch_cpu_port.."t ", " "..switch_cpu_port.."t", " "..switch_cpu_port.."t " } +****0 local matchCount = 0 +****0 local m = 0 +****0 for i,p in pairs(patterns) do +****0 section["ports"], m = section["ports"]:gsub(p, substits[i]) +****0 matchCount = matchCount + m + end + +****0 if (matchCount > 0) then +****0 create_8021q_dev(section["vlan"]) +****0 uci:set("network", section[".name"], "ports", section["ports"]) + end + end + +****0 uci:foreach("network", "switch_vlan", tag_cpu_port) + + +****0 local sw_secname = ground_routing.sectionNamePrefix..link_name.."_sw_"..switch_dev.."_"..vlan +****0 local ports = switch_cpu_port.."t" +****0 for _,p in pairs(section["switch_ports"]) do +****0 ports = ports.." "..p + end + +****0 uci:set("network", sw_secname, "switch_vlan") +****0 uci:set("network", sw_secname, "device", switch_dev) +****0 uci:set("network", sw_secname, "vlan", vlan) +****0 uci:set("network", sw_secname, "ports", ports) + end + +****0 create_8021q_dev(vlan) + +****0 uci:save("network") + end + + local clean_needed -- if there are no hwd_gr sections defined, don't clean all switch_vlan sections + 1 config.foreach("hwd_gr", function(s) clean_needed = true end) + 1 if clean_needed then ground_routing.delete_all_switch_vlan_sections() end + + 1 config.foreach("hwd_gr", parse_gr) + end + + 1 return ground_routing + +============================================================================== +packages/lime-hwd-openwrt-wan/files/usr/lib/lua/lime/hwd/openwrt_wan.lua +============================================================================== + #!/usr/bin/lua + + --! LibreMesh community mesh networks meta-firmware + --! + --! Copyright (C) 2014-2023 Gioacchino Mazzurco + --! Copyright (C) 2023 Asociación Civil Altermundi + --! + --! SPDX-License-Identifier: AGPL-3.0-only + + + 2 local hardware_detection = require("lime.hardware_detection") + 2 local config = require("lime.config") + 2 local utils = require("lime.utils") + + 2 local openwrt_wan = {} + + 2 openwrt_wan.sectionName = hardware_detection.sectionNamePrefix.."openwrt_wan" + + 2 function openwrt_wan.clean() + 1 if config.autogenerable(openwrt_wan.sectionName) then + 1 config.delete(openwrt_wan.sectionName) + end + end + + 2 function openwrt_wan.detect_hardware() + 4 if config.autogenerable(openwrt_wan.sectionName) then + local ifname + 4 local board = utils.getBoardAsTable() + 4 local networkTable = board['network'] + 4 if networkTable then + 3 local wanTable = networkTable['wan'] + 3 if wanTable then + 2 ifname = wanTable['device'] + end + end + 4 if ifname and ifname ~= "" then + 2 local protos = {} + 2 local net = require("lime.network") + 2 local utils = require("lime.utils") + 13 for _, pArgs in pairs(config.get("network", "protocols")) do + 11 local pArr = utils.split(pArgs, net.protoParamsSeparator) + 11 if ( pArr[1] == "bmx6" or pArr[1] == "bmx7") then + 2 pArr[2] = 0 + 2 pArgs = table.concat(pArr, net.protoParamsSeparator) + 2 table.insert(protos, pArgs) + 9 elseif ( pArr[1]~="lan" and pArr[1]~="wan" ) then + 7 table.insert(protos, pArgs) + end + end + 2 table.insert(protos, "wan") + + 2 config.init_batch() + 2 config.set(openwrt_wan.sectionName, "net") + 2 config.set(openwrt_wan.sectionName, "autogenerated", "true") + 2 config.set(openwrt_wan.sectionName, "protocols", protos) + 2 config.set(openwrt_wan.sectionName, "linux_name", ifname) + 2 config.end_batch() + + 2 utils.dbg("WAN interface:", ifname) + else + 2 utils.dbg("WAN interface not found") + end + end + end + + 2 return openwrt_wan + +============================================================================== +packages/lime-hwd-usbradio/files/usr/lib/lua/lime/hwd/usbradio.lua +============================================================================== + #!/usr/bin/lua + + 1 local utils = require("lime.utils") + 1 local config = require("lime.config") + 1 local hardware_detection = require("lime.hardware_detection") + + 1 local libuci = require("uci") + 1 local fs = require("nixio.fs") + + 1 usbradio = {} + + 1 usbradio.sectionNamePrefix = hardware_detection.sectionNamePrefix.."usbradio_" + + --! Remove configuration about no more plugged usb radio + 2 function usbradio.clean() + 1 local uci = libuci:cursor() + + --! This function control if a usb radio configuration section is valid otherwise delete it + local function test_and_clean_device(s) + --! Check if passed section is an usb radio +****0 local sectionName = s[".name"] +****0 if utils.stringStarts(sectionName, usbradio.sectionNamePrefix) then + --! Check if the section is autogenerated otherwise do not touch it + --! We check it to avoid delete usb radio sections manually configured +****0 if config.get_bool(sectionName, "autogenerated") then +****0 local phyIndex = sectionName:match("%d+$") + --! Check if the usb radio exist + --! If the usb radio does not exist anymore (numberOfMatches < 1) delete it +****0 local _, numberOfMatches = fs.glob("/sys/devices/"..s["path"].."/ieee80211/phy"..phyIndex) +****0 if numberOfMatches < 1 then +****0 local radioName = s["radio_name"] +****0 uci:delete("wireless", radioName) +****0 config.delete(sectionName) + end + end + end + end + + --! For each wifi-device section call test_and_clean_device function + 1 uci:foreach("wireless", "wifi-device", test_and_clean_device) + 1 uci:save("wireless") + end + + + --! Detect the usb radio and configurate it + 2 function usbradio.detect_hardware() + 1 local stdOutput = io.popen("find /sys/devices | grep usb | grep ieee80211 | grep 'phy[0-9]*$'") + --! Repeat for each usb radio found + 1 for path in stdOutput:lines() do + --! Define useful variables +****0 local endBasePath, phyEnd = string.find(path, "/ieee80211/phy") +****0 local phyPath = string.sub(path, 14, endBasePath-1) +****0 local phyIndex = string.sub(path, phyEnd+1) +****0 local radioName = "radio"..phyIndex +****0 local sectionName = usbradio.sectionNamePrefix..radioName + + --! If sectionName exist and it is autogenerated configure it + --! If sectionName does not exist it is created and configured + --! Check if a sectionName is autogenerated avoid delete usb radio introduced by the user +****0 if config.autogenerable(sectionName) then +****0 local uci = libuci:cursor() + + --! Delete the usb radio +****0 uci:delete("wireless", radioName) + --! Create and configure the usb radio directly in the OpenWRT system (wireless) +****0 uci:set("wireless", radioName, "wifi-device") +****0 uci:set("wireless", radioName, "type", "mac80211") +****0 uci:set("wireless", radioName, "channel", "11") --TODO: working on all 802.11bgn devices; find a general way for working in different devices +****0 uci:set("wireless", radioName, "band", "2g") --TODO: working on all 802.11gn devices; find a general way for working in different devices +****0 uci:set("wireless", radioName, "path", phyPath) +****0 uci:set("wireless", radioName, "htmode", "HT20") +****0 uci:set("wireless", radioName, "disabled", "0") + +****0 uci:save("wireless") + + --! Write just once on the disk all the config.set +****0 config.init_batch() +****0 config.set(sectionName, "wifi") +****0 config.set(sectionName, "autogenerated", "true") +****0 config.set(sectionName, "radio_name", radioName) + + --! Configuration of an usb radio using general option in LiMe +****0 for option_name, value in pairs(config.get_all("wifi")) do + --! Options that start with point are hidden option, we exclude them as they can cause problems +****0 if (option_name:sub(1,1) ~= ".") then + --! Needed a table or a string for config.set +****0 if ( type(value) ~= "table" ) then value = tostring(value) end +****0 config.set(sectionName, option_name, value) + end + end + +****0 local modes = {} +****0 for _, mode in pairs(config.get("wifi", "modes")) do +****0 if mode ~= "adhoc" then table.insert(modes, mode) end + end +****0 config.set(sectionName, "modes", modes) + +****0 config.end_batch() + end + end + end + + + 1 return usbradio + +============================================================================== +packages/lime-hwd-watchcat/files/usr/lib/lua/lime/hwd/watchcat.lua +============================================================================== + #!/usr/bin/lua + + 1 local hardware_detection = require("lime.hardware_detection") + 1 local config = require("lime.config") + 1 local utils = require("lime.utils") + + 1 local watchcat = {} + + 1 watchcat.sectionNamePrefix = hardware_detection.sectionNamePrefix.."watchcat_" + + local function reload_watchcat() +****0 os.execute("/etc/init.d/watchcat reload") + end + + 1 function watchcat.clean() + 1 local uci = config.get_uci_cursor() + 1 local modified = false + + local function clear_watchcat_section(section) +****0 local is_ours = utils.stringStarts(section[".name"], watchcat.sectionNamePrefix) + +****0 local is_anon = section[".anonymous"] + +****0 if is_ours or is_anon then +****0 uci:delete("watchcat", section[".name"]) +****0 modified = true + end + end + + 1 uci:foreach("watchcat", "watchcat", clear_watchcat_section) + 1 if modified then +****0 uci:save("watchcat") +****0 reload_watchcat() + end + end + + 1 function watchcat.detect_hardware() + 1 local uci = config.get_uci_cursor() + 1 local user_defined = false + + 2 config.foreach("hwd_watchcat", function(user_section) +****0 user_defined = true +****0 local identifier = user_section.id or "default" +****0 local section_name = watchcat.sectionNamePrefix .. identifier + +****0 uci:set("watchcat", section_name, "watchcat") + +****0 for option_key, option_value in pairs(user_section) do + -- discards .name, .type keys and id name sections +****0 if option_key:sub(1,1) ~= "." and option_key ~= "id" then +****0 uci:set("watchcat", section_name, option_key, option_value) + end + end + end) + -- only saved if we actually aplied any user section + 1 if user_defined then +****0 uci:save("watchcat") +****0 reload_watchcat() + end + end + + 1 return watchcat + +============================================================================== +packages/lime-proto-babeld/files/usr/lib/lua/lime/proto/babeld.lua +============================================================================== + #!/usr/bin/lua + + --! LiMe Proto Babeld + --! Copyright (C) 2018 Gioacchino Mazzurco + --! + --! This program is free software: you can redistribute it and/or modify + --! it under the terms of the GNU Affero General Public License as + --! published by the Free Software Foundation, either version 3 of the + --! License, or (at your option) any later version. + --! + --! This program is distributed in the hope that it will be useful, + --! but WITHOUT ANY WARRANTY; without even the implied warranty of + --! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + --! GNU Affero General Public License for more details. + --! + --! You should have received a copy of the GNU Affero General Public License + --! along with this program. If not, see . + + 1 local network = require("lime.network") + 1 local config = require("lime.config") + 1 local fs = require("nixio.fs") + + 1 babeld = {} + + 1 babeld.configured = false + + 2 function babeld.configure(args) + 12 if babeld.configured then return end + 1 babeld.configured = true + + 1 utils.log("lime.proto.babeld.configure(...)") + + 1 fs.writefile("/etc/config/babeld", "") + + 1 local uci = config.get_uci_cursor() + + 1 if config.get("network", "babeld_over_librenet6", false) then +****0 uci:set("babeld", "librenet6", "interface") +****0 uci:set("babeld", "librenet6", "ifname", "librenet6") +****0 uci:set("babeld", "librenet6", "type", "tunnel") + end + + 1 uci:set("babeld", "general", "general") + 1 uci:set("babeld", "general", "local_port", "30003") + 1 uci:set("babeld", "general", "ubus_bindings", "true") + + 1 uci:set("babeld", "ula6", "filter") + 1 uci:set("babeld", "ula6", "type", "redistribute") + 1 uci:set("babeld", "ula6", "ip", "fc00::/7") + 1 uci:set("babeld", "ula6", "action", "allow") + + 1 uci:set("babeld", "public6", "filter") + 1 uci:set("babeld", "public6", "type", "redistribute") + 1 uci:set("babeld", "public6", "ip", "2000::0/3") + 1 uci:set("babeld", "public6", "action", "allow") + + 1 uci:set("babeld", "default6", "filter") + 1 uci:set("babeld", "default6", "type", "redistribute") + 1 uci:set("babeld", "default6", "ip", "0::0/0") + 1 uci:set("babeld", "default6", "le", "0") + 1 uci:set("babeld", "default6", "action", "allow") + + 1 uci:set("babeld", "mesh4", "filter") + 1 uci:set("babeld", "mesh4", "type", "redistribute") + 1 uci:set("babeld", "mesh4", "ip", "10.0.0.0/8") + 1 uci:set("babeld", "mesh4", "action", "allow") + + 1 uci:set("babeld", "mptp4", "filter") + 1 uci:set("babeld", "mptp4", "type", "redistribute") + 1 uci:set("babeld", "mptp4", "ip", "172.16.0.0/12") + 1 uci:set("babeld", "mptp4", "action", "allow") + + 1 uci:set("babeld", "default4", "filter") + 1 uci:set("babeld", "default4", "type", "redistribute") + 1 uci:set("babeld", "default4", "ip", "0.0.0.0/0") + 1 uci:set("babeld", "default4", "le", "0") + 1 uci:set("babeld", "default4", "action", "allow") + + -- Avoid redistributing extra local addesses + 1 uci:set("babeld", "localdeny", "filter") + 1 uci:set("babeld", "localdeny", "type", "redistribute") + 1 uci:set("babeld", "localdeny", "local", "true") + 1 uci:set("babeld", "localdeny", "action", "deny") + + -- Avoid redistributing enything else + 1 uci:set("babeld", "denyany", "filter") + 1 uci:set("babeld", "denyany", "type", "redistribute") + 1 uci:set("babeld", "denyany", "action", "deny") + + 1 uci:save("babeld") + + end + + 2 function babeld.setup_interface(ifname, args) + 11 if not args["specific"] and ifname:match("^wlan%d+.ap") then + 6 utils.log("lime.proto.babeld.setup_interface(%s, ...) ignored", ifname) + 6 return + end + + 5 utils.log("lime.proto.babeld.setup_interface(%s, ...)", ifname) + + 5 local vlanId = args[2] or 17 + 5 local vlanProto = args[3] or "8021ad" + 5 local nameSuffix = args[4] or "_babeld" + + local owrtInterfaceName, linuxVlanIfName, owrtDeviceName = + 5 network.createVlanIface(ifname, vlanId, nameSuffix, vlanProto) + + 5 local ipv4, _ = network.primary_address() + + 5 local uci = config.get_uci_cursor() + + 5 if(vlanId ~= 0 and (ifname:match("^eth") or ifname:match("^lan"))) then + 1 uci:set("network", owrtDeviceName, "mtu", tostring(network.MTU_ETH_WITH_VLAN)) + end + + 5 uci:set("network", owrtInterfaceName, "proto", "static") + 5 uci:set("network", owrtInterfaceName, "ipaddr", ipv4:host():string()) + 5 uci:set("network", owrtInterfaceName, "netmask", "255.255.255.255") + 5 uci:save("network") + + 5 uci:set("babeld", owrtInterfaceName, "interface") + 5 uci:set("babeld", owrtInterfaceName, "ifname", linuxVlanIfName) + --! It is quite common to have dummy radio device attached via ethernet so + --! disable wired optimization always as it would consider the link down at + --! first packet lost + 5 uci:set("babeld", owrtInterfaceName, "type", "wireless") + + 5 uci:save("babeld") + end + + 2 function babeld.runOnDevice(linuxDev, args) +****0 utils.log("lime.proto.babeld.runOnDevice(%s, ...)", linuxDev) + +****0 local vlanId = args[2] or 17 +****0 local vlanProto = args[3] or "8021ad" + +****0 local vlanDev = network.createVlan(linuxDev, vlanId, vlanProto) +****0 network.createStatic(vlanDev) + +****0 local libubus = require("ubus") +****0 local ubus = libubus.connect() +****0 ubus:call('babeld', 'add_interface', { ifname = vlanDev }) + end + + 1 return babeld + +============================================================================== +packages/lime-proto-batadv/files/usr/lib/lua/lime/proto/batadv.lua +============================================================================== + #!/usr/bin/lua + + 2 local fs = require("nixio.fs") + 2 local lan = require("lime.proto.lan") + 2 local utils = require("lime.utils") + 2 local network = require("lime.network") + 2 local config = require("lime.config") + + 2 batadv = {} + + 2 batadv.configured = false + + 4 function batadv.configure(args) + 14 if batadv.configured then return end + 3 batadv.configured = true + + 3 local uci = config.get_uci_cursor() + + 3 uci:set("network", "bat0", "interface") + 3 uci:set("network", "bat0", "proto", "batadv") + -- BATMAN_V uses throughput rather than packet loss (as in BATMAN_IV) for evaluating + -- the quality of a link. Still, by default we continue selecting BATMAN_IV + 3 local routing_algo = config.get("network", "batadv_routing_algo", "BATMAN_IV") + 3 uci:set("network", "bat0", "routing_algo", routing_algo) + 3 uci:set("network", "bat0", "bridge_loop_avoidance", "1") + 3 uci:set("network", "bat0", "multicast_mode", "0") + -- by default, BATMAN-adv sends out one Originator Message (OGM) every second (orig_interval=1000) + -- in a network with static nodes, a larger interval between OGM packets can be used (e.g. 2000) + -- see https://github.com/libremesh/lime-packages/issues/1010 + 3 local orig_interval = config.get("network", "batadv_orig_interval", "2000") + 3 uci:set("network", "bat0", "orig_interval", orig_interval) + + -- if anygw enabled disable DAT that doesn't play well with it + -- and set gw_mode=client everywhere. Since there's no gw_mode=server, this makes bat0 never forward requests + -- so a rogue DHCP server doesn't affect whole network (DHCP requests are always answered locally) + 16 for _,proto in pairs(config.get("network", "protocols")) do + 13 if proto == "anygw" then + 2 uci:set("network", "bat0", "distributed_arp_table", "0") + 2 uci:set("network", "bat0", "gw_mode", "client") + end + end + 3 uci:save("network") + 3 lan.setup_interface("bat0", nil) + + -- enable alfred on bat0 if installed + 3 if utils.is_installed("alfred") then +****0 uci:set("alfred", "alfred", "batmanif", "bat0") +****0 uci:save("alfred") + end + end + + 4 function batadv.setup_interface(ifname, args) + 11 if not args["specific"] then + 10 if ifname:match("^wlan%d+.ap") then + 12 utils.log( "lime.proto.batadv.setup_interface(%s, ...) ignored", + 6 ifname ) + 6 return + end + end + + 5 utils.log("lime.proto.batadv.setup_interface(%s, ...)", ifname) + + 5 local vlanId = args[2] or "%N1" + 5 local vlanProto = args[3] or "8021ad" + 5 local nameSuffix = args[4] or "_batadv" + 5 local mtu = 1532 + + --! Unless a specific integer is passed, parse network_id (%N1) template + --! and use that number to get a vlanId between 29 and 284 for batadv + --! (to avoid overlapping with other protocols, + --! complex definition is for keeping retrocompatibility) + 5 if not tonumber(vlanId) then vlanId = 29 + (utils.applyNetTemplate10(vlanId) - 13) % 256 end + + 5 local owrtInterfaceName, _, owrtDeviceName = network.createVlanIface(ifname, vlanId, nameSuffix, vlanProto) + + 5 local uci = config.get_uci_cursor() + 5 uci:set("network", owrtInterfaceName, "proto", "batadv_hardif") + 5 uci:set("network", owrtInterfaceName, "master", "bat0") + + 5 if ifname:match("^eth") then + --! TODO: Use DSA to check if ethernet device is capable of bigger MTU + --! reducing it +****0 mtu = network.MTU_ETH_WITH_VLAN + end + + --! Avoid dmesg flooding caused by BLA with messages like "br-lan: + --! received packet on bat0 with own address as source address". + --! Tweak MAC address for each of the interfaces used by Batman-adv + --! 00 + Locally administered unicast .. 2 bytes from interface name + --! .. 3 bytes from main interface + 5 local id = utils.get_id(ifname) + 5 local vMacaddr = network.primary_mac(); + 5 vMacaddr[1] = "02" + 5 vMacaddr[2] = id[2] + 5 vMacaddr[3] = id[3] + 5 uci:set("network", owrtDeviceName, "macaddr", table.concat(vMacaddr, ":")) + + 5 uci:set("network", owrtDeviceName, "mtu", mtu) + 5 uci:save("network") + end + + 4 function batadv.runOnDevice(linuxDev, args) +****0 args = args or {} +****0 local vlanId = args[2] or "%N1" +****0 local vlanProto = args[3] or "8021ad" + +****0 utils.log("lime.proto.batadv.runOnDevice(%s, ...)", linuxDev) + + +****0 local mtu = 1532 + +****0 if not tonumber(vlanId) then +****0 vlanId = 29 + (utils.applyNetTemplate10(vlanId) - 13) % 256 + end + +****0 local devName = network.createVlan(linuxDev, vlanId, vlanProto) +****0 local ifName = network.limeIfNamePrefix..linuxDev .. "_batadv" + +****0 local ifaceConf = { + name = ifName, + proto = "batadv_hardif", + auto = "1", + device = devName, +****0 master = "bat0" + } + +****0 local libubus = require("ubus"); +****0 local ubus = libubus.connect() +****0 ubus:call('network', 'add_dynamic', ifaceConf) +****0 ubus:call('network.interface.'..ifName, 'up', {}) + + + --! TODO: as of today ubus silently fails to properly setting up a linux network + --! device for batman ADV usage dinamycally work around it by using + --! shell commands instead +****0 network.createStatic(devName) +****0 utils.unsafe_shell("batctl if add "..devName) + end + + 2 return batadv + +============================================================================== +packages/lime-proto-bmx7/files/usr/lib/lua/lime/proto/bmx7.lua +============================================================================== + #!/usr/bin/lua + + 1 local network = require("lime.network") + 1 local config = require("lime.config") + 1 local fs = require("nixio.fs") + 1 local libuci = require("uci") + 1 local wireless = require("lime.wireless") + 1 local utils = require("lime.utils") + + 1 bmx7 = {} + + 1 bmx7.configured = false + 1 bmx7.f = "bmx7" + + 2 function bmx7.configure(args) + 12 if bmx7.configured then return end + 1 bmx7.configured = true + + 1 local uci = libuci:cursor() + 1 local ipv4, ipv6 = network.primary_address() + + 1 fs.writefile("/etc/config/"..bmx7.f, "") + + 1 uci:set(bmx7.f, "general", "bmx7") + 1 uci:set(bmx7.f, "general", "dbgMuteTimeout", "1000000") + 1 uci:set(bmx7.f, "general", "tunOutTimeout", "100000") + 1 uci:set(bmx7.f, "general", "configSync", "0") + 1 uci:set(bmx7.f, "general", "syslog", "0") + + 1 uci:set(bmx7.f, "main", "tunDev") + 1 uci:set(bmx7.f, "main", "tunDev", "main") + 1 uci:set(bmx7.f, "main", "tun4Address", ipv4:string()) + 1 uci:set(bmx7.f, "main", "tun6Address", ipv6:string()) + + -- If publish own IP enabled, configure tunIn + 1 local pub_own_ip = config.get_bool("network", "bmx7_publish_ownip", false) + 1 if (pub_own_ip) then +****0 uci:set(bmx7.f, "myIP4", "tunIn") +****0 uci:set(bmx7.f, "myIP4", "tunIn", "myIP4") +****0 uci:set(bmx7.f, "myIP4", "network", ipv4:host():string()..'/32') +****0 uci:set(bmx7.f, "myIP6", "tunIn") +****0 uci:set(bmx7.f, "myIP6", "tunIn", "myIP6") +****0 uci:set(bmx7.f, "myIP6", "network", ipv6:host():string()..'/128') + end + + -- Enable bmx7 uci config plugin + 1 uci:set(bmx7.f, "config", "plugin") + 1 uci:set(bmx7.f, "config", "plugin", "bmx7_config.so") + + -- Enable JSON plugin to get bmx7 information in json format + 1 if utils.is_installed("bmx7-json") then +****0 uci:set(bmx7.f, "json", "plugin") +****0 uci:set(bmx7.f, "json", "plugin", "bmx7_json.so") + end + + -- Enable topology plugin to get netjson file + 1 if utils.is_installed("bmx7-topology") then +****0 uci:set(bmx7.f, "topology", "plugin") +****0 uci:set(bmx7.f, "topology", "plugin", "bmx7_topology.so") + end + + -- Enable iwinfo plugin to get better link bandwidth estimation + 1 if utils.is_installed("bmx7-iwinfo") then +****0 uci:set(bmx7.f, "iwinfo", "plugin") +****0 uci:set(bmx7.f, "iwinfo", "plugin", "bmx7_iwinfo.so") + end + + -- Enable SMS plugin to enable sharing of small files + 1 if utils.is_installed("bmx7-sms") then +****0 uci:set(bmx7.f, "sms", "plugin") +****0 uci:set(bmx7.f, "sms", "plugin", "bmx7_sms.so") + end + + -- Enable tun plugin, DISCLAIMER: this must be positioned before table plugin if used. + 1 uci:set(bmx7.f, "ptun", "plugin") + 1 uci:set(bmx7.f, "ptun", "plugin", "bmx7_tun.so") + + -- Disable ThrowRules because they are broken in IPv6 with current Linux Kernel + 1 uci:set(bmx7.f, "ipVersion", "ipVersion") + 1 uci:set(bmx7.f, "ipVersion", "ipVersion", "6") + + -- Search for networks in 172.16.0.0/12 + 1 uci:set(bmx7.f, "nodes", "tunOut") + 1 uci:set(bmx7.f, "nodes", "tunOut", "nodes") + 1 uci:set(bmx7.f, "nodes", "network", "172.16.0.0/12") + + -- Search for networks in 10.0.0.0/8 + 1 uci:set(bmx7.f, "clouds", "tunOut") + 1 uci:set(bmx7.f, "clouds", "tunOut", "clouds") + 1 uci:set(bmx7.f, "clouds", "network", "10.0.0.0/8") + + -- Search for internet in the mesh cloud + 1 uci:set(bmx7.f, "inet4", "tunOut") + 1 uci:set(bmx7.f, "inet4", "tunOut", "inet4") + 1 uci:set(bmx7.f, "inet4", "network", "0.0.0.0/0") + 1 uci:set(bmx7.f, "inet4", "maxPrefixLen", "0") + + -- Search for internet IPv6 gateways in the mesh cloud + 1 uci:set(bmx7.f, "inet6", "tunOut") + 1 uci:set(bmx7.f, "inet6", "tunOut", "inet6") + 1 uci:set(bmx7.f, "inet6", "network", "::/0") + 1 uci:set(bmx7.f, "inet6", "maxPrefixLen", "0") + + -- Search for other mesh cloud announcements that have public ipv6 + 1 uci:set(bmx7.f, "publicv6", "tunOut") + 1 uci:set(bmx7.f, "publicv6", "tunOut", "publicv6") + 1 uci:set(bmx7.f, "publicv6", "network", "2000::/3") + 1 uci:set(bmx7.f, "publicv6", "maxPrefixLen", "64") + + -- Set prefered GW if defined + 1 local pref_gw = config.get("network", "bmx7_pref_gw") + 1 if (pref_gw ~= "none") then +****0 uci:set(bmx7.f, "inet4p", "tunOut") +****0 uci:set(bmx7.f, "inet4p", "tunOut", "inet4p") +****0 uci:set(bmx7.f, "inet4p", "network", "0.0.0.0/0") +****0 uci:set(bmx7.f, "inet4p", "maxPrefixLen", "0") +****0 uci:set(bmx7.f, "inet4p", "gwName", pref_gw) +****0 uci:set(bmx7.f, "inet4p", "rating", "1000") + +****0 uci:set(bmx7.f, "inet6p", "tunOut") +****0 uci:set(bmx7.f, "inet6p", "tunOut", "inet6p") +****0 uci:set(bmx7.f, "inet6p", "network", "::/0") +****0 uci:set(bmx7.f, "inet6p", "maxPrefixLen", "0") +****0 uci:set(bmx7.f, "inet6p", "gwName", pref_gw) +****0 uci:set(bmx7.f, "inet6p", "rating", "1000") + else + 1 uci:delete(bmx7.f, "inet4p", "tunOut") + 1 uci:delete(bmx7.f, "inet6p", "tunOut") + end + + 1 local hasBatadv = false + 1 local bmxOverBatdv = config.get_bool("network", "bmx7_over_batman") + 1 local hasLan = false + 11 for _,protoArgs in pairs(config.get("network", "protocols")) do + 10 local proto = utils.split(protoArgs, network.protoParamsSeparator)[1] + 10 if(proto == "lan") then hasLan = true + 9 elseif(proto == "batadv") then hasBatadv = true end + end + + 1 if config.get("network", "bmx7_over_librenet6", false) then +****0 uci:set(bmx7.f, "librenet6", "dev") +****0 uci:set(bmx7.f, "librenet6", "dev", "librenet6") + end + + 1 local enablePKI = config.get_bool("network", "bmx7_enable_pki") + 1 if (enablePKI) then +****0 uci:set(bmx7.f, "general", "trustedNodesDir", "/etc/bmx7/trustedNodes") + end + + 1 if(hasLan) then + 1 uci:set(bmx7.f, "lm_net_br_lan", "dev") + 1 uci:set(bmx7.f, "lm_net_br_lan", "dev", "br-lan") + end + + 1 if(hasLan and hasBatadv and not bmxOverBatdv) then + --! Let firewall4 append a table of family 'bridge' with a chain + --! that hooks into postrouting and prevents bmx7 over batadv. + 1 local includeDir = "/usr/share/nftables.d/ruleset-post/" + 1 local fileName = "lime-proto-bmx7_bmx7-not-over-bat0.nft" + 1 fs.mkdirr(includeDir) + 2 fs.symlink( + 1 "/usr/share/lime/"..fileName, + 1 includeDir..fileName + 1 ) + else +****0 fs.unlink( +****0 "/usr/share/nftables.d/ruleset-post/".. + "lime-proto-bmx7_bmx7-not-over-bat0.nft" + ) + end + + 1 uci:save(bmx7.f) + + 1 uci:delete("firewall", "bmxtun") + + 1 uci:set("firewall", "bmxtun", "zone") + 1 uci:set("firewall", "bmxtun", "name", "bmx7tun") + 1 uci:set("firewall", "bmxtun", "input", "ACCEPT") + 1 uci:set("firewall", "bmxtun", "output", "ACCEPT") + 1 uci:set("firewall", "bmxtun", "forward", "ACCEPT") + 1 uci:set("firewall", "bmxtun", "mtu_fix", "1") + 1 uci:set("firewall", "bmxtun", "conntrack", "1") + 1 uci:set("firewall", "bmxtun", "device", "X7+") + 1 uci:set("firewall", "bmxtun", "family", "ipv4") + + 1 uci:save("firewall") + end + + 2 function bmx7.setup_interface(ifname, args) + 11 if not args["specific"] and + 10 ( ifname:match("^wlan%d+.ap") or ifname:match("^eth%d+") ) + 6 then return end + + 5 vlanId = tonumber(args[2]) or 18 + 5 vlanProto = args[3] or "8021ad" + 5 nameSuffix = args[4] or "_bmx7" + + 5 local owrtInterfaceName, linux802adIfName, owrtDeviceName = network.createVlanIface(ifname, vlanId, nameSuffix, vlanProto) + + 5 local uci = libuci:cursor() + 5 local mtu = config.get("network", "bmx7_mtu", "1500") + 5 uci:set("network", owrtDeviceName, "mtu", mtu) + + -- BEGIN [Workaround issue 38] + 5 if ifname:match("^wlan%d+") then + 3 local macAddr = wireless.get_phy_mac("phy"..ifname:match("%d+")) + 3 local vlanIp = { 169, 254, tonumber(macAddr[5], 16), tonumber(macAddr[6], 16) } + 3 uci:set("network", owrtInterfaceName, "proto", "static") + 3 uci:set("network", owrtInterfaceName, "ipaddr", table.concat(vlanIp, ".")) + 3 uci:set("network", owrtInterfaceName, "netmask", "255.255.255.255") + end + --- END [Workaround issue 38] + + 5 uci:save("network") + + 5 uci:set(bmx7.f, owrtInterfaceName, "dev") + 5 uci:set(bmx7.f, owrtInterfaceName, "dev", linux802adIfName) + + 5 if ifname:match("^wlan%d+") then + 3 local rateMax = config.get("network", "bmx7_wifi_rate_max", "auto") + 3 if rateMax ~= "auto" then +****0 uci:set(bmx7.f, owrtInterfaceName, "rateMax", rateMax) + end + end + + 5 uci:save(bmx7.f) + end + + 2 function bmx7.apply() +****0 utils.unsafe_shell("killall bmx7 ; sleep 2 ; killall -9 bmx7") +****0 utils.unsafe_shell("bmx7") + end + + 2 function bmx7.bgp_conf(templateVarsIPv4, templateVarsIPv6) +****0 local uci = libuci:cursor() + + -- Enable Routing Table Redistribution plugin +****0 uci:set(bmx7.f, "table", "plugin") +****0 uci:set(bmx7.f, "table", "plugin", "bmx7_table.so") + + -- Redistribute proto bird routes +****0 uci:set(bmx7.f, "fromBird", "redistTable") +****0 uci:set(bmx7.f, "fromBird", "redistTable", "fromBird") +****0 uci:set(bmx7.f, "fromBird", "table", "254") +****0 uci:set(bmx7.f, "fromBird", "bandwidth", "100") +****0 uci:set(bmx7.f, "fromBird", "proto", "12") + + -- Avoid aggregation as it use lot of CPU with huge number of routes +****0 uci:set(bmx7.f, "fromBird", "aggregatePrefixLen", "128") + + -- Disable proactive tunnels announcement as it use lot of CPU with + -- huge number of routes +****0 uci:set(bmx7.f, "general", "proactiveTunRoutes", "0") + + -- BMX7 security features are at moment not used by LiMe, disable hop + -- by hop links signature as it consume a lot of CPU expecially in + -- setups with multiples interfaces and lot of routes like LiMe +****0 uci:set(bmx7.f, "general", "linkSignatureLen", "0") + +****0 uci:save(bmx7.f) + + local base_bgp_conf = [[ + protocol direct { + interface "X7*"; + } +****0 ]] + +****0 return base_bgp_conf + end + + 1 return bmx7 + +============================================================================== +packages/lime-proto-olsr/files/usr/lib/lua/lime/proto/olsr.lua +============================================================================== + #!/usr/bin/lua + + 1 local network = require("lime.network") + 1 local config = require("lime.config") + 1 local fs = require("nixio.fs") + 1 local libuci = require("uci") + 1 local wireless = require("lime.wireless") + 1 local utils = require("lime.utils") + 1 local ip = require("luci.ip") + + 1 local olsr = {} + + 1 olsr.configured = false + + 1 function olsr.configure(args) + 12 if olsr.configured then return end + 1 olsr.configured = true + + 1 local uci = libuci:cursor() + 1 local ipv4 = network.primary_address() + + 1 fs.writefile("/etc/config/olsrd", "") + + 1 uci:set("olsrd", "lime", "olsrd") + 1 uci:set("olsrd", "lime", "LinkQualityAlgorithm", "etx_ff") + 1 uci:set("olsrd", "lime", "IpVersion", "4") + + 1 uci:set("olsrd", "limejson", "LoadPlugin") + 1 uci:set("olsrd", "limejson", "library", "olsrd_jsoninfo.so.0.0") + 1 uci:set("olsrd", "limejson", "accept", "127.0.0.1") + + 1 uci:set("olsrd", "limehna", "Hna4") + 1 uci:set("olsrd", "limehna", "netaddr", ipv4:network():string()) + 1 uci:set("olsrd", "limehna", "netmask", ipv4:mask():string()) + + 1 uci:save("olsrd") + end + + 1 function olsr.setup_interface(ifname, args) + 11 if not args["specific"] then + 10 if ifname:match("^wlan%d+.ap") then return end + end + + 5 local vlanId = tonumber(args[2]) or 14 + 5 local vlanProto = args[3] or "8021ad" + 5 local nameSuffix = args[4] or "_olsr" + 5 local ipPrefixTemplate = args[5] or "169.254.%M5.%M6/16" + 5 local owrtInterfaceName, linux802adIfName, owrtDeviceName = network.createVlanIface(ifname, vlanId, nameSuffix, vlanProto) + 5 local macAddr = network.get_mac(utils.split(ifname, ".")[1]) + 5 local ipAddr = ip.IPv4(utils.applyMacTemplate10(ipPrefixTemplate, macAddr)) + + 5 local uci = libuci:cursor() + 5 uci:set("network", owrtInterfaceName, "proto", "static") + 5 uci:set("network", owrtInterfaceName, "ipaddr", ipAddr:host():string()) + 5 uci:set("network", owrtInterfaceName, "netmask", ipAddr:mask():string()) + 5 uci:save("network") + + 5 uci:set("olsrd", owrtInterfaceName, "Interface") + 5 uci:set("olsrd", owrtInterfaceName, "interface", owrtInterfaceName) + 5 uci:save("olsrd") + end + + + 1 return olsr + +============================================================================== +packages/lime-proto-olsr2/files/usr/lib/lua/lime/proto/olsr2.lua +============================================================================== + #!/usr/bin/lua + + 1 local network = require("lime.network") + 1 local config = require("lime.config") + 1 local fs = require("nixio.fs") + 1 local libuci = require("uci") + 1 local wireless = require("lime.wireless") + 1 local utils = require("lime.utils") + 1 local ip = require("luci.ip") + 1 olsr2 = {} + + 1 olsr2.configured = false + + 2 function olsr2.configure(args) + 12 if olsr2.configured then return end + 1 olsr2.configured = true + + 1 local uci = libuci:cursor() + 1 local ipv4, ipv6 = network.primary_address() + 1 local origInterfaceName = network.limeIfNamePrefix.."olsr_originator_lo" + + 1 fs.writefile("/etc/config/olsrd2", "") + 1 uci:set("olsrd2", "lime", "global") + 1 uci:set("olsrd2", "lime", "failfast", "no") + 1 uci:set("olsrd2", "lime", "pidfile", "/var/run/olsrd2.pid") + 1 uci:set("olsrd2", "lime", "lockfile", "/var/lock/olsrd2") + 1 uci:set("olsrd2", "lime", "olsrv2") + 1 uci:set("olsrd2", "lime", "lan", {ipv4:string(), ipv6:string()}) + 1 uci:set("olsrd2", "limelog", "log") + 1 uci:set("olsrd2", "limejson", "syslog", "true") + 1 uci:set("olsrd2", "limejson", "info", "all") + 1 uci:set("olsrd2", "limetelnet", "telnet") + 1 uci:set("olsrd2", "limetelnet", "port", "2009") + 1 uci:set("olsrd2", origInterfaceName, "interface") + 1 uci:set("olsrd2", origInterfaceName, "ifname", "loopback") + 1 uci:save("olsrd2") + + 1 uci:set("network", origInterfaceName, "interface") + 1 uci:set("network", origInterfaceName, "ifname", "@loopback") + 1 uci:set("network", origInterfaceName, "proto", "static") + 1 uci:set("network", origInterfaceName, "ipaddr", ipv4:host():string()) + 1 uci:set("network", origInterfaceName, "netmask", "255.255.255.255") + 1 uci:set("network", origInterfaceName, "ip6addr", ipv6:host():string().."/128") + 1 uci:save("network") + end + + 2 function olsr2.setup_interface(ifname, args) + 11 if not args["specific"] then + 10 if ifname:match("^wlan%d+.ap") then return end + end + 5 local vlanId = tonumber(args[2]) or 16 + 5 local vlanProto = args[3] or "8021ad" + 5 local nameSuffix = args[4] or "_olsr" + 5 local owrtInterfaceName, linux802adIfName, owrtDeviceName = network.createVlanIface(ifname, vlanId, nameSuffix, vlanProto) + 5 local uci = libuci:cursor() + + 5 uci:set("olsrd2", owrtInterfaceName, "interface") + 5 uci:set("olsrd2", owrtInterfaceName, "ifname", owrtInterfaceName) + 5 uci:save("olsrd2") + + end + + + 1 return olsr2 + +============================================================================== +packages/lime-proto-olsr6/files/usr/lib/lua/lime/proto/olsr6.lua +============================================================================== + #!/usr/bin/lua + + 1 local network = require("lime.network") + 1 local config = require("lime.config") + 1 local fs = require("nixio.fs") + 1 local libuci = require("uci") + 1 local wireless = require("lime.wireless") + 1 local utils = require("lime.utils") + 1 local ip = require("luci.ip") + + 1 local olsr = {} + + 1 olsr.configured = false + + 1 function olsr.configure(args) + 12 if olsr.configured then return end + 1 olsr.configured = true + + 1 local uci = libuci:cursor() + 1 local _, ipv6 = network.primary_address() + + 1 fs.writefile("/etc/config/olsrd6", "") + + 1 uci:set("olsrd6", "lime", "olsrd") + 1 uci:set("olsrd6", "lime", "LinkQualityAlgorithm", "etx_ff") + 1 uci:set("olsrd6", "lime", "IpVersion", "6") + + 1 uci:set("olsrd6", "limejson", "LoadPlugin") + 1 uci:set("olsrd6", "limejson", "library", "olsrd_jsoninfo.so.0.0") + 1 uci:set("olsrd6", "limejson", "accept", "::1") + + 1 uci:set("olsrd6", "limehna", "Hna6") + 1 uci:set("olsrd6", "limehna", "netaddr", ipv6:network():string()) + 1 uci:set("olsrd6", "limehna", "prefix", ipv6:prefix()) + + 1 uci:save("olsrd6") + end + + 1 function olsr.setup_interface(ifname, args) + 11 if not args["specific"] then + 10 if ifname:match("^wlan%d+.ap") then return end + end + + 5 vlanId = tonumber(args[2]) or 15 + 5 vlanProto = args[3] or "8021ad" + 5 nameSuffix = args[4] or "_olsr6" + 5 local ipPrefixTemplate = args[5] or "fc00::%M1%M2:%M3%M4:%M5%M6/64" + + 5 local owrtInterfaceName, linux802adIfName, owrtDeviceName = network.createVlanIface(ifname, vlanId, nameSuffix, vlanProto) + 5 local macAddr = network.get_mac(utils.split(ifname, ".")[1]) + 5 local ipAddr = ip.IPv6(utils.applyMacTemplate16(ipPrefixTemplate, macAddr)) + + 5 local uci = libuci:cursor() + 5 uci:set("network", owrtInterfaceName, "proto", "static") + 5 uci:set("network", owrtInterfaceName, "ip6addr", ipv6:string()) +****0 uci:save("network") + +****0 uci:set("olsrd6", owrtInterfaceName, "Interface") +****0 uci:set("olsrd6", owrtInterfaceName, "interface", owrtInterfaceName) +****0 uci:save("olsrd6") + end + + 1 return olsr + +============================================================================== +packages/lime-proto-wan/files/usr/lib/lua/lime/proto/wan.lua +============================================================================== + #!/usr/bin/lua + + --! LibreMesh community mesh networks meta-firmware + --! + --! Copyright (C) 2014-2023 Gioacchino Mazzurco + --! Copyright (C) 2023 Asociación Civil Altermundi + --! + --! SPDX-License-Identifier: AGPL-3.0-only + + 1 local libuci = require("uci") + + 1 wan = {} + + 1 wan.configured = false + + 2 function wan.configure(args) + 1 if wan.configured then return end + 1 wan.configured = true + + 1 local uci = libuci:cursor() + 1 uci:set("network", "wan", "interface") + 1 uci:set("network", "wan", "proto", "dhcp") + 1 uci:save("network") + end + + 2 function wan.setup_interface(ifname, args) + 1 local uci = libuci:cursor() + 1 uci:set("network", "wan", "device", ifname) + 1 uci:save("network") + + --! Accepting link local traffic also on WAN should not cause hazards. + --! It is very helpful in cases where the devices have problem to the other + --! ports, to have at least an addictional way to enter for rescue operation + 1 local ALLOW_WAN_LL_SECT = "lime_allow_wan_all_link_local" + 1 uci:set("firewall", ALLOW_WAN_LL_SECT, "rule") + 1 uci:set("firewall", ALLOW_WAN_LL_SECT, "name", ALLOW_WAN_LL_SECT) + 1 uci:set("firewall", ALLOW_WAN_LL_SECT, "src", "wan") + 1 uci:set("firewall", ALLOW_WAN_LL_SECT, "family", "ipv6") + 1 uci:set("firewall", ALLOW_WAN_LL_SECT, "src_ip", "fe80::/10") + 1 uci:set("firewall", ALLOW_WAN_LL_SECT, "dest_ip", "fe80::/10") + 1 uci:set("firewall", ALLOW_WAN_LL_SECT, "target", "ACCEPT") + 1 uci:save("firewall") + end + + 1 return wan + +============================================================================== +packages/lime-system/files/usr/bin/migrate-wifi-bands-cfg +============================================================================== + #!/usr/bin/lua + + 3 local config = require 'lime.config' + 3 local wireless = require 'lime.wireless' + 3 local uci = config.get_uci_cursor() + + 3 local uci_files = { config.UCI_COMMUNITY_NAME, config.UCI_NODE_NAME } + + local function move_modes_to_specific_band(file_name) + 6 local modes = uci:get(file_name, 'wifi', 'modes') + 6 if modes == nil then + 3 return + end + 3 local modes_2ghz = {} + 3 local modes_5ghz = {} + 11 for _, mode in pairs(modes) do + 8 local mode_name = utils.split(mode, '_')[1] + 8 local mode_band = utils.split(mode, '_')[2] + 8 if mode_band == nil or mode_band == '2ghz' then + 7 table.insert(modes_2ghz, mode_name) + end + 8 if mode_band == nil or mode_band == '5ghz' then + 2 table.insert(modes_5ghz, mode_name) + end + end + 3 modes_2ghz = utils.tableLength(modes_2ghz) > 0 and modes_2ghz or {'manual'} + 3 modes_5ghz = utils.tableLength(modes_5ghz) > 0 and modes_5ghz or {'manual'} + 3 uci:set(file_name, '2ghz', 'lime-wifi-band') + 3 uci:set(file_name, '2ghz', 'modes', modes_2ghz) + 3 uci:set(file_name, '5ghz', 'lime-wifi-band') + 3 uci:set(file_name, '5ghz', 'modes', modes_5ghz) + 3 uci:delete(file_name, 'wifi', 'modes') + end + + + local function move_restof_to_specific_band(file_name) + 6 local options = uci:get_all(file_name, 'wifi') + 6 if options == nil then +****0 return + end + 27 for key, value in pairs(options) do + 21 local band_name = utils.split(key, '_')[2] + 21 if band_name == '2ghz' or band_name == '5ghz' then + 3 uci:set(file_name, band_name, 'lime-wifi-band') + 3 local derived_key = utils.split(key, '_')[1] + 3 uci:set(file_name, band_name, derived_key, value) + 3 uci:delete(file_name, 'wifi', key) + end + end + end + + local function migrate_file(file_name) + 6 move_modes_to_specific_band(file_name) + 6 move_restof_to_specific_band(file_name) + 6 local changes = uci:changes() + 6 if utils.tableLength(changes) > 0 then + 6 uci:commit(file_name) + end + end + + 9 for _, file_name in pairs(uci_files) do + 6 migrate_file(file_name) + end + + 3 print('migrate-wifi-modes: migration finished, now run lime-config') + +============================================================================== +packages/lime-system/files/usr/lib/lua/lime/config.lua +============================================================================== + #!/usr/bin/lua + + --! LibreMesh is modular but this doesn't mean parallel, modules are executed + --! sequencially, so we don't need to worry about transactionality and all other + --! stuff that affects parrallels database, at moment we don't need parallelism + --! as this is just some configuration stuff and is not performance critical. + + 46 local fs = require("nixio.fs") + 46 local libuci = require("uci") + 46 local nixio = require "nixio" + + 46 config = {} + + 92 function config.log(text) + 3 nixio.syslog('info', '[config] ' .. text) + end + + 46 config.uci = nil + 46 config.hooksDir = "/etc/hotplug.d/lime-config" + + 92 function config.get_uci_cursor() + 394 if config.uci == nil then + 46 config.uci = libuci:cursor() + end + 394 return config.uci + end + + 92 function config.set_uci_cursor(cursor) + 213 config.uci = cursor + end + + 46 config.uci = config.get_uci_cursor() + + 46 config.UCI_AUTOGEN_NAME = 'lime-autogen' + 46 config.UCI_NODE_NAME = 'lime-node' + 46 config.UCI_MAC_NAME = 'lime-000000000000' + 46 config.UCI_COMMUNITY_NAME = 'lime-community' + 46 config.UCI_DEFAULTS_NAME = 'lime-defaults' + 46 config.UCI_CONFIG_NAME = config.UCI_AUTOGEN_NAME + + 92 function config.get_config_path() + 18 return config.uci:get_confdir() .. '/' .. config.UCI_CONFIG_NAME + end + + 92 function config.reset_node_config() + 1 config.initialize_config_file(config.UCI_NODE_NAME) + end + + 92 function config.initialize_config_file(config_name) + 1 local lime_path = config.uci:get_confdir() .. "/" .. config_name + 1 os.execute(string.format('cp /usr/share/lime/configs/%s %s', config_name, lime_path)) + end + + 92 function config.get(sectionname, option, fallback) + 178 local limeconf = config.uci:get(config.UCI_CONFIG_NAME, sectionname, option) + 178 if limeconf then return limeconf end + + 20 if ( fallback ~= nil ) then + 19 config.log("Use fallback value for "..sectionname.."."..option..": "..tostring(fallback)) + 19 return fallback + else + 1 config.log("WARNING: Attempt to access undeclared default for: "..sectionname.."."..option) + 1 config.log(debug.traceback()) + 1 return nil + end + end + + --! Execute +callback+ for each config of type +configtype+ found in + --! +/etc/config/lime-autogen+. + 92 function config.foreach(configtype, callback) + 24 return config.uci:foreach(config.UCI_CONFIG_NAME, configtype, callback) + end + + 92 function config.get_all(sectionname) + 49 return config.uci:get_all(config.UCI_CONFIG_NAME, sectionname) + end + + 92 function config.get_bool(sectionname, option, default) + 23 local val = config.get(sectionname, option, default) + 23 return (val and ((val == '1') or (val == 'on') or (val == 'true') or (val == 'enabled') or (val == 1) or (val == true))) + end + + 46 config.batched = false + + 92 function config.init_batch() + 2 config.batched = true + end + + 92 function config.set(...) + 91 local aty = type(arg[3]) + 91 if (aty ~= "nil" and aty ~= "string" and aty ~= "table") then + 4 arg[3] = tostring(arg[3]) + end + 91 config.uci:set(config.UCI_CONFIG_NAME, unpack(arg)) + 91 if(not config.batched) then config.uci:save(config.UCI_CONFIG_NAME) end + end + + 92 function config.delete(...) + 3 config.uci:delete(config.UCI_CONFIG_NAME, unpack(arg)) + 3 if(not config.batched) then config.uci:save(config.UCI_CONFIG_NAME) end + end + + 92 function config.end_batch() + 2 if(config.batched) then + 2 config.uci:save(config.UCI_CONFIG_NAME) + 2 config.batched = false + end + end + + 92 function config.autogenerable(section_name) + 5 return ( (not config.get_all(section_name)) or config.get_bool(section_name, "autogenerated") ) + end + + + --! Merge two uci files. If an option exists in both files the value of high_prio is selected + 92 function config.uci_merge_files(high_prio, low_prio, output) + 52 local uci = config.get_uci_cursor() + + 52 local high_pt = uci:get_all(high_prio) + 52 local low_pt = uci:get_all(low_prio) + + --! populate high_prio with low_prio values that are not in high_prio + 109 for section_name, section in pairs(low_pt) do + 57 local high_section = high_pt[section_name] + 57 if high_section ~= nil then + --! copy only some attributes + 231 for option_name, option in pairs(section) do + --! if the options starts with a dot it is not a real options it is an attribute + --! like .name, .type, .anonymous and .index + 196 if option_name[1] ~= '.' and high_section[option_name] == nil then + 26 high_section[option_name] = option + end + end + else + 22 high_pt[section_name] = section + end + end + + --! populate output from high_prio using uci + 141 for section_name, section in pairs(high_pt) do + 89 local section_type = section['.type'] + 89 uci:set(output, section_name, section_type) + 719 for option_name, option in pairs(section) do + --! if the options starts with a dot it is not a real options it is an attribute + --! like .name, .type, .anonymous and .index + 630 if option_name[1] ~= '.' then + 630 local otype = type(option) + 630 if (otype ~= "nil" and otype ~= "string" and otype ~= "table") then + 178 option = tostring(option) + end + 630 uci:set(output, section_name, option_name, option) + end + end + end + 52 uci:commit(output) + end + + 92 function config.uci_autogen() + --! start clearing the config + 17 local f = io.open(config.get_config_path(), "w") + 17 f:write('') + 17 f:close() + + --! clean uci cache + 17 local uci = config.get_uci_cursor() + 17 uci:load(config.UCI_AUTOGEN_NAME) + + 85 for _, cfg_name in pairs({config.UCI_DEFAULTS_NAME, config.UCI_COMMUNITY_NAME, config.UCI_MAC_NAME, config.UCI_NODE_NAME}) do + 68 local cfg = io.open(config.uci:get_confdir() .. '/' .. cfg_name) + 68 if cfg ~= nil then + 51 config.uci_merge_files(cfg_name, config.UCI_AUTOGEN_NAME, config.UCI_AUTOGEN_NAME) + end + end + 17 uci:load(config.UCI_AUTOGEN_NAME) + end + + --! commit all uci changes not yet commited + 92 function config.uci_commit_all() + 5 local uci = config.get_uci_cursor() + 8 for k, _ in pairs(uci:changes()) do + 3 assert(uci:commit(k)) + end + end + + 92 function config.main() + --! Get mac address and set mac-based configuration file name + 1 local network = require("lime.network") + 1 local utils = require("lime.utils") + + 1 config.UCI_MAC_NAME = "lime-" .. table.concat(network.primary_mac(),"") + 1 print("Mac-config: " .. config.UCI_MAC_NAME) + + --! Populate the default template configs if lime-node and lime-community + --! are not found in /etc/config + 3 for _, cfg_name in pairs({config.UCI_COMMUNITY_NAME, config.UCI_NODE_NAME}) do + 2 local lime_path = config.uci:get_confdir() .. "/" .. cfg_name + 2 local cf = io.open(lime_path) + 2 if not cf then +****0 config.initialize_config_file(cfg_name) + else + 2 cf:close() + end + end + 1 config.uci_autogen() + + 1 local modules_name = { "hardware_detection", "wireless", "network", "firewall", "system", + 1 "generic_config" } + + 1 if utils.isModuleAvailable("lime.wifi_unstuck_wa") then + 1 table.insert(modules_name, "wifi_unstuck_wa") + end + + 1 local modules = {} + + 8 for i, name in pairs(modules_name) do modules[i] = require("lime."..name) end + 8 for _,module in pairs(modules) do + 7 xpcall(module.clean, function(errmsg) print(errmsg) ; print(debug.traceback()) end) + end + + 8 for _,module in pairs(modules) do + 7 xpcall(module.configure, function(errmsg) print(errmsg) ; print(debug.traceback()) end) + end + + 1 for hook in fs.dir(config.hooksDir) do +****0 local hookCmd = config.hooksDir.."/"..hook.." after" +****0 print("executed hook:", hookCmd, os.execute(hookCmd)) + end + + 1 local cfgpath = config.get_config_path() + --! flush all config changes + 1 local uci = config.get_uci_cursor() + 1 uci:commit(config.UCI_CONFIG_NAME) + 1 local autogen_content = utils.read_file(cfgpath) or '' + 1 notice_message = ('# DO NOT EDIT THIS FILE!. This is autogenerated by lime-config!\n' .. + 1 '# Instead please edit /etc/config/lime-node and/or /etc/config/lime-community files\n' .. + 1 '# and then regenerate this file executing lime-config\n\n') + 1 utils.write_file(cfgpath, notice_message .. autogen_content) + end + + 46 return config + +============================================================================== +packages/lime-system/files/usr/lib/lua/lime/firewall.lua +============================================================================== + #!/usr/bin/lua + + --! LibreMesh community mesh networks meta-firmware + --! + --! Copyright (C) 2020 Asociación Civil Altermundi + --! Copyright (C) 2020 Gioacchino Mazzurco + --! + --! SPDX-License-Identifier: AGPL-3.0-only + + + 1 local fs = require("nixio.fs") + 1 local utils = require("lime.utils") + 1 local config = require("lime.config") + + 1 firewall = {} + + 2 function firewall.clean() + --! There could be things to cleanup here, but we don't do it as it would + --! interfere with rules generated by network protocols, deleting them too. + --! So better we do nothing here. + end + + 2 function firewall.configure() + 1 local uci = config:get_uci_cursor() + 1 local lanIfs = {} + 2 uci:foreach("firewall", "defaults", + function(section) +****0 uci:set("firewall", section[".name"], "input", "ACCEPT") +****0 uci:set("firewall", section[".name"], "output", "ACCEPT") +****0 uci:set("firewall", section[".name"], "forward", "ACCEPT") + end + ) + + 2 uci:foreach("network", "interface", + function(section) + 33 if "lan" == section[".name"] or + 32 "lm_" == string.sub(section[".name"], 1, 3) and + 28 "_if" == string.sub(section[".name"], -3) then + 26 table.insert(lanIfs, section[".name"]) + end + end + ) + + 2 uci:foreach("firewall", "zone", + function(section) +****0 if uci:get("firewall", section[".name"], "name") == "lan" then +****0 uci:set("firewall", section[".name"], "input", "ACCEPT") +****0 uci:set("firewall", section[".name"], "output", "ACCEPT") +****0 uci:set("firewall", section[".name"], "forward", "ACCEPT") +****0 uci:set("firewall", section[".name"], "mtu_fix", "1") +****0 uci:set("firewall", section[".name"], "network", lanIfs) + end + end + ) + end + + 1 return firewall + +============================================================================== +packages/lime-system/files/usr/lib/lua/lime/generic_config.lua +============================================================================== + #!/usr/bin/lua + + 5 local config = require("lime.config") + 5 local utils = require("lime.utils") + + 5 gen_cfg = {} + + 5 gen_cfg.ASSET_BASE_DIR = '/etc/lime-assets/' + 5 gen_cfg.NODE_ASSET_DIRNAME = 'node/' + 5 gen_cfg.COMMUNITY_ASSET_DIRNAME = 'community/' + 5 gen_cfg.CONFIG_FIRST_BOOT_SIGNAL_FILE = '/etc/.cfg_first_boot_already_run' + 5 gen_cfg.RUN_ASSET_AT_FIRSTBOOT = 'ATFIRSTBOOT' + 5 gen_cfg.RUN_ASSET_AT_CONFIG = 'ATCONFIG' + + 10 function gen_cfg.clean() + -- nothing to clean, but needs to be declared to comply with the API + end + + 10 function gen_cfg.configure() + 1 gen_cfg.do_generic_uci_configs() + 1 gen_cfg.do_copy_assets() + 1 if not utils.file_exists(gen_cfg.CONFIG_FIRST_BOOT_SIGNAL_FILE) then + 1 gen_cfg.do_run_assets(gen_cfg.RUN_ASSET_AT_FIRSTBOOT) + 1 utils.write_file(gen_cfg.CONFIG_FIRST_BOOT_SIGNAL_FILE, '') + end + 1 gen_cfg.do_run_assets(gen_cfg.RUN_ASSET_AT_CONFIG) + end + + + --! Generic UCI configuration from libremesh. Eg usage: + --! config generic_uci_config libremap + --! list uci_set "libremap.settings=libremap" + --! list uci_set "libremap.settings.community=our.libre.org" + --! list uci_set "libremap.settings.community_lat=-200.123" + --! list uci_set "libremap.settings.community_lon=500.9" + 10 function gen_cfg.do_generic_uci_configs() + 4 local uci = config.get_uci_cursor() + 4 local ok = true + 4 utils.log("Applying generic configs:") + 8 config.foreach("generic_uci_config", function(gen_uci_cfg) + 4 utils.log(" " .. gen_uci_cfg[".name"]) + 13 for _, v in pairs(gen_uci_cfg["uci_set"]) do + 9 if uci:set(v) ~= true then + 2 utils.log(" Error on generic config uci_set: %s", v) + 2 ok = false + end + end + end) + 4 config.uci_commit_all() + 4 utils.log("Done applying generic configs.") + 4 return ok + end + + 10 function gen_cfg.get_asset(asset) + 5 if (utils.stringStarts(asset, gen_cfg.NODE_ASSET_DIRNAME) or + 3 utils.stringStarts(asset, gen_cfg.COMMUNITY_ASSET_DIRNAME)) then + 4 local asset = gen_cfg.ASSET_BASE_DIR .. asset + 4 if utils.file_exists(asset) then + 3 return asset + end + end + end + + --! copy_asset copy an file from the assets directory into a specified path. + --! The node asset directories are /etc/lime-assets/node and /etc/lime-assets/community. + --! The community directory should contain the same files in all the community nodes. + --! + --! config copy_asset collectd + --! option asset 'community/collectd.conf' # or 'node/collectd.conf' or 'community/mynode_collectd.conf' + --! option dst '/etc/collectd.conf' + --! + + 10 function gen_cfg.do_copy_assets() + 3 local uci = config.get_uci_cursor() + 3 local ok = true + 3 utils.log("Copying assets:") + 6 config.foreach("copy_asset", function(copy_asset) + 2 local asset = copy_asset["asset"] + 2 utils.log(" %s (%s)", copy_asset[".name"], asset) + 2 local dst = copy_asset["dst"] + 2 local src = gen_cfg.get_asset(asset) + 2 if src ~= nil then + 1 local dst_dirname = dst:match("(.*/)") + 1 if not utils.file_exists(dst_dirname) then +****0 os.execute("mkdir -p " .. utils.shell_quote(dst_dirname)) + end + + 1 src = utils.shell_quote(src) + 1 dst = utils.shell_quote(dst) + 1 os.execute('cp -dpf ' .. src .. ' ' .. dst) + else + 1 utils.log(" Error copying asset '%s': file not found.", asset) + 1 ok = false + end + end) + 3 utils.log("Done copying assets.") + 3 return ok + end + + --! Executes a file from the assets directory scheme explained in copy_asset. + --! + --! config run_asset dropbear + --! option asset 'community/dropbear.sh' + --! option when 'ATFIRSTBOOT' # ATFIRSTBOOT, ATCONFIG + --! + 10 function gen_cfg.do_run_assets(when) + 5 local uci = config.get_uci_cursor() + 5 local ok = true + 5 utils.log("Running assets on " .. when .. " :") + 10 config.foreach("run_asset", function(run_asset) + 3 local asset = run_asset["asset"] + 3 if run_asset["when"] == when then + 3 utils.log(" %s (%s)", run_asset[".name"], asset) + 3 local src = gen_cfg.get_asset(asset) + 3 if src ~= nil then + 2 local retval = os.execute("chmod +x " .. src .. "; " .. src) + 2 if retval ~= 0 then + 1 utils.log(" Warning: the asset '%s': returnen non zero status.", src) + 1 ok = false + end + else + 1 utils.log(" Error running asset '%s': file not found .", asset) + 1 ok = false + end + end + end) + 5 utils.log("Done running assets.") + 5 return ok + end + + + 5 return gen_cfg + +============================================================================== +packages/lime-system/files/usr/lib/lua/lime/hardware_detection.lua +============================================================================== + #!/usr/bin/lua + + 2 local fs = require("nixio.fs") + 2 local utils = require("lime.utils") + + + 2 local hardware_detection = {} + + 2 hardware_detection.sectionNamePrefix = "lm_hwd_" + 2 hardware_detection.search_paths = {"/usr/lib/lua/lime/hwd/*.lua"} + + --! Hardware detection module clean() + --! Call clean() from all installed submodules + 2 function hardware_detection.clean() + 3 for _,search_path in ipairs(hardware_detection.search_paths) do + 6 for hwd_module_path in fs.glob(search_path) do + 4 local module_name = "lime.hwd." .. fs.basename(hwd_module_path):sub(1,-5) + 4 if utils.isModuleAvailable(module_name) then + 4 require(module_name).clean() + end + end + end + end + + --! Hardware detection module configure() + --! Call detect_hardware() from all installed submodules + 2 function hardware_detection.configure() + 3 for _,search_path in ipairs(hardware_detection.search_paths) do + 6 for hwd_module_path in fs.glob(search_path) do + 4 local module_name = "lime.hwd." .. fs.basename(hwd_module_path):sub(1,-5) + 4 if utils.isModuleAvailable(module_name) then + 4 require(module_name).detect_hardware() + end + end + end + end + + + 2 return hardware_detection + +============================================================================== +packages/lime-system/files/usr/lib/lua/lime/mode/ap.lua +============================================================================== + #!/usr/bin/lua + + 1 local ap = {} + + 1 ap.wifi_mode="ap" + + 1 function ap.setup_radio(radio, args) + --! checks("table", "?table") + + 3 args["network"] = "lan" + 3 return wireless.createBaseWirelessIface(radio, ap.wifi_mode, nil, args) + end + + 1 return ap + +============================================================================== +packages/lime-system/files/usr/lib/lua/lime/mode/apname.lua +============================================================================== + #!/usr/bin/lua + + 1 local apname = {} + + 1 apname.wifi_mode="ap" + + 1 function apname.setup_radio(radio, args) + --! checks("table", "?table") + + 3 args["network"] = "lan" + 3 return wireless.createBaseWirelessIface(radio, apname.wifi_mode, "name", args) + end + + 1 return apname + +============================================================================== +packages/lime-system/files/usr/lib/lua/lime/mode/apup.lua +============================================================================== + #!/usr/bin/lua + + --! LibreMesh community mesh networks meta-firmware + --! + --! Copyright (C) 2024 Gioacchino Mazzurco + --! Copyright (C) 2024 Asociación Civil Altermundi + --! + --! SPDX-License-Identifier: AGPL-3.0-only + + 3 local wireless = require("lime.wireless") + + 3 local apup = {} + + 3 function apup.WIFI_MODE() +****0 return "ap" + end + + 3 function apup.WIFI_MODE_SUFFIX() +****0 return "up" + end + + 3 function apup.PEER_SUFFIX() + 27 return "peer" + end + + 3 function apup.setup_radio(radio, args) + --! checks("table", "?table") + +****0 args["network"] = "lan" +****0 args["apup"] = "1" +****0 args["apup_peer_ifname_prefix"] = +****0 wireless.calcIfname(radio[".name"], apup.PEER_SUFFIX(), "") + +****0 return wireless.createBaseWirelessIface( +****0 radio, apup.WIFI_MODE(), apup.WIFI_MODE_SUFFIX(), args ) + end + + --! TODO: port all modes to .WIFI_MODE() + 3 apup.wifi_mode="ap" + + 3 return apup + +============================================================================== +packages/lime-system/files/usr/lib/lua/lime/mode/client.lua +============================================================================== + #!/usr/bin/lua + + 1 local client = {} + + 1 client.wifi_mode="sta" + + 1 function client.setup_radio(radio, args) + --! checks("table", "?table") + 1 return wireless.createBaseWirelessIface(radio, client.wifi_mode, nil, args) + end + + 1 return client + +============================================================================== +packages/lime-system/files/usr/lib/lua/lime/mode/ieee80211s.lua +============================================================================== + #!/usr/bin/lua + + 2 local ieee80211s = {} + + 2 ieee80211s.wifi_mode="mesh" + + 2 function ieee80211s.setup_radio(radio, args) + --! checks("table", "?table") + + 8 return wireless.createBaseWirelessIface(radio, ieee80211s.wifi_mode, nil, args) + end + + 2 return ieee80211s + +============================================================================== +packages/lime-system/files/usr/lib/lua/lime/network.lua +============================================================================== + #!/usr/bin/lua + + --! LibreMesh community mesh networks meta-firmware + --! + --! Copyright (C) 2013-2024 Gioacchino Mazzurco + --! Copyright (C) 2023-2024 Asociación Civil Altermundi + --! + --! SPDX-License-Identifier: AGPL-3.0-only + + 19 network = {} + + 19 local ip = require("luci.ip") + 19 local fs = require("nixio.fs") + + 19 local config = require("lime.config") + 19 local utils = require("lime.utils") + + + 19 function network.PROTO_PARAM_SEPARATOR() return ":" end + 19 function network.PROTO_VLAN_SEPARATOR() return "_" end + 19 function network.LIME_UCI_IFNAME_PREFIX() return "lm_net_" end + + + 19 network.MTU_ETH = 1500 + 19 network.MTU_ETH_WITH_VLAN = network.MTU_ETH - 4 + + -- Deprecated use corresponding functions instead + 19 network.protoParamsSeparator=":" + 19 network.protoVlanSeparator="_" + 19 network.limeIfNamePrefix="lm_net_" + + + 38 function network.get_mac(ifname) + 1 local _, macaddr = next(network.get_own_macs(ifname)) + 1 return utils.split(macaddr, ":") + end + + --! Return a table of macs based on the interface globing filter + 38 function network.get_own_macs(interface_filter) + 10 if interface_filter == nil then + 1 interface_filter = '*' + end + + 10 local macs = {} + 10 local search_path = "/sys/class/net/" .. interface_filter .. "/address" + 53 for address_path in fs.glob(search_path) do + 43 mac = io.open(address_path):read("*l") + 43 macs[mac] = 1 + end + + 10 local result = {} + 53 for mac, _ in pairs(macs) do + 43 table.insert(result, mac) + end + 10 return result + end + + + 38 function network.assert_interface_exists(ifname) +****0 assert( ifname ~= nil and ifname ~= "", +****0 "network.primary_interface() could not determine ifname!" ) + +****0 assert( fs.lstat("/sys/class/net/"..ifname), +****0 "network.primary_interface() "..ifname.." doesn't exists!" ) + end + + 38 function network.primary_interface() + 33 local ifname = config.get("network", "primary_interface", "eth0") + 33 if ifname == "auto" then + 1 local board = utils.getBoardAsTable() + 1 ifname = board['network']['lan']['device'] + end + 33 network.assert_interface_exists(ifname) + 33 return ifname + end + + 38 function network.primary_mac() + 31 return network.get_mac(network.primary_interface()) + end + + 38 function network.generate_host(ipprefix, hexsuffix) + 24 local num = 0 + -- If it's a network prefix calculate offset to add + 24 if ipprefix:equal(ipprefix:network()) then + 24 local addr_len = ipprefix:is4() and 32 or ipprefix:is6() and 128 + 24 num = tonumber(hexsuffix,16) % 2^(addr_len - ipprefix:prefix()) + end + + 24 return ipprefix:add(num) + end + + 38 function network.primary_address(offset) + 12 local offset = offset or 0 + 12 local pm = network.primary_mac() + 12 local ipv4_template = config.get("network", "main_ipv4_address") + 12 local ipv6_template = config.get("network", "main_ipv6_address") + + 12 local ipv4_maskbits = ipv4_template:match("[^/]+/(%d+)") + 12 ipv4_template = ipv4_template:gsub("/%d-/","/") + 12 local ipv6_maskbits = ipv6_template:match("[^/]+/(%d+)") + 12 ipv6_template = ipv6_template:gsub("/%d-/","/") + + 12 ipv4_template = utils.applyMacTemplate10(ipv4_template, pm) + 12 ipv6_template = utils.applyMacTemplate16(ipv6_template, pm) + + 12 ipv4_template = utils.applyNetTemplate10(ipv4_template) + 12 ipv6_template = utils.applyNetTemplate16(ipv6_template) + + 12 local m4, m5, m6 = tonumber(pm[4], 16), tonumber(pm[5], 16), tonumber(pm[6], 16) + 12 local hexsuffix = utils.hex((m4 * 256*256 + m5 * 256 + m6) + offset) + 12 ipv4_template = network.generate_host(ip.IPv4(ipv4_template), hexsuffix) + 12 ipv6_template = network.generate_host(ip.IPv6(ipv6_template), hexsuffix) + + 12 ipv4_template:prefix(tonumber(ipv4_maskbits)) + 12 local mc = ipv4_template + --! Generated address is network address like 192.0.2.0/24 ? + 12 local invalid = ipv4_template:equal(mc:network()) and "NETWORK" + --! If anygw enabled, generated address is the one reserved for anygw like 192.0.2.1/24 ? + 12 if utils.isModuleAvailable("lime.proto.anygw") then + 2 local generalProtocols = config.get("network", "protocols") + 4 for _,protocol in pairs(generalProtocols) do + 2 if protocol == 'anygw' then +****0 invalid = invalid or ipv4_template:equal(mc:minhost()) and "ANYGW" + break + end + end + end + --! Generated address is the broadcast address like 192.0.2.255/24 ? + 12 invalid = invalid or ipv4_template:equal(mc:broadcast()) and "BROADCAST" + 12 if invalid then + 2 ipv4_template = mc:maxhost() + 2 ipv4_template:prefix(tonumber(ipv4_maskbits)) + 4 utils.log("INVALID main_ipv4_address " ..tostring(mc).. " IDENTICAL TO RESERVED " + 2 ..invalid.. " ADDRESS. USING " ..tostring(ipv4_template)) + end + + 12 ipv6_template:prefix(tonumber(ipv6_maskbits)) + + 12 return ipv4_template, ipv6_template + end + + 38 function network.setup_rp_filter() + 2 local sysctl_file_path = "/etc/sysctl.conf"; + 2 local sysctl_options = ""; + 2 local sysctl_file = io.open(sysctl_file_path, "r"); + 4 while sysctl_file:read(0) do + 2 local sysctl_line = sysctl_file:read(); + 2 if not string.find(sysctl_line, ".rp_filter") then sysctl_options = sysctl_options .. sysctl_line .. "\n" end + end + 2 sysctl_file:close() + + 2 sysctl_options = sysctl_options .. "net.ipv4.conf.default.rp_filter=2\nnet.ipv4.conf.all.rp_filter=2\n"; + 2 sysctl_file = io.open(sysctl_file_path, "w"); + 2 if sysctl_file ~= nil then +****0 sysctl_file:write(sysctl_options); +****0 sysctl_file:close(); + end + end + + 38 function network.setup_dns() + 2 local cloudDomain = config.get("system", "domain") + 2 local resolvers = config.get("network", "resolvers") + + 2 local uci = config.get_uci_cursor() + 4 uci:foreach("dhcp", "dnsmasq", + function(s) +****0 uci:set("dhcp", s[".name"], "domain", cloudDomain) +****0 uci:set("dhcp", s[".name"], "local", "/"..cloudDomain.."/") +****0 uci:set("dhcp", s[".name"], "expandhosts", "1") +****0 uci:set("dhcp", s[".name"], "domainneeded", "1") + --! allow queries from non-local ips (i.e. from other clouds) +****0 uci:set("dhcp", s[".name"], "localservice", "0") +****0 uci:set("dhcp", s[".name"], "server", resolvers) +****0 uci:set("dhcp", s[".name"], "confdir", "/etc/dnsmasq.d") + end + ) + 2 uci:save("dhcp") + + 2 fs.mkdir("/etc/dnsmasq.d") + end + + 38 function network.clean() + 1 utils.log("Clearing network config...") + + 1 local uci = config.get_uci_cursor() + + 1 uci:delete("network", "globals", "ula_prefix") + 1 uci:set("network", "wan", "proto", "none") + 1 uci:set("network", "wan6", "proto", "none") + + --! Delete sections generated by LiMe + local function delete_lime_section(s) + 5 if utils.stringStarts(s[".name"], network.limeIfNamePrefix) then +****0 uci:delete("network", s[".name"]) + end + end + 1 uci:foreach("network", "interface", delete_lime_section) + 1 uci:foreach("network", "device", delete_lime_section) + 1 uci:foreach("network", "rule", delete_lime_section) + 1 uci:foreach("network", "route", delete_lime_section) + 1 uci:foreach("network", "rule6", delete_lime_section) + 1 uci:foreach("network", "route6", delete_lime_section) + + 1 uci:save("network") + + 1 if config.get_bool("network", "use_odhcpd", false) then +****0 utils.log("Use odhcpd as dhcp server") +****0 uci:set("dhcp", "odchpd", "maindhcp", 1) +****0 os.execute("[ -e /etc/init.d/odhcpd ] && /etc/init.d/odhcpd enable") + else + 1 utils.log("Disabling odhcpd") + 1 uci:set("dhcp", "odchpd", "maindhcp", 0) + 1 os.execute("[ -e /etc/init.d/odhcpd ] && /etc/init.d/odhcpd disable") + end + + 1 utils.log("Cleaning dnsmasq") + 1 uci:foreach("dhcp", "dnsmasq", function(s) uci:delete("dhcp", s[".name"], "server") end) + 1 uci:save("dhcp") + + 1 utils.log("Disabling 6relayd...") + 1 fs.writefile("/etc/config/6relayd", "") + end + + 38 function network._get_lower(dev) +****0 local lower_if_path = utils.unsafe_shell("ls /sys/class/net/" .. dev .. "/ | grep ^lower") +****0 local lower_if_table = utils.split(lower_if_path, "_") +****0 local lower_if = lower_if_table[#lower_if_table] +****0 return lower_if and lower_if:gsub("\n", "") + end + + 38 function network._is_dsa_conduit(dev) + 17 return "reg" == fs.stat("/sys/class/net/" .. dev .. "/dsa/tagging", "type") + end + + + 38 function network.scandevices(specificIfaces) + 1 local devices = {} + 1 local wireless = require("lime.wireless") + 1 local cpu_ports = {} + 1 local board = utils.getBoardAsTable() + + 1 function dev_parser(dev) + 17 if dev == nil then +****0 utils.log("network.scandevices.dev_parser got nil device") +****0 return + end + + --! Avoid configuration on DSA conduit interfaces. + --! See also: + --! https://www.kernel.org/doc/html/latest/networking/dsa/dsa.html#common-pitfalls-using-dsa-setups + 17 if network._is_dsa_conduit(dev) then +****0 utils.log( "network.scandevices.dev_parser ignored DSA conduit " .. +****0 "device %s", dev ) +****0 return + end + + --! Filter out ethernet ports connected to switch in a swconfig device. + 17 for cpu_port,_ in pairs(cpu_ports) do +****0 if cpu_port == dev then +****0 utils.log( "network.scandevices.dev_parser ignored ethernet " .. +****0 "device %s connected to internal switch", dev ) +****0 return + end + end + + 17 if dev:match("^eth%d+$") then + --! We only get here with devices not listed in board.json, e.g + --! pluggable ethernet dongles. +****0 utils.log( "network.scandevices.dev_parser found plain Ethernet " .. +****0 "device %s", dev ) + 17 elseif dev:match("^wlan%d+"..wireless.WIFI_MODE_SEPARATOR().."%w+$") then + 18 utils.log( "network.scandevices.dev_parser found WiFi device %s", + 18 dev ) + 8 elseif specificIfaces[dev] then +****0 utils.log( "network.scandevices.dev_parser found device %s that " .. + "matches the config net section %s", dev, +****0 specificIfaces[dev][".name"]) + else + 8 return + end + + 9 local is_dsa = utils.is_dsa(dev) + 9 devices[dev] = devices[dev] or {} + 9 devices[dev]["dsa"] = is_dsa + end + + 1 function owrt_ifname_parser(section) + 9 local ifn = section["ifname"] + 9 if ( type(ifn) == "string" ) then + 18 utils.log( "network.scandevices.owrt_ifname_parser found ifname %s", + 9 ifn ) + 9 dev_parser(ifn) + end + end + + 1 function board_port_parser(dev) + 2 local is_dsa = utils.is_dsa(dev) + 2 devices[dev] = devices[dev] or {} + 2 devices[dev]["dsa"] = is_dsa + 2 if is_dsa then +****0 utils.log( "network.scandevices found DSA-port %s in board.json", +****0 dev ) + else + 2 utils.log( "network.scandevices found device %s in board.json", dev ) + end + end + + --! Collect switch facing ethernet ports for swconfig devices from board.json + 1 for switch, switch_table in pairs(board["switch"] or {}) do +****0 for _,port_table in pairs(switch_table["ports"] or {}) do +****0 local dev = port_table["device"] +****0 if dev then +****0 cpu_ports[dev] = true + end + end + end + + --! Collect dsa ports and usable ethernet and vlan devices from board.json + 3 for role, role_table in pairs(board["network"] or {}) do + --! "ports" and "device" fields may be specified at the same time. + --! In this case, "ports" must be used. + 2 local ports = role_table["ports"] + 2 if ports == nil then + 1 ports = { role_table["device"] } + end + 2 local protocol = role_table["protocol"] + --! Protocol can be dhcp, static, pppoe, ncm, qmi, mbim. + --! Ethernet interfaces usually have protocol "dhcp" or "static", + --! depending on their role. + 2 if protocol == "dhcp" or protocol == "static" then + 4 for _,port in pairs(ports) do + 2 board_port_parser(port) + end + end + end + + --! Scrape from uci wireless + 1 local uci = config.get_uci_cursor() + 1 uci:foreach("wireless", "wifi-iface", owrt_ifname_parser) + + --! Scrape from /sys/class/net/ + 1 local stdOut = io.popen("ls -1 /sys/class/net/") + 9 for dev in stdOut:lines() do dev_parser(dev) end + 1 stdOut:close() + + 1 return devices + end + + 38 function network.configure() + 2 local specificIfaces = {} + + 4 config.foreach("net", function(iface) + 1 if iface["linux_name"] then + 1 specificIfaces[iface["linux_name"]] = iface + end + end) + + 2 local fisDevs = network.scandevices(specificIfaces) + + 2 network.setup_rp_filter() + + 2 network.setup_dns() + + 2 local generalProtocols = config.get("network", "protocols") + 13 for _,protocol in pairs(generalProtocols) do + 11 local protoModule = "lime.proto."..utils.split(protocol,":")[1] + 11 if utils.isModuleAvailable(protoModule) then + 9 local proto = require(protoModule) + 27 xpcall(function() proto.configure(utils.split(protocol, network.protoParamsSeparator)) end, + 18 function(errmsg) print(errmsg) ; print(debug.traceback()) end) + end + end + + --! For each scanned fisical device, if there is a specific config apply that one otherwise apply general config + 14 for device,flags in pairs(fisDevs) do + 12 local owrtIf = specificIfaces[device] + 12 local deviceProtos = generalProtocols + 12 if owrtIf then + 1 deviceProtos = owrtIf["protocols"] or {"manual"} + 1 flags["specific"] = true + 1 flags["_specific_section"] = owrtIf + end + + 123 for _,protoParams in pairs(deviceProtos) do + 111 local args = utils.split(protoParams, network.protoParamsSeparator) + 111 local protoName = args[1] + 111 if protoName == "manual" then break end -- If manual is specified do not configure interface + 111 local protoModule = "lime.proto."..protoName + 111 local needsConfig = utils.isModuleAvailable(protoModule) + 111 if protoName ~= 'lan' and not flags["specific"] then + --! Work around issue 1121. Do not configure any other + --! protocols than lime.proto.lan on dsa devices unless there + --! is a config net section for the device. + 90 needsConfig = needsConfig and not utils.is_dsa(device) + end + 111 if needsConfig then + 193 for k,v in pairs(flags) do args[k] = v end + 89 local proto = require(protoModule) + 267 xpcall(function() proto.configure(args) ; proto.setup_interface(device, args) end, + 183 function(errmsg) print(errmsg) ; print(debug.traceback()) end) + end + end + end + end + + 38 function network.sanitizeIfaceName(ifName) + 83 return network.limeIfNamePrefix..ifName:gsub("[^%w_]", "_") + end + + -- Creates a network Interface with static protocol + -- ipAddr can be IPv4 or IPv6 + -- the function can be called twice to set both IPv4 and IPv6 + 38 function network.createStaticIface(linuxBaseIfname, openwrtNameSuffix, ipAddr, gwAddr) +****0 local openwrtNameSuffix = openwrtNameSuffix or "" +****0 local owrtInterfaceName = network.sanitizeIfaceName(linuxBaseIfname) .. openwrtNameSuffix +****0 local uci = config.get_uci_cursor() + +****0 uci:set("network", owrtInterfaceName, "interface") +****0 uci:set("network", owrtInterfaceName, "proto", "static") +****0 uci:set("network", owrtInterfaceName, "auto", "1") +****0 uci:set("network", owrtInterfaceName, "ifname", linuxBaseIfname) + +****0 local addr = luci.ip.new(ipAddr) +****0 local host = addr:host():string() + +****0 if addr:is4() then +****0 local mask = addr:mask():string() +****0 uci:set("network", owrtInterfaceName, "ipaddr", host) +****0 uci:set("network", owrtInterfaceName, "netmask", mask) +****0 if gwAddr then +****0 uci:set("network", owrtInterfaceName, "gateway", gwAddr) + end +****0 elseif addr:is6() then +****0 uci:set("network", owrtInterfaceName, "ip6addr", addr:string()) +****0 if gwAddr then +****0 uci:set("network", owrtInterfaceName, "ip6gw", gwAddr) + end + else +****0 uci:delete("network", owrtInterfaceName, "interface") + end + +****0 uci:save("network") + end + + 38 function network.createVlanIface(linuxBaseIfname, vid, openwrtNameSuffix, vlanProtocol) + 33 vlanProtocol = vlanProtocol or "8021ad" + 33 openwrtNameSuffix = openwrtNameSuffix or "" + 33 vid = tonumber(vid) + + --! sanitize passed linuxBaseIfName for constructing uci section name + --! because only alphanumeric and underscores are allowed + 33 local owrtInterfaceName = network.sanitizeIfaceName(linuxBaseIfname) + 33 local owrtDeviceName = owrtInterfaceName + 33 local linux802adIfName = linuxBaseIfname + + 33 local uci = config.get_uci_cursor() + + 33 owrtInterfaceName = owrtInterfaceName..openwrtNameSuffix.."_if" + + 33 if vid ~= 0 then + 31 local vlanId = tostring(vid) + --! sanitize passed linuxBaseIfName for constructing uci section name + --! because only alphanumeric and underscores are allowed + 31 owrtDeviceName = network.sanitizeIfaceName(linuxBaseIfname)..openwrtNameSuffix.."_dev" + + 31 if linuxBaseIfname:match("^wlan") then + 19 linuxBaseIfname = "@"..network.sanitizeIfaceName(linuxBaseIfname) + end + + --! Do not use . as separator as this will make netifd create an 802.1q interface anyway + --! and sanitize linuxBaseIfName because it can contain dots as well (i.e. switch ports) + 31 linux802adIfName = linux802adIfName:gsub("[^%w-]", "-")..network.protoVlanSeparator..vlanId + + 31 uci:set("network", owrtDeviceName, "device") + 31 uci:set("network", owrtDeviceName, "type", vlanProtocol) + 31 uci:set("network", owrtDeviceName, "name", linux802adIfName) + --! This is ifname also on current OpenWrt + 31 uci:set("network", owrtDeviceName, "ifname", linuxBaseIfname) + 31 uci:set("network", owrtDeviceName, "vid", vlanId) + end + + 33 uci:set("network", owrtInterfaceName, "interface") + 33 local proto = "none" + 33 if vid == 0 then + 2 proto = "static" + end + 33 uci:set("network", owrtInterfaceName, "proto", proto) + 33 uci:set("network", owrtInterfaceName, "auto", "1") + + --! In case of wifi interface not using vlan (vid == 0) avoid to set + --! ifname in network because it is already set in wireless, because + --! setting ifname on both places cause a netifd race condition + 33 if vid ~= 0 or not linux802adIfName:match("^wlan") then + 33 uci:set("network", owrtInterfaceName, "device", linux802adIfName) + end + + 33 uci:save("network") + + 33 return owrtInterfaceName, linux802adIfName, owrtDeviceName + end + + 38 function network.createMacvlanIface(baseIfname, linuxName, argsDev, argsIf) + --! baseIfname can be a linux interface name like eth0 or an openwrt + --! interface name like @lan of the base interface; + --! linuxName is the linux name of the new interface; + --! argsDev optional additional arguments for device like + --! { macaddr="aa:aa:aa:aa:aa:aa", mode="vepa" }; + --! argsIf optional additional arguments for ifname like + --! { proto="static", ip6addr="2001:db8::1/64" } + --! + --! Although this function is defined here lime-system may not depend + --! on macvlan if it doesn't use this function directly. Instead a + --! lime.proto which want to use macvlan so this function should depend + --! on its own on kmod-macvlan as needed. + +****0 argsDev = argsDev or {} +****0 argsIf = argsIf or {} + +****0 local owrtDeviceName = network.limeIfNamePrefix..baseIfname.."_"..linuxName.."_dev" +****0 local owrtInterfaceName = network.limeIfNamePrefix..baseIfname.."_"..linuxName.."_if" + --! sanitize uci sections name +****0 owrtDeviceName = owrtDeviceName:gsub("[^%w_]", "_") +****0 owrtInterfaceName = owrtInterfaceName:gsub("[^%w_]", "_") + +****0 local uci = config.get_uci_cursor() + +****0 uci:set("network", owrtDeviceName, "device") +****0 uci:set("network", owrtDeviceName, "type", "macvlan") +****0 uci:set("network", owrtDeviceName, "name", linuxName) + --! This is ifname also on current OpenWrt +****0 uci:set("network", owrtDeviceName, "ifname", baseIfname) +****0 for k,v in pairs(argsDev) do +****0 uci:set("network", owrtDeviceName, k, v) + end + +****0 uci:set("network", owrtInterfaceName, "interface") +****0 uci:set("network", owrtInterfaceName, "proto", "none") +****0 uci:set("network", owrtInterfaceName, "device", linuxName) +****0 uci:set("network", owrtInterfaceName, "auto", "1") +****0 for k,v in pairs(argsIf) do +****0 uci:set("network", owrtInterfaceName, k, v) + end + +****0 uci:save("network") + +****0 return owrtInterfaceName, linuxName, owrtDeviceName + end + + --! Create a static interface at runtime via ubus + 38 function network.createStatic(linuxBaseIfname) +****0 local ipv4, ipv6 = network.primary_address() +****0 local ubusIfaceName = network.sanitizeIfaceName( +****0 network.LIME_UCI_IFNAME_PREFIX()..linuxBaseIfname.."_static") +****0 local ifaceConf = { + name = ubusIfaceName, + proto = "static", + auto = "1", + ifname = linuxBaseIfname, + ipaddr = ipv4:host():string(), +****0 netmask = "255.255.255.255" + } + +****0 local libubus = require("ubus") +****0 local ubus = libubus.connect() +****0 ubus:call('network', 'add_dynamic', ifaceConf) +****0 ubus:call('network.interface.'..ifaceConf.name, 'up', {}) + + --! TODO: As of today ubus silently fails to properly setup the interface, + --! subsequent status query return NO_DEVICE error + --! ubus -v call network.interface.lm_net_lm_net_wlan0_peer1_static status + --! { + --! "up": false, + --! "pending": false, + --! "available": false, + --! "autostart": true, + --! "dynamic": true, + --! "proto": "static", + --! "data": { + --! + --! }, + --! "errors": [ + --! { + --! "subsystem": "interface", + --! "code": "NO_DEVICE" + --! } + --! ] + --! } + --! + --! ATM work around the problem configuring IP addresses via ip command + +****0 utils.unsafe_shell("ip link set up dev "..ifaceConf.ifname) +****0 utils.unsafe_shell("ip address add "..ifaceConf.ipaddr.."/32 dev "..ifaceConf.ifname) + +****0 return ifaceConf.name + end + + --! Create a vlan at runtime via ubus + 38 function network.createVlan(linuxBaseIfname, vid, vlanProtocol) +****0 local vlanConf = { + name = linuxBaseIfname .. network.PROTO_VLAN_SEPARATOR() .. vid, + type = vlanProtocol or "8021ad", + ifname = linuxBaseIfname, +****0 vid = vid + } + +****0 utils.log("lime.network.createVlan(%s, ...)", linuxBaseIfname) +****0 utils.dumptable(vlanConf) + +****0 local libubus = require("ubus") +****0 local ubus = libubus.connect() +****0 ubus:call('network', 'add_dynamic_device', vlanConf) + + --! TODO: as of today ubus silently fails to properly creating a device + --! dinamycally work around it by using ip command instead +****0 utils.unsafe_shell("ip link add name "..vlanConf.name.." link "..vlanConf.ifname.." type vlan proto 802.1ad id "..vlanConf.vid) + +****0 return vlanConf.name + end + + --! Run protocols at runtime on top of linux network devices + -- TODO: probably some code between here and configure might be deduplicaded + 38 function network.runProtocols(linuxBaseIfname) +****0 utils.log("lime.network.runProtocols(%s, ...)", linuxBaseIfname) +****0 local protoConfs = config.get("network", "protocols") +****0 for _,protoConf in pairs(protoConfs) do +****0 local args = utils.split(protoConf, network.PROTO_PARAM_SEPARATOR()) +****0 local protoModule = "lime.proto."..args[1] +****0 if utils.isModuleAvailable(protoModule) then +****0 local proto = require(protoModule) +****0 xpcall(function() proto.runOnDevice(linuxBaseIfname, args) end, +****0 function(errmsg) print(errmsg) ; print(debug.traceback()) end) + end + end + end + + 19 return network + +============================================================================== +packages/lime-system/files/usr/lib/lua/lime/proto/ieee80211s.lua +============================================================================== + #!/usr/bin/lua + + 1 local config = require("lime.config") + 1 local ieee80211s_mode = require("lime.mode.ieee80211s") + + 1 local ieee80211s = {} + + 1 function ieee80211s.configure(args) + end + + 1 function ieee80211s.setup_interface(ifname, args) + 11 if ifname:match("^wlan%d+."..ieee80211s_mode.wifi_mode) then + 3 local uci = config.get_uci_cursor() + + --! sanitize passed ifname for constructing uci section name + --! because only alphanumeric and underscores are allowed + 3 local networkInterfaceName = network.limeIfNamePrefix..ifname:gsub("[^%w_]", "_") + + 3 uci:set("network", networkInterfaceName, "interface") + 3 uci:set("network", networkInterfaceName, "proto", "none") + 3 uci:set("network", networkInterfaceName, "mtu", "1536") + 3 uci:set("network", networkInterfaceName, "auto", "1") + + 3 uci:save("network") + end + end + + 1 function ieee80211s.runOnDevice(linuxDev, args) end + + 1 return ieee80211s + +============================================================================== +packages/lime-system/files/usr/lib/lua/lime/proto/lan.lua +============================================================================== + #!/usr/bin/lua + + 3 lan = {} + + 3 local network = require("lime.network") + 3 local config = require("lime.config") + 3 local utils = require("lime.utils") + + 3 lan.configured = false + + --! Find a device section in network with + --! option name 'br-lan' + --! option type 'bridge' + local function find_br_lan(uci) + local br_lan_section = nil + 14 uci:foreach("network", "device", + function(s) + 5 if br_lan_section then return end + 5 local dev_type = uci:get("network", s[".name"], "type") + 5 local dev_name = uci:get("network", s[".name"], "name") + 5 if not (dev_type == 'bridge') then return end + 5 if not (dev_name == 'br-lan') then return end + 5 br_lan_section = s[".name"] + end + ) + 7 return br_lan_section + end + + 6 function lan.configure(args) + 13 if lan.configured then return end + 2 lan.configured = true + + 2 local ipv4, ipv6 = network.primary_address() + 2 local uci = config.get_uci_cursor() + 2 uci:set("network", "lan", "interface") + 2 uci:set("network", "lan", "ip6addr", ipv6:string()) + 2 uci:set("network", "lan", "ipaddr", ipv4:host():string()) + 2 uci:set("network", "lan", "netmask", ipv4:mask():string()) + 2 uci:set("network", "lan", "proto", "static") + 2 uci:set("network", "lan", "mtu", "1500") + 2 local br_lan_section = find_br_lan(uci) + 2 if br_lan_section then uci:delete("network", br_lan_section, "ports") end + 2 uci:save("network") + + -- disable bat0 on alfred if batadv not enabled + 2 if utils.is_installed("alfred") then +****0 local is_batadv_enabled = false +****0 local generalProtocols = config.get("network", "protocols") +****0 for _,protocol in pairs(generalProtocols) do +****0 local protoModule = "lime.proto."..utils.split(protocol,":")[1] +****0 if protoModule == "lime.proto.batadv" then +****0 is_batadv_enabled = true + break + end + end +****0 if not is_batadv_enabled then +****0 uci:set("alfred", "alfred", "batmanif", "none") +****0 uci:save("alfred") + end + end + end + + 6 function lan.setup_interface(ifname, args) + 14 if ifname:match("^wlan") then return end + 5 if ifname:match(network.protoVlanSeparator.."%d+$") then return end + + 5 local uci = config.get_uci_cursor() + 5 local bridgedIfs = {} + 5 local br_lan_section = find_br_lan(uci) + 5 if not br_lan_section then return end + 3 local oldIfs = uci:get("network", br_lan_section, "ports") or {} + -- it should be a table, it was a string in old OpenWrt releases + 3 if type(oldIfs) == "string" then oldIfs = utils.split(oldIfs, " ") end + 4 for _,iface in pairs(oldIfs) do + 1 if iface ~= ifname then + 1 table.insert(bridgedIfs, iface) + end + end + 3 table.insert(bridgedIfs, ifname) + 3 uci:set("network", br_lan_section, "ports", bridgedIfs) + 3 uci:save("network") + end + + 6 function lan.bgp_conf(templateVarsIPv4, templateVarsIPv6) + local base_conf = [[ + protocol direct { + interface "br-lan"; + } +****0 ]] +****0 return base_conf + end + + 3 function lan.runOnDevice(linuxDev, args) end + + 3 return lan + +============================================================================== +packages/lime-system/files/usr/lib/lua/lime/system.lua +============================================================================== + #!/usr/bin/lua + + 9 local fs = require("nixio.fs") + + 9 local config = require("lime.config") + 9 local network = require("lime.network") + 9 local utils = require("lime.utils") + + + 9 system = {} + + 18 function system.get_hostname() + 7 local system_hostname = utils.applyMacTemplate16(config.get("system", "hostname"), network.primary_mac()) + 7 return utils.sanitize_hostname(system_hostname) + end + + 18 function system.set_hostname() + 1 local hostname = system.get_hostname() + 1 local uci = config.get_uci_cursor() + 1 uci:foreach("system", "system", function(s) uci:set("system", s[".name"], "hostname", hostname) end) + 1 uci:save("system") + end + + 18 function system.setup_root_password() + + 4 local policy = config.get("system", "root_password_policy") + + 4 if policy == "DO_NOTHING" then + --! nothing... + 2 elseif policy == "SET_SECRET" then + 1 local secret = config.get("system", "root_password_secret") + 1 local current_secret = utils.get_root_secret() + 1 if current_secret == nil then +****0 error("Can't get root password") + end + 1 if current_secret ~= secret then + 1 utils.set_root_secret(secret) + end + 1 elseif policy == "RANDOM" then + --! Not having a password can be specified by the secret being empty + --! or also being '*' or '!'. So we asume there is no password set + --! in both cases. + 1 if #utils.get_root_secret() <= 1 then + 1 utils.set_password('root', utils.random_string(30)) + end + else +****0 error('Invalid root_password_policy: ' .. policy) + end + end + + 18 function system.clean() + -- nothing to clean + end + + 18 function system.configure() + 1 utils.log("Configuring system...") + 1 system.set_hostname() + + 1 system.setup_root_password() + + 1 utils.log("Let uhttpd listen on IPv4/IPv6") + 1 local uci = config.get_uci_cursor() + 1 uci:set("uhttpd", "main", "listen_http", "80") + 1 uci:set("uhttpd", "main", "listen_https", "443") + 1 uci:set("uhttpd", "main", "max_requests", "6") + 1 uci:set("uhttpd", "main", "script_timeout", "15") + 1 uci:save("uhttpd") + end + + 18 function system.apply() + -- apply hostname + local hostname +****0 local uci = config.get_uci_cursor() +****0 uci:foreach("system", "system", function(s) +****0 hostname = uci:get("system", s[".name"], "hostname") -- FIXME Doesn't we already have hostaname in s["hostname"] without executing the get ? + end) +****0 fs.writefile("/proc/sys/kernel/hostname", hostname) + + -- apply uhttpd settings +****0 os.execute("/etc/init.d/uhttpd reload") + end + + 9 return system + +============================================================================== +packages/lime-system/files/usr/lib/lua/lime/utils.lua +============================================================================== + #!/usr/bin/lua + + --! LibreMesh community mesh networks meta-firmware + --! + --! Copyright (C) 2014-2023 Gioacchino Mazzurco + --! Copyright (C) 2023 Asociación Civil Altermundi + --! + --! SPDX-License-Identifier: AGPL-3.0-only + + 46 utils = {} + + 46 local config = require("lime.config") + 46 local json = require("luci.jsonc") + 46 local fs = require("nixio.fs") + 46 local nixio = require("nixio") + + 46 utils.BOARD_JSON_PATH = "/etc/board.json" + 46 utils.SHADOW_FILENAME = "/etc/shadow" + 46 utils.KEEP_ON_UPGRADE_FILES_BASE_PATH = '/lib/upgrade/keep.d/' + + 92 function utils.dbg(...) + 4 local ofd = io.stderr + + 8 ofd:write( debug.getinfo(2, 'S').source, ":", + 4 debug.getinfo(2, 'l').currentline, " ", + 4 debug.getinfo(2, 'n').name ) + + 10 for n=1, select('#', ...) do + --! Assigantion needed to take only the Nth element discarding the rest + 6 local nE = select(n, ...) + 6 ofd:write(" ", nE) + end + + 4 ofd:write("\n") + end + + 92 function utils.log(...) + 117 if DISABLE_LOGGING ~= nil then return end + 117 if os.getenv("LUA_DISABLE_LOGGING") ~= nil and os.getenv("LUA_ENABLE_LOGGING") == nil then return end + 80 print(string.format(...)) + end + + 92 function utils.disable_logging() +****0 DISABLE_LOGGING = 1 + end + + 92 function utils.enable_logging() +****0 DISABLE_LOGGING = nil + end + + 92 function utils.split(string, sep) + 410 local ret = {} + 1093 for token in string.gmatch(string, "[^"..sep.."]+") do table.insert(ret, token) end + 410 return ret + end + + 92 function utils.stringStarts(string, start) + 26 return (string.sub(string, 1, string.len(start)) == start) + end + + 92 function utils.stringEnds(string, _end) + 14 return ( _end == '' or string.sub( string, -string.len(_end) ) == _end) + end + + 92 function utils.hex(x) + 12 return string.format("%02x", x) + end + + 92 function utils.printf(fmt, ...) +****0 print(string.format(fmt, ...)) + end + + --! escape the magic characters: ( ) . % + - * ? [ ] ^ $ + --! useful to use with gsub / match when finding exactly a string + 92 function utils.literalize(str) + 122 local ret, _ = str:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", function(c) return "%" .. c end) + 55 return ret + end + + 92 function utils.isModuleAvailable(name) + 149 if package.loaded[name] then + 93 return true + else + 276 for _, searcher in ipairs(package.searchers or package.loaders) do + 239 local loader = searcher(name) + 239 if type(loader) == 'function' then + 19 package.preload[name] = loader + 19 return true + end + end + 37 return false + end + end + + 92 function utils.applyMacTemplate16(template, mac) + 287 for i=1,6,1 do template = template:gsub("%%M"..i, mac[i]) end + 41 local macid = utils.get_id(mac) + 287 for i=1,6,1 do template = template:gsub("%%m"..i, macid[i]) end + 41 return template + end + + 92 function utils.applyMacTemplate10(template, mac) + 119 for i=1,6,1 do template = template:gsub("%%M"..i, tonumber(mac[i], 16)) end + 17 local macid = utils.get_id(mac) + 119 for i=1,6,1 do template = template:gsub("%%m"..i, tonumber(macid[i], 16)) end + 17 return template + end + + 92 function utils.applyHostnameTemplate(template) + 17 local system = require("lime.system") + 17 return template:gsub("%%H", system.get_hostname()) + end + + 92 function utils.get_id(input) + 134 if type(input) == "table" then + 58 input = table.concat(input, "") + end + 134 local id = {} + 134 local fd = io.popen('echo "' .. input .. '" | md5sum') + 134 if fd then + 134 local md5 = fd:read("*a") + 134 local j = 1 + 2278 for i=1,16,1 do + 2144 id[i] = string.sub(md5, j, j + 1) + 2144 j = j + 2 + end + 134 fd:close() + end + 134 return id + end + + 92 function utils.network_id() + 29 local network_essid = config.get("wifi", "ap_ssid") + 29 return utils.get_id(network_essid) + end + + 92 function utils.applyNetTemplate16(template) + 12 local netid = utils.network_id() + 84 for i=1,6,1 do template = template:gsub("%%N"..i, netid[i]) end + 12 return template + end + + 92 function utils.applyNetTemplate10(template) + 17 local netid = utils.network_id() + 119 for i=1,6,1 do template = template:gsub("%%N"..i, tonumber(netid[i], 16)) end + 17 return template + end + + + --! This function is inspired to http://lua-users.org/wiki/VarExpand + --! version: 0.0.1 + --! code: Ketmar // Avalon Group + --! licence: public domain + --! expand $var and ${var} in string + --! ${var} can call Lua functions: ${string.rep(' ', 10)} + --! `$' can be screened with `\' + --! `...': args for $ + --! if `...' is just a one table -- take it as args + 92 function utils.expandVars(s, ...) +****0 local args = {...} + args = #args == 1 and type(args[1]) == "table" and args[1] or args; + + --! return true if there was an expansion + local function DoExpand(iscode) +****0 local was = false +****0 local mask = iscode and "()%$(%b{})" or "()%$([%a%d_]*)" +****0 local drepl = iscode and "\\$" or "\\\\$" + s = s:gsub(mask, + function(pos, code) +****0 if s:sub(pos-1, pos-1) == "\\" then +****0 return "$"..code + else +****0 was = true + local v, err +****0 if iscode then +****0 code = code:sub(2, -2) + else +****0 local n = tonumber(code) +****0 if n then +****0 v = args[n] + else +****0 v = args[code] + end + end +****0 if not v then +****0 v, err = loadstring("return "..code) +****0 if not v then error(err) end +****0 v = v() + end +****0 if v == nil then v = "" end +****0 v = tostring(v):gsub("%$", drepl) +****0 return v + end + end) +****0 if not (iscode or was) then s = s:gsub("\\%$", "$") end +****0 return was + end +****0 repeat DoExpand(true); until not DoExpand(false) +****0 return s + end + + --! return a string that can be a part of an url + 92 function utils.slugify(s) + 10 s = s:gsub('[^-a-zA-Z0-9]', '-') + 10 return s + end + + 92 function utils.hostname() + 59 return io.input("/proc/sys/kernel/hostname"):read("*line") + end + + 92 function utils.sanitize_hostname(hostname) + 7 hostname = hostname:gsub(' ', '-') + 7 hostname = hostname:gsub('[^-a-zA-Z0-9]', '') + 7 hostname = hostname:gsub('^-*', '') + 7 hostname = hostname:gsub('-*$', '') + 7 hostname = hostname:sub(1, 32) + 7 return hostname + end + + --! validate that a hostname is also DNS valid + 92 function utils.is_valid_hostname(hostname) + 15 if hostname and (#hostname < 64) and + 15 hostname:match("^[a-zA-Z0-9][a-zA-Z0-9%-]*[a-zA-Z0-9]$") + then + 4 return true + end + 11 return false + end + + 92 function utils.file_exists(name) + 55 local f=io.open(name,"r") + 55 if f~=nil then io.close(f) return true else return false end + end + + 92 function utils.read_file(name) + 19 local f = io.open(name,"r") + 19 local ret = nil + 19 if f ~= nil then + 16 ret = f:read("*all") + 16 f:close() + end + 19 return ret + end + + 92 function utils.write_file(name, content) + 33 local f = io.open(name, "w") + 33 local ret = false + 33 if f ~= nil then + 32 f:write(content) + 32 f:close() + 32 ret = true + end + 33 return ret + end + + 92 function utils.is_installed(pkg) + 11 return utils.file_exists('/usr/lib/opkg/info/'..pkg..'.control') + end + + 92 function utils.has_value(tab, val) + 39 for index, value in ipairs(tab) do + 32 if value == val then + 20 return true + end + end + 7 return false + end + + --! contact array a2 to the end of array a1 + 92 function utils.arrayConcat(a1,a2) +****0 for _,i in ipairs(a2) do +****0 table.insert(a1,i) + end +****0 return a1 + end + + --! melt table t1 into t2, if keys exists in both tables use value of t2 + 92 function utils.tableMelt(t1, t2) + 198 for key, value in pairs(t2) do + 153 t1[key] = value + end + 45 return t1 + end + + 92 function utils.tableLength(t) + 35 local count = 0 + 75 for _ in pairs(t) do count = count + 1 end + 35 return count + end + + 92 function utils.indexFromName(name) + 36 return tonumber(name:match("%d+")) + end + + 92 function utils.getBoardAsTable(board_path) + 1 if board_path == nil then +****0 board_path = utils.BOARD_JSON_PATH + end + 1 return json.parse(fs.readfile(board_path)) + end + + 92 function utils.current_board() + 1 return utils.read_file("/tmp/sysinfo/board_name"):gsub("\n","") + end + + 92 function utils.printJson(obj) + 7 print(json.stringify(obj)) + end + + --! use rpcd_readline() in libexec/rpcd/ scripts to access the arguments that + --! are passed through stdin. The use of this functions allows testing. + 92 function utils.rpcd_readline() +****0 return io.read() + end + + --! for testing only + 46 utils._uptime_line = nil + + 92 function utils.uptime_s() + 6 local uptime_line = utils._uptime_line or io.open("/proc/uptime"):read("*l") + 6 return tonumber(string.match(uptime_line, "^%S+")) + end + + --! Escape strings for safe shell usage. + 92 function utils.shell_quote(s) + --! Based on Python's shlex.quote() + 8 return "'" .. string.gsub(s, "'", "'\"'\"'") .. "'" + end + + --! Excutes a shell command, waits for completion and returns stdout. + --! Warning! Use this function carefully as it could be exploited if used with + --! untrusted input. Always use function utils.shell_quote() to escape untrusted + --! input. + 92 function utils.unsafe_shell(command) + 24 local handle = io.popen(command) + 24 local result = handle:read("*a") + 24 handle:close() + 24 return result + end + + --! based on luci.sys.setpassword + 92 function utils.set_password(username, password) + 1 local user = utils.shell_quote(username) + 1 local pass = utils.shell_quote(password) + 2 return os.execute(string.format("(echo %s; sleep 1; echo %s) | passwd %s >/dev/null 2>&1", + 1 pass, pass, user)) + end + + 92 function utils.get_root_secret() + 4 local f = io.open(utils.SHADOW_FILENAME, "r") + 4 if f ~= nil then + 3 local root_line = f:read("*l") --! root user is always in the first line + 3 local secret = root_line:match("root:(.-):") + 3 return secret + end + end + + 92 function utils.set_root_secret(secret) + 2 local f = io.open(utils.SHADOW_FILENAME, "r") + 2 if f ~= nil then + --! perform a backup of the shadow + 2 local f_bkp = io.open(utils.SHADOW_FILENAME .. "-", "w") + 2 f_bkp:write(f:read("*a")) + 2 f:seek("set") + 2 f_bkp:close() + + 2 local root_line = f:read("*l") --! root user is always in the first line + 2 local starts, ends = string.find(root_line, "root:.-:") + 2 local content = "root:" .. secret .. root_line:sub(ends) .. "\n" + 2 content = content .. f:read("*a") + 2 f:close() + 2 f = io.open(utils.SHADOW_FILENAME, "w") + 2 f:write(content) + 2 f:close() + end + end + + 92 function utils.set_shared_root_password(password) + 3 local uci = config.get_uci_cursor() + 3 utils.set_password('root', password) -- this takes 1 second, it may be replaced with nixio.crypt(password, '$1$vv44cu1H') + 3 uci:set("lime-community", 'system', 'root_password_policy', 'SET_SECRET') + 3 uci:set("lime-community", 'system', 'root_password_secret', utils.get_root_secret()) + end + + --! returns a random string. filter is an optional function to reduce the possible characters. + --! by default the filter allows all the alphanumeric characters + 92 function utils.random_string(length, filter) + 67 if filter == nil then + --! all alphanumeric characters + 1315 filter = function (c) return string.match(c, "%w") ~= nil end + end + 67 local urandom = io.open("/dev/urandom", "rb") + 67 local out = "" + 2796 while length > #out do + 2729 local c = urandom:read(1) + 2729 if filter(c) then + 422 out = out .. c + end + end + 67 return out + end + + --! Return a list of files to keep when upgrading. Existence of the files is not guaranteed. + 92 function utils.keep_on_upgrade_files() + 1 local file_lists = config.get("system", "keep_on_upgrade", "") + 1 local files = {} + 4 for _, list in pairs(utils.split(file_lists, " ")) do + --! convert non absolute paths to absolute + 3 if not utils.stringStarts(list, "/") then + 2 list = utils.KEEP_ON_UPGRADE_FILES_BASE_PATH .. list + end + 3 if utils.file_exists(list) then + 7 for file_name in io.lines(list) do + --! exclude comments, blank lines, etc + 5 if utils.stringStarts(file_name, "/") then + 3 table.insert(files, file_name) + end + end + end + end + 1 return files + end + + --! Run a command in a shell and daemonized directly in pid 1 (similar to diswon/nohup) + --! If out_file optional arg is passed then stdout and stderr will go to that file. + 92 function utils.execute_daemonized(cmd, out_file, stdin) + 3 if out_file == nil then + 1 out_file = "/dev/null" + end + 3 if not stdin then + 1 stdin = "0<&-" -- closing standard input + else + 2 stdin = "0<" .. stdin + end + + 3 return os.execute("(( " .. cmd .. " " .. stdin .. " &> " .. out_file .. " &) &)") + end + + 92 function utils.bitwise_xor(a, b) + 6 local r = 0 + 198 for i = 0, 31 do + 192 local x = a / 2 + b / 2 + 192 if x ~= math.floor(x) then + 19 r = r + 2^i + end + 192 a = math.floor(a / 2) + 192 b = math.floor(b / 2) + end + 6 return r + end + + 92 function utils.mac2ipv6linklocal(text) + 6 function f(mac0, mac1, mac2, mac3, mac4, mac5, mac6) + 6 local mac0 = string.format("%x", utils.bitwise_xor(tonumber(mac0, 16), 0x02)) + --! going from and to string to remove leading zeroes and convert to lowercase + 6 local gr1 = string.format("%x", tonumber(mac0 .. mac1, 16)) + 6 local gr2 = string.format("%x", tonumber(mac2 .. 'ff', 16)) + 6 local gr3 = string.format("%x", tonumber('fe' .. mac3, 16)) + 6 local gr4 = string.format("%x", tonumber(mac4 .. mac5, 16)) + 6 return 'fe80::' .. gr1 .. ":" .. gr2 .. ":" .. gr3 .. ":" .. gr4 + end + 6 local ret, _ = string.gsub(text, "(%x%x):(%x%x):(%x%x):(%x%x):(%x%x):(%x%x)", f) + 6 return ret + end + + --! do a HTTP GET returning the body or nil. If out_file is provided then the body is saved + --! to this file and true is returned instead + 92 function utils.http_client_get(url, timeout_s, out_file) + 4 local remove_file = false + 4 if not out_file then + 2 remove_file = true + 2 out_file = os.tmpname() + end + 8 local cmd = string.format("uclient-fetch -q -O %s --timeout=%d %s 2> /dev/null", out_file, + 4 timeout_s, url) + 4 local exit_value = os.execute(cmd) + 4 if exit_value == 0 then +****0 if remove_file then +****0 local data = utils.read_file(out_file) +****0 os.execute("rm -f " .. out_file) +****0 return data + else +****0 return true + end + else + 4 return nil + end + end + + 92 function utils.release_info() + 4 local result = {} + 4 local release_data = utils.read_file("/etc/openwrt_release") + 25 for key, value in release_data:gmatch("(.-)='(%C-)'\n") do + 21 result[key] = value + end + 3 return result + end + + 92 function utils.open_with_lock(fname, max_wait_s) + 70 max_wait_s = max_wait_s or 1 + 70 local locked = false + + 70 local fd = nixio.open(fname, nixio.open_flags("rdwr", "creat") ) + 70 if not fd then +****0 return nil, "Can't create file" + end + + 70 for i=0,max_wait_s do + 70 if not fd:lock("tlock") then +****0 nixio.nanosleep(1) + else + 70 locked = true + 70 break + end + end + + 70 if not locked then +****0 if fd then fd:close() end +****0 return nil, "Failed acquiring lock" + end + + 70 return fd + end + + --! Object store database. Uses json as on disk format. Uses posix file locks to prevent corruption, + --! be aware that this mechanism does only works between processes and not beteen threads or same thread. + 92 function utils.read_obj_store(datafile) + 34 local fd = utils.open_with_lock(datafile) + local store + 34 if fd then + 34 store = json.parse(nixio.fs.readfile(datafile)) or {} + 34 fd:close() + end + 34 return store + end + + 92 function utils.write_obj_store_var(datafile, name, data) + 13 local fd = utils.open_with_lock(datafile) + local store + 13 if fd then + 13 store = json.parse(nixio.fs.readfile(datafile)) or {} + 13 fd:seek(0) + 13 store[name] = data + 13 fd:write(json.stringify(store)) + 13 fd:close() + end + 13 return store + end + + 92 function utils.write_obj_store(datafile, data) + 21 local fd = utils.open_with_lock(datafile) + 21 if fd then + 21 fd:write(json.stringify(data)) + 21 fd:close() + 21 return true + end + end + + 92 function utils.get_ifnames() +****0 local ifnames = {} +****0 for ifname in fs.dir("/sys/class/net/") do +****0 table.insert(ifnames, ifname) + end +****0 return ifnames + end + + 92 function utils.is_valid_mac(string) + 3 local string = string:match("%w%w:%w%w:%w%w:%w%w:%w%w:%w%w") + 3 if string then + 3 return true + else +****0 return false + end + end + + --! TODO: Better having a C strcmp/memcmp like behavior so the output can be + --! used for sorting beyond determining equality + 92 function utils.deepcompare(t1,t2) + 1993 if t1 == t2 then return true end + 397 local ty1 = type(t1) + 397 local ty2 = type(t2) + 397 if ty1 ~= ty2 then return false end + 397 if ty1 ~= 'table' and ty2 ~= 'table' then return t1 == t2 end + 1376 for k1, v1 in pairs(t1) do + 993 local v2 = t2[k1] + 993 if v2 == nil or not utils.deepcompare(v1, v2) then return false end + end + 1366 for k2, v2 in pairs(t2) do + 983 local v1 = t1[k2] + 983 if v1 == nil or not utils.deepcompare(v1, v2) then return false end + end + 383 return true + end + + 92 function utils.is_dsa(port) + --! Code adapted from Jow https://forum.openwrt.org/t/how-to-detect-dsa/111868/4 + 88 port = port or "*" + 88 return 0 == os.execute("grep -sq DEVTYPE=dsa /sys/class/net/"..port.."/uevent") + end + + 92 function utils.dumptable(table, nesting) +****0 local nesting = nesting or 1 +****0 if type(table) ~= "table" then +****0 print("dumptable: first argument is expected to be a table but you passed a", type(table), table) + else +****0 if next(table) == nil then +****0 print(table, "empty") + else +****0 for k,v in pairs(table) do +****0 print(string.rep('\t', nesting), k, ' = ', v) +****0 if type(v) == 'table' then dumptable(v, nesting+1) end + end + end + end + end + + 46 return utils + +============================================================================== +packages/lime-system/files/usr/lib/lua/lime/wireless.lua +============================================================================== + #!/usr/bin/lua + + 16 local config = require("lime.config") + 16 local network = require("lime.network") + 16 local utils = require("lime.utils") + 16 local fs = require("nixio.fs") + 16 local iwinfo = require("iwinfo") + + 16 wireless = {} + + 16 wireless.limeIfNamePrefix="lm_" + + 32 function wireless.WIFI_MODE_SEPARATOR() + 32 return "-" + end + + 32 function wireless.get_phy_mac(phy) +****0 local path = "/sys/class/ieee80211/"..phy.."/macaddress" +****0 local mac = assert(fs.readfile(path), "wireless.get_phy_mac(..) failed reading: "..path):gsub("\n","") +****0 return utils.split(mac, ":") + end + + 32 function wireless.clean() + 1 utils.log("Clearing wireless config...") + 1 local uci = config.get_uci_cursor() + 4 uci:foreach("wireless", "wifi-iface", function(s) uci:delete("wireless", s[".name"]) end) + 1 uci:save("wireless") + end + + 32 function wireless.scandevices() + 13 local devices = {} + 13 local uci = config.get_uci_cursor() + 30 uci:foreach("wireless", "wifi-device", function(dev) devices[dev[".name"]] = dev end) + + 13 local sorted_devices = {} + 30 for _, dev in pairs(devices) do + 17 table.insert(sorted_devices, utils.indexFromName(dev[".name"])+1, dev) + end + + 13 local band_2ghz_index = 0 + 13 local band_5ghz_index = 0 + + 30 for _, dev in pairs(sorted_devices) do + 17 if wireless.is5Ghz(dev[".name"]) then + 15 dev.per_band_index = band_5ghz_index + 15 band_5ghz_index = band_5ghz_index + 1 + else + 2 dev.per_band_index = band_2ghz_index + 2 band_2ghz_index = band_2ghz_index + 1 + end + end + 13 return devices + end + + 32 function wireless.is5Ghz(radio) + 31 local config = require("lime.config") + 31 local uci = config.get_uci_cursor() + 31 wifi_band = uci:get('wireless', radio, 'band') + 31 if wifi_band then return wifi_band=='5g' end + 5 wifi_channel = uci:get('wireless', radio, 'channel') + 5 if tonumber(wifi_channel) then +****0 wifi_channel = tonumber(wifi_channel) +****0 return 32<=wifi_channel and wifi_channel<178 + end + 5 local backend = iwinfo.type(radio) + 5 local devModes = backend and iwinfo[backend].hwmodelist(radio) + 5 return devModes and (devModes.a or devModes.ac) + end + + 16 wireless.availableModes = { adhoc=true, ap=true, apname=true, apbb=true, ieee80211s=true } + 32 function wireless.isMode(m) + 84 return wireless.availableModes[m] + end + + 32 function wireless.is_mesh(iface) + 7 return iface.mode == 'mesh' or iface.mode == 'adhoc' + end + + 32 function wireless.mesh_ifaces() + 5 local uci = config.get_uci_cursor() + 5 local ifaces = {} + + 10 uci:foreach("wireless", "wifi-iface", function(entry) + 6 if entry.disabled ~= '1' and wireless.is_mesh(entry) then + 4 table.insert(ifaces, entry.ifname) + end + end) + --add apup interfaces + --this are not listed in uci + 5 local shell_output = utils.unsafe_shell("ls /sys/class/net/ -R") + 5 if shell_output ~= nil then + 32 for line in shell_output:gmatch("[^\n]+") do + -- Check if the line contains the pattern 'wlanX-peerY' + 27 local apup = require("lime.mode.apup") + 27 local iface = line:match("wlan(%d+)-"..apup.PEER_SUFFIX().."(%d+)$") + 27 if iface then + -- Add the matched interface to the table +****0 table.insert(ifaces, line) + end + end + end + 5 return ifaces + end + + 32 function wireless.get_radio_ifaces(radio) + 7 local uci = config.get_uci_cursor() + 7 local ifaces = {} + + 14 uci:foreach("wireless", "wifi-iface", function(entry) + 3 if entry.disabled ~= '1' and entry.device == radio then + 3 table.insert(ifaces, entry) + end + end) + 7 return ifaces + end + + 32 function wireless.calcIfname(radioName, mode, nameSuffix) + 15 local phyIndex = tostring(utils.indexFromName(radioName)) + 15 return "wlan"..phyIndex..wireless.WIFI_MODE_SEPARATOR()..mode..nameSuffix + end + + 32 function wireless.createBaseWirelessIface(radio, mode, nameSuffix, extras) + --! checks("table", "string", "?string", "?table") + --! checks(...) come from http://lua-users.org/wiki/LuaTypeChecking -> https://github.com/fab13n/checks + 15 nameSuffix = nameSuffix or "" + 15 local radioName = radio[".name"] + 15 local ifname = wireless.calcIfname(radioName, mode, nameSuffix) + --! sanitize generated ifname for constructing uci section name + --! because only alphanumeric and underscores are allowed + 15 local wirelessInterfaceName = wireless.limeIfNamePrefix..ifname:gsub("[^%w_]", "_").."_"..radioName + 15 local networkInterfaceName = network.limeIfNamePrefix..ifname:gsub("[^%w_]", "_") + + 15 local uci = config.get_uci_cursor() + + 15 uci:set("wireless", wirelessInterfaceName, "wifi-iface") + 15 uci:set("wireless", wirelessInterfaceName, "mode", mode) + 15 uci:set("wireless", wirelessInterfaceName, "device", radioName) + 15 uci:set("wireless", wirelessInterfaceName, "ifname", ifname) + 15 uci:set("wireless", wirelessInterfaceName, "network", networkInterfaceName) + + 15 if extras then + 48 for key, value in pairs(extras) do + 33 uci:set("wireless", wirelessInterfaceName, key, value) + end + end + + 15 uci:save("wireless") + + 15 return uci:get_all("wireless", wirelessInterfaceName) + end + + 32 function wireless.resolve_ssid(ssid) + 17 local result = utils.applyHostnameTemplate(ssid) + 17 result = utils.applyMacTemplate16(result, network.primary_mac()) + 17 result = string.sub(result, 1, 32) + 17 return result + end + + 32 function wireless.configure() + 6 local specificRadios = {} + 12 config.foreach("wifi", function(radio) + 2 specificRadios[radio[".name"]] = radio + end) + + 6 local allRadios = wireless.scandevices() + 15 for _,radio in pairs(allRadios) do + 9 local radioName = radio[".name"] + 9 local radioBand = wireless.is5Ghz(radioName) and '5ghz' or '2ghz' + 9 local radioOptions = specificRadios[radioName] or {} + 9 local bandOptions = config.get_all(radioBand) or {} + 9 local options = config.get_all("wifi") + + 9 options = utils.tableMelt(options, bandOptions) + 9 options = utils.tableMelt(options, radioOptions) + + --! If manual mode is used toghether with other modes it results in an + --! unpredictable behaviour + 9 if options["modes"][1] ~= "manual" then + --! fallback to "auto" in client mode + local channel + 9 if options["modes"][1] ~= "client" then + 8 channel = options["channel"] + 8 if type(channel) == "table" then + 4 channel = channel[1 + radio.per_band_index % #channel] + end + else + 1 channel = options["channel"] or "auto" + end + + 9 local uci = config.get_uci_cursor() + 9 uci:set("wireless", radioName, "disabled", 0) + 9 uci:set("wireless", radioName, "distance", options["distance"]) + 9 uci:set("wireless", radioName, "noscan", 1) + 9 uci:set("wireless", radioName, "channel", channel) + 9 if options["country"] then uci:set("wireless", radioName, "country", options["country"]) end + 9 if options["legacy_rates"] and not wireless.is5Ghz(radioName) then uci:set("wireless", radioName, "legacy_rates", options["legacy_rates"]) end + 9 if options["txpower"] then uci:set("wireless", radioName, "txpower", options["txpower"]) end + 9 if options["htmode"] then uci:set("wireless", radioName, "htmode", options["htmode"]) end + 9 uci:save("wireless") + + 24 for _,modeName in pairs(options["modes"]) do + 15 local args = {} + 15 local mode = require("lime.mode."..modeName) + + -- gather mode specific configs (eg ieee80211s_mcast_rate_5ghz) + 221 for key,value in pairs(options) do + 206 local keyPrefix = utils.split(key, "_")[1] + 206 local isGoodOption = ( (key ~= "modes") + 191 and (not key:match("^%.")) + 191 and (not key:match("channel")) + 144 and (not key:match("country")) + 130 and (not key:match("legacy_rates")) + 128 and (not key:match("txpower")) + 128 and (not key:match("htmode")) + 126 and (not key:match("distance")) + 117 and (not key:match("unstuck_interval")) + 102 and (not key:match("unstuck_timeout")) + 206 and (not (wireless.isMode(keyPrefix) and keyPrefix ~= modeName))) + 206 if isGoodOption then + 27 local nk = key:gsub("^"..modeName.."_", "") + 27 if nk == "ssid" then + 7 value = wireless.resolve_ssid(value) + end + 27 args[nk] = value + end + end + + 15 mode.setup_radio(radio, args) + end + end + end + end + + 32 function wireless.get_band_config(band) + 12 local general_cfg = config.get_all("wifi") or {} + 12 local band_cfg = config.get_all(band) or {} + 12 local result = general_cfg + 12 utils.tableMelt(result, band_cfg) + 12 return result + end + + 32 function wireless.get_community_band_config(band) + 5 local uci = config.get_uci_cursor() + 5 local default_general_cfg = uci:get_all(config.UCI_DEFAULTS_NAME, "wifi") or {} + 5 local default_band_cfg = uci:get_all(config.UCI_DEFAULTS_NAME, band) or {} + 5 local community_general_cfg = uci:get_all(config.UCI_COMMUNITY_NAME, "wifi") or {} + 5 local community_band_cfg = uci:get_all(config.UCI_COMMUNITY_NAME, band) or {} + 5 local result = default_general_cfg + 5 utils.tableMelt(result, default_band_cfg) + 5 utils.tableMelt(result, community_general_cfg) + 5 utils.tableMelt(result, community_band_cfg) + 5 return result + end + + 32 function wireless.add_band_mode(band, mode_name) + 1 local uci = config.get_uci_cursor() + 1 local cfg = wireless.get_band_config(band) + 1 if not utils.has_value(cfg.modes, mode_name) then + 1 local modes = uci:get(config.UCI_NODE_NAME, band, 'modes') + 1 if not modes or modes[1] == 'manual' then + 1 modes = { mode_name } + else +****0 table.insert(modes, mode_name) + end + 1 uci:set(config.UCI_NODE_NAME, band, 'lime-wifi-band') + 1 uci:set(config.UCI_NODE_NAME, band, 'modes', modes) + 1 uci:commit(config.UCI_NODE_NAME) + 1 utils.unsafe_shell('lime-config') + end + end + + 32 function wireless.remove_band_mode(band, mode_name) + 1 local uci = config.get_uci_cursor() + 1 local cfg = wireless.get_band_config(band) + 1 if utils.has_value(cfg.modes, mode_name) then + 1 local new_modes = {} + 3 for _, mode in pairs(cfg.modes) do + 2 if mode ~= mode_name then + 1 table.insert(new_modes, mode) + end + end + 1 if utils.tableLength(new_modes) == 0 then +****0 new_modes = {'manual'} + end + 1 uci:set(config.UCI_NODE_NAME, band, 'lime-wifi-band') + 1 uci:set(config.UCI_NODE_NAME, band, 'modes', new_modes) + 1 uci:commit(config.UCI_NODE_NAME) + 1 utils.unsafe_shell('lime-config') + end + end + + 32 function wireless.set_band_config(band, cfg) + 2 local uci = config.get_uci_cursor() + 2 uci:set(config.UCI_NODE_NAME, band, 'lime-wifi-band') + 5 for key, value in pairs(cfg) do + 3 uci:set(config.UCI_NODE_NAME, band, key, value) + end + 2 uci:commit(config.UCI_NODE_NAME) + 2 utils.unsafe_shell('lime-config') + end + + 16 return wireless + +============================================================================== +packages/pirania/files/usr/lib/lua/portal/portal.lua +============================================================================== + 5 local utils = require('lime.utils') + 5 local config = require('lime.config') + 5 local shared_state = require("shared-state") + 5 local read_for_access = require("read_for_access.read_for_access") + + 5 local portal = {} + + 5 portal.PAGE_CONTENT_OBJ_PATH = '/etc/pirania/portal.json' + + 5 function portal.get_config() + 10 local uci = config.get_uci_cursor() + 10 local activated = uci:get("pirania", "base_config", "enabled") == '1' + 10 local with_vouchers = uci:get("pirania", "base_config", "with_vouchers") == '1' + 10 return {activated = activated, with_vouchers = with_vouchers} + end + + 5 function portal.set_config(activated, with_vouchers) + 3 local uci = config.get_uci_cursor() + + 6 uci:set("pirania", "base_config", "with_vouchers", + 3 with_vouchers and "1" or "0") + 3 if activated then + 1 uci:set("pirania", "base_config", "enabled", "1") + 1 uci:commit("pirania") + 1 utils.unsafe_shell("captive-portal start") + else + 2 uci:set("pirania", "base_config", "enabled", "0") + 2 uci:commit("pirania") + 2 utils.unsafe_shell("captive-portal stop") + end + 3 return true + end + + 5 function portal.get_page_content() + 2 local db = shared_state.SharedStateMultiWriter:new('pirania_persistent'):get() + 2 if db.portal then + 1 return db.portal.data + else + 1 return utils.read_obj_store(portal.PAGE_CONTENT_OBJ_PATH) + end + end + + + 5 function portal.set_page_content(title, main_text, logo, link_title, link_url, background_color) + 1 local data = {title=title, main_text=main_text, logo=logo, link_title=link_title, link_url=link_url, background_color=background_color} + 1 local db = shared_state.SharedStateMultiWriter:new('pirania_persistent') + 1 return db:insert({portal=data}) + end + + 5 function portal.get_authorized_macs() +****0 local auth_macs = {} +****0 local with_vouchers = portal.get_config().with_vouchers +****0 if with_vouchers then +****0 local vouchera = require("voucher.vouchera") +****0 vouchera.init() +****0 auth_macs = vouchera.get_authorized_macs() + else +****0 auth_macs = read_for_access.get_authorized_macs() + end +****0 return auth_macs + end + + 5 function portal.update_captive_portal(daemonized) +****0 if daemonized then +****0 utils.execute_daemonized('captive-portal update') + else +****0 os.execute('captive-portal update') + end + end + + 5 return portal + +============================================================================== +packages/pirania/files/usr/lib/lua/read_for_access/cgi_handlers.lua +============================================================================== + 1 local utils = require('voucher.utils') + 1 local read_for_access = require('read_for_access.read_for_access') + 1 local portal = require('portal.portal') + 1 local config = require('lime.config') + + 1 local handlers = {} + + 1 function handlers.authorize_mac() + 4 local uci = config.get_uci_cursor() + 4 local with_vouchers = portal.get_config().with_vouchers + 4 if with_vouchers then + 1 return uci:get("pirania", "base_config", "url_auth") + end + 3 local client_data = utils.getIpv4AndMac(os.getenv('REMOTE_ADDR')) + 3 read_for_access.authorize_mac(client_data.mac) + 3 local params = utils.urldecode_params(os.getenv("QUERY_STRING")) + 3 local url_prev = utils.urldecode(params['prev']) + 3 local url_authenticated = uci:get("pirania", "base_config", "url_authenticated") + 3 return url_prev or url_authenticated + end + + 1 return handlers + +============================================================================== +packages/pirania/files/usr/lib/lua/read_for_access/read_for_access.lua +============================================================================== + 6 local fs = require('nixio.fs') + 6 local utils = require('lime.utils') + 6 local config = require('lime.config') + + 6 local read_for_access = {} + + 6 function uptime_s() + 7 return math.floor(utils.uptime_s()) + end + + 6 function read_for_access.set_workdir(workdir) + 9 if not utils.file_exists(workdir) then + 1 os.execute('mkdir -p ' .. workdir) + end + 9 if fs.stat(workdir, "type") ~= "dir" then +****0 error("Can't configure workdir " .. workdir) + end + 9 read_for_access.AUTH_MACS_FILE = workdir .. '/auth_macs' + end + + 6 read_for_access.set_workdir('/tmp/pirania/read_for_access') + + 6 function read_for_access.authorize_mac(mac) + 4 local uci = config.get_uci_cursor() + 4 local found = false + 4 if utils.file_exists(read_for_access.AUTH_MACS_FILE) then + 1 for line in io.lines(read_for_access.AUTH_MACS_FILE) do + 1 if line:match(mac) then + 1 found = true + 1 break + end + end + end + 4 local duration = uci:get("pirania", "read_for_access", "duration_m") + 4 local timestamp = uptime_s() + tonumber(duration) * 60 + 4 if not found then + 3 local ofile = io.open(read_for_access.AUTH_MACS_FILE, 'a') + 3 ofile:write(mac .. ' ' .. timestamp .. '\n') + 3 ofile:close() + else + 1 local content = utils.read_file(read_for_access.AUTH_MACS_FILE) + 1 content = content:gsub("(" .. mac .. ") %d+", "%1 " .. timestamp) + 1 utils.write_file(read_for_access.AUTH_MACS_FILE, content) + end + 4 os.execute('/usr/bin/captive-portal update') + end + + 6 function read_for_access.get_authorized_macs() + 3 local result = {} + 3 local current_time = uptime_s() + 3 if not utils.file_exists(read_for_access.AUTH_MACS_FILE) then +****0 return result + end + 6 for line in io.lines(read_for_access.AUTH_MACS_FILE) do + 3 local words = {} + 9 for w in line:gmatch("%S+") do table.insert(words, w) end + 3 if tonumber(words[2]) > current_time then + 2 table.insert(result, words[1]) + end + end + 3 return result + end + + 6 return read_for_access + +============================================================================== +packages/pirania/files/usr/lib/lua/voucher/cgi_handlers.lua +============================================================================== + 1 local vouchera = require('voucher.vouchera') + 1 local utils = require('voucher.utils') + + 1 handlers = {} + + 1 local TESTING_URL_INFO = '/info' + 1 local TESTING_URL_FAIL = '/fail' + 1 local TESTING_URL_AUTHENTICATED = '/authenticated' + + + 2 function handlers.preactivate_voucher() + 7 local uci_cursor = require('uci').cursor() + + --! using defaults for testing as uci environment is not available + 7 local url_authenticated = uci_cursor:get("pirania", "base_config", "url_authenticated") or TESTING_URL_AUTHENTICATED + 7 local url_fail = uci_cursor:get("pirania", "base_config", "url_fail") or TESTING_URL_FAIL + 7 local url_info = uci_cursor:get("pirania", "base_config", "url_info") or TESTING_URL_INFO + + 7 vouchera.init() + + local url + + 7 local client_data = utils.getIpv4AndMac(os.getenv('REMOTE_ADDR')) + 7 local client_is_authorized = vouchera.is_mac_authorized(client_data.mac) + + 7 if client_is_authorized then + 1 url = url_authenticated + else + local output + 6 local params = utils.urldecode_params(os.getenv("QUERY_STRING")) + 6 local code = params['voucher'] + 6 local prevUrl = params['prev'] + --! if client does not have javascript then activate right away without going to the INFO portal + 6 if params['nojs'] == 'true' then +****0 if vouchera.activate(code, client_data.mac) then +****0 url = url_authenticated + else +****0 url = url_fail + end + else + 6 local setParams = prevUrl and '?voucher=' .. code .. '&prev=' .. prevUrl or '?voucher=' .. code + --! redirect to the INFO portal for some seconds with the url params already set to activate + --! the voucher after this time + 6 if vouchera.is_activable(code) then + 2 url = url_info .. setParams + else + 4 url = url_fail .. (prevUrl and '?prev=' .. prevUrl or '') + end + end + end + 7 return url + end + + 2 function handlers.activate_voucher() + 3 local uci_cursor = require('uci').cursor() + + 3 local url_authenticated = uci_cursor:get("pirania", "base_config", "url_authenticated") or TESTING_URL_AUTHENTICATED + 3 local url_fail = uci_cursor:get("pirania", "base_config", "url_fail") or TESTING_URL_FAIL + + 3 vouchera.init() + 3 local client_data = utils.getIpv4AndMac(os.getenv('REMOTE_ADDR')) + 3 local client_is_authorized = vouchera.is_mac_authorized(client_data.mac) + + 3 local params = utils.urldecode_params(os.getenv("QUERY_STRING")) + 3 local code = params['voucher'] + 3 local prevUrl = params['prev'] + + 3 if client_is_authorized then + 1 return url_authenticated + end + + 2 local url = url_fail .. (prevUrl and '?prev=' .. prevUrl or '') + + 2 if code and client_data.mac then + 1 if vouchera.activate(code, client_data.mac) then + 1 if prevUrl ~= nil then + 1 url = prevUrl + else +****0 url = url_authenticated + end + end + end + 2 return url + end + + 1 return handlers + +============================================================================== +packages/pirania/files/usr/lib/lua/voucher/config.lua +============================================================================== + 3 local uci = require("uci") + 3 local pirania_config = 'pirania' + + 3 local ucicursor = uci.cursor() + + 3 local config = { + 3 db_path = ucicursor:get(pirania_config, 'base_config', 'db_path'), + 3 hooksDir = ucicursor:get(pirania_config, 'base_config', 'hooks_path'), + 3 prune_expired_for_days = ucicursor:get(pirania_config, 'base_config', 'prune_expired_for_days') + } + + 3 return config + +============================================================================== +packages/pirania/files/usr/lib/lua/voucher/hooks.lua +============================================================================== + #!/usr/bin/lua + + 3 local config = require('voucher.config') + 3 local fs = require("nixio.fs") + + 3 local hooks = {} + + + 3 function hooks.run(action) +****0 local hookPath = config.hooksDir..action..'/' +****0 local files = fs.dir(hookPath) +****0 if files then +****0 for file in files do +****0 os.execute("(( sh "..hookPath..file.." 0<&- &>/dev/null &) &)") + end + end + end + + 3 if debug.getinfo(2).name == nil then +****0 local arguments = { ... } +****0 if (arguments ~= nil and arguments[1] ~= nil) then +****0 hooks.run(arguments[1]) + end + end + + 3 return hooks + +============================================================================== +packages/pirania/files/usr/lib/lua/voucher/store.lua +============================================================================== + #!/bin/lua + + 3 local fs = require("nixio.fs") + 3 local json = require("luci.jsonc") + 3 local hooks = require('voucher.hooks') + 3 local utils = require("voucher.utils") + + 3 local store = {} + + 3 function store.load_db(db_path, voucher_init) + 61 local vouchers = {} + + 61 local f = io.open(db_path, "r") + 61 if f ~= nil then + 48 io.close(f) + else + 13 os.execute("mkdir -p " .. db_path) + end + + 81 for fname in fs.glob(db_path .. '/*.json') do + 20 local f = io.open(fname, "r") + 20 if f ~= nil then + 20 local json_obj = json.parse(f:read("*all")) + 20 f:close() + local voucher, err + 20 if json_obj then + 19 voucher, err = voucher_init(json_obj) + else + 1 err = "invalid json" + end + + 20 if voucher ~= nil then + 19 if vouchers[voucher.id] ~= nil then +****0 utils.log('warning', "vouchers: multiple vouchers with the same id " .. voucher.id) + end + 19 vouchers[voucher.id] = voucher + else + 1 utils.log('warning', "vouchers: Error loading voucher file " .. fname .. ", " .. err) + end + end + end + 61 return vouchers + end + + 3 function store.add_voucher(db_path, voucher, voucher_init) + 75 local fname = db_path .. "/" .. voucher.id .. ".json" + --! check if it already exists and if it is equal do not rewrite it + 75 local f = io.open(fname, "r") + 75 if f ~= nil then + 25 local json_obj = json.parse(f:read("*all")) + 25 f:close() + 25 local local_voucher = voucher_init(json_obj) + 25 if local_voucher == voucher then +****0 return false + end + end + 75 f = io.open(fname, "w") + 75 f:write(json.stringify(voucher)) + 75 f:close() + 75 return true + end + + 3 function store.remove_voucher(db_path, voucher) + 2 local fname = db_path .. "/" .. voucher.id .. ".json" + 2 local removed_db = io.open(db_path .. "/removed.txt", "a") + 2 if removed_db then + 2 removed_db:write(voucher.id .. ",") + 2 removed_db:close() + end + 2 local removed = os.execute("rm " .. fname) == 0 + 2 return removed + end + + 3 return store + +============================================================================== +packages/pirania/files/usr/lib/lua/voucher/utils.lua +============================================================================== + #!/bin/lua + 4 local nixio = require('nixio') + 4 local lhttp = require('lucihttp') + + 4 local utils = {} + + 4 function utils.log(...) + 1 nixio.syslog(...) + end + + local function checkIfIpv4(ip) + 7 if ip == nil or type(ip) ~= "string" then +****0 return 0 + end + -- check for format 1.11.111.111 for ipv4 + 7 local chunks = {ip:match("(%d+)%.(%d+)%.(%d+)%.(%d+)")} + 7 if (#chunks == 4) then + 35 for _,v in pairs(chunks) do + 28 if (tonumber(v) < 0 or tonumber(v) > 255) then +****0 return 0 + end + end + 7 return true + else +****0 return false + end + end + + --! get ipv4 and MAC from a ip_address that could be ipv4 or ipv6 + 4 function utils.getIpv4AndMac(ip_address) + 7 local isIpv4 = checkIfIpv4(ip_address) + 7 if (isIpv4) then + 7 local ipv4macCommand = "cat /proc/net/arp | grep "..ip_address.." | awk -F ' ' '{print $4}' | head -n 1" + 7 fd = io.popen(ipv4macCommand, 'r') + 7 ipv4mac = fd:read('*l') + 7 fd:close() + 7 local res = {} + 7 res.ip = ip_address + 7 res.mac = ipv4mac + 7 return res + else +****0 local ipv6macCommand = "ip neigh | grep "..ip_address.." | awk -F ' ' '{print $5}' | head -n 1" +****0 fd6 = io.popen(ipv6macCommand, 'r') +****0 ipv6mac = fd6:read('*l') +****0 fd6:close() +****0 local ipv4cCommand = "cat /proc/net/arp | grep "..ipv6mac.." | awk -F ' ' '{print $1}' | head -n 1" +****0 fd4 = io.popen(ipv4cCommand, 'r') +****0 ipv4 = fd4:read('*l') +****0 fd4:close() +****0 local res = {} +****0 res.ip = ipv4 +****0 res.mac = ipv6mac +****0 return res + end + end + + --! from given url or string. Returns a table with urldecoded values. + --! Simple parameters are stored as string values associated with the parameter + --! name within the table. Parameters with multiple values are stored as array + --! containing the corresponding values. + 4 function utils.urldecode_params(url, tbl) + local parser, name + 12 local params = tbl or { } + + 24 parser = lhttp.urlencoded_parser(function (what, buffer, length) + 44 if what == parser.TUPLE then + 16 name, value = nil, nil + 28 elseif what == parser.NAME then + 14 name = lhttp.urldecode(buffer) + 14 elseif what == parser.VALUE and name then + 14 params[name] = lhttp.urldecode(buffer) or "" + end + + 44 return true + end) + + 12 if parser then + 12 parser:parse((url or ""):match("[^?]*$")) + 12 parser:parse(nil) + end + + 12 return params + end + + 4 function utils.urlencode(value) + 7 if value ~= nil then + 7 local str = tostring(value) + 7 return lhttp.urlencode(str, lhttp.ENCODE_IF_NEEDED + lhttp.ENCODE_FULL) or str + end +****0 return nil + end + + 4 function utils.urldecode(value) + 3 if value ~= nil then + 1 local str = tostring(value) + 1 return lhttp.urldecode(str, lhttp.DECODE_IF_NEEDED) or str + end + 2 return nil + end + + 4 return utils + +============================================================================== +packages/pirania/files/usr/lib/lua/voucher/vouchera.lua +============================================================================== + #!/bin/lua + + 3 local store = require('voucher.store') + 3 local config = require('voucher.config') + 3 local utils = require('lime.utils') + 3 local portal = require('portal.portal') + 3 local hooks = require('voucher.hooks') + 3 local vouchera = {} + + 3 vouchera.ID_SIZE = 6 + 3 vouchera.CODE_SIZE = 6 + + --! Simplify the comparison of vouchers using a metatable for the == operator + 3 local voucher_metatable = { + __eq = function(self, value) + 34 return self.tostring() == value.tostring() + end + } + + --! obj attrs id, name, code, mac, duration_m, mod_counter, creation_date, activation_date + 3 function voucher_init(obj) + 106 local voucher = {} + + 106 if not obj.id then + 39 obj.id = utils.random_string(vouchera.ID_SIZE) + end + + 106 voucher.id = obj.id + 106 if type(obj.id) ~= "string" then +****0 return nil, "id must be a string" + end + + 106 if type(obj.name) ~= "string" then +****0 return nil, "name must be a string" + end + 106 voucher.name = obj.name + + 106 if type(obj.code) ~= "string" then +****0 return nil, "code must be a string" + end + 106 voucher.code = obj.code + + 106 if type(obj.mac) == "string" and #obj.mac ~= 17 then +****0 return nil, "invalid mac" + end + 106 voucher.mac = obj.mac + + + 106 if not (type(obj.duration_m) == "nil" or type(obj.duration_m) == "number") then +****0 return nil, "invalid duration_m type" + end + 106 voucher.duration_m = obj.duration_m -- use nil to create a permanent voucher + + 106 if not obj.creation_date then +****0 return nil, "creation_date can't be nil" + end + + 106 voucher.author_node = obj.author_node + + 106 voucher.creation_date = obj.creation_date + + 106 voucher.activation_date = obj.activation_date + + 106 if not (type(obj.activation_deadline) == "nil" or type(obj.activation_deadline) == "number") then +****0 return nil, "invalid activation_deadline type", type(obj.activation_deadline) + end + 106 voucher.activation_deadline = obj.activation_deadline + + 106 voucher.invalidation_date = obj.invalidation_date + + 106 voucher.mod_counter = obj.mod_counter or 1 + + --! tostring must reflect all the state of a voucher (so vouchers can be compared reliably using tostring) + voucher.tostring = function() + 68 local v = voucher + 68 local creation = os.date("%c", v.creation_date) + 68 local expiration = ' - ' + 68 if v.expiration_date() then + 19 expiration = os.date("%c", v.expiration_date()) + end + 136 return(string.format('%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s', v.id, v.name, v.code, v.mac or 'xx:xx:xx:xx:xx:xx', + 136 creation, v.duration_m or 'perm', expiration, v.mod_counter)) + end + + voucher.expiration_date = function() + local ret = nil + 193 if voucher.duration_m and voucher.mac and voucher.activation_date then + 77 ret = voucher.activation_date + voucher.duration_m * 60 + end + 193 return ret + end + + voucher.is_active = function() + 41 if voucher.is_invalidated() or voucher.mac == nil then + 28 return false + else + 13 if voucher.expiration_date() and voucher.expiration_date() <= os.time() then + 2 return false + end + end + 11 return true + end + + voucher.is_invalidated = function() + 77 return voucher.invalidation_date ~= nil + end + + voucher.is_expired = function() + 35 local curr_time = os.time() + 35 return (voucher.expiration_date() ~= nil and voucher.expiration_date() < curr_time) or + 35 (voucher.activation_deadline ~= nil and voucher.activation_deadline < curr_time) + end + + voucher.is_activable = function() + 22 return voucher.mac == nil and not voucher.is_invalidated() and not voucher.is_expired() + end + + voucher.status = function() + 15 local status = 'available' + 15 if voucher.is_invalidated() then + 1 status = 'invalidated' + 14 elseif voucher.is_expired() then + 1 status = 'expired' + 13 elseif voucher.is_active() then + 1 status = 'active' + end + 15 return status + end + + 106 setmetatable(voucher, voucher_metatable) + 106 return voucher + end + + 3 function vouchera.init(cfg) + 61 if cfg ~= nil then +****0 config = cfg + end + 61 vouchera.config = config + 61 vouchera.PRUNE_OLDER_THAN_S = tonumber(config.prune_expired_for_days) * 60 * 60 * 24 + 61 vouchera.vouchers = store.load_db(config.db_path, voucher_init) + + --! Automatic voucher pruning + 80 for _, voucher in pairs(vouchera.vouchers) do + 19 if vouchera.should_be_pruned(voucher) then + 1 vouchera.remove_locally(voucher.id) + end + end + end + + 3 function vouchera.add(obj) + 50 if not obj.creation_date then + 48 obj.creation_date = os.time() + end + 50 obj.author_node = utils.hostname() + 50 local voucher, errmsg = voucher_init(obj) + 50 if vouchera.vouchers[obj.id] ~= nil then +****0 return nil, "voucher with same id already exists" + end + 50 if voucher and store.add_voucher(config.db_path, voucher, voucher_init) then + 50 vouchera.vouchers[obj.id] = voucher + 50 return voucher + end +****0 return nil, "can't create voucher: " .. tostring(errmsg) + end + + 3 function vouchera.get_by_id(id) + 12 return vouchera.vouchers[id] + end + + 3 function vouchera.create(basename, qty, duration_m, activation_deadline) + 7 local vouchers = {} + 30 for n=1, qty do + local name + 23 if qty == 1 then + 1 name = basename + else + 22 name = basename .. "-" .. tostring(n) + end + 23 local v = {name=name, code=vouchera.gen_code(), duration_m=duration_m, + 23 activation_deadline=activation_deadline} + 23 local voucher, msg = vouchera.add(v) + 23 if voucher == nil then +****0 return nil, msg + end + 23 table.insert(vouchers, n, {id=voucher.id, code=voucher.code}) + end + 7 portal.update_captive_portal(true) + 7 hooks.run('db_change') + 7 return vouchers + end + + --! Remove a voucher from the local db. This won't trigger a remove in the shared db. + 3 function vouchera.remove_locally(id) + 3 if vouchera.vouchers[id] ~= nil then + 2 if store.remove_voucher(config.db_path, vouchera.vouchers[id]) then + 2 vouchera.vouchers[id] = nil + 2 return true + else +****0 return nil, "can't remove voucher" + end + end + 1 return nil, "can't find voucher to remove" + end + + local function modify_voucher_with_func(id, func) + 27 local voucher = vouchera.vouchers[id] + 27 if voucher then + 25 func(voucher) + 25 voucher.mod_counter = voucher.mod_counter + 1 + 25 return store.add_voucher(config.db_path, voucher, voucher_init) + end + 2 return voucher + end + + --! Remove a voucher from the shared db. + --! This will eventualy prune the voucher in all the dbs after PRUNE_OLDER_THAN_S seconds. + --! It is important to maintain the "removed" (invalidated) voucher in the shared db for some time + --! so that all nodes (even nodes that are offline when this is executed) have time to update locally + --! and eventualy prune the voucher. + 3 function vouchera.invalidate(id) + 9 local voucher = vouchera.vouchers[id] + 9 local is_active = voucher ~= nil and voucher.is_active() + local function _update(v) + 7 v.invalidation_date = os.time() + end + 9 voucher = modify_voucher_with_func(id, _update) + 9 if is_active then + 4 portal.update_captive_portal(true) + end + 9 hooks.run('db_change') + 9 return voucher + end + + --! Activate a voucher returning true or false depending on the status of the operation. + 3 function vouchera.activate(code, mac) + 18 local voucher = vouchera.is_activable(code) + 18 if voucher then + 15 function _update(v) + 15 v.mac = mac + 15 v.activation_date = os.time() + end + 15 modify_voucher_with_func(voucher.id, _update) + 15 portal.update_captive_portal(false) + 15 hooks.run('db_change') + end + 18 return voucher + end + + 3 function vouchera.deactivate(id) + local function _update(v) + 1 v.mac = nil + end + 1 return modify_voucher_with_func(id, _update) + end + + --! updates the database with the new voucher information + 3 function vouchera.update_with(voucher) +****0 vouchera.vouchers[voucher.id] = voucher +****0 return store.add_voucher(config.db_path, voucher, voucher_init) + end + + --! Return true if there is an activated voucher that grants a access to the specified MAC + 3 function vouchera.is_mac_authorized(mac) + 15 if mac ~= nil then + 11 for k, v in pairs(vouchera.vouchers) do + 7 if v.mac == mac and v.is_active() then + 4 return true + end + end + end + 11 return false + end + + --! Check if a code would be good to be activated but without activating it right away. + 3 function vouchera.is_activable(code) + 31 for _, v in pairs(vouchera.vouchers) do + 24 if v.code == code then + 22 if v.is_activable() then + 18 return v + else + 4 return false + end + end + end + 7 return false + end + + 3 function vouchera.should_be_pruned(voucher) + 27 local current_time = os.time() + 27 return (voucher.expiration_date() ~= nil and ( + 7 voucher.expiration_date() <= (current_time - vouchera.PRUNE_OLDER_THAN_S))) or + 27 ((voucher.invalidation_date or false) and (voucher.invalidation_date <= (current_time - vouchera.PRUNE_OLDER_THAN_S))) + end + + 3 function vouchera.rename(id, new_name) + local function _update(v) + 2 v.name = new_name + end + 2 return modify_voucher_with_func(id, _update) + end + + 3 function vouchera.gen_code() + 1408 return utils.random_string(vouchera.CODE_SIZE, function (c) return c:match('%u') ~= nil end) + end + + 3 function vouchera.list() + 2 local vouchers = {} + 12 for k, v in pairs(vouchera.vouchers) do + 20 table.insert(vouchers, { + 10 id=v.id, + 10 name=v.name, + 10 code=v.code, + 10 mac=v.mac, + 10 duration_m=v.duration_m, + 10 creation_date=v.creation_date, + 10 activation_date=v.activation_date, + 10 expiration_date=v.expiration_date(), + 10 is_active=v.is_active(), + 10 permanent=not v.duration_m, + 10 activation_deadline=v.activation_deadline, + 10 author_node=v.author_node, + 10 status=v.status(), + }) + end + 2 return vouchers + end + + 3 function vouchera.get_authorized_macs() +****0 local auth_macs = {} +****0 for _, voucher in pairs(vouchera.vouchers) do +****0 if voucher.is_active() then +****0 table.insert(auth_macs, voucher.mac) + end + end +****0 return auth_macs + end + + 3 vouchera.voucher = voucher_init + + 3 return vouchera + +============================================================================== +packages/pirania/files/usr/libexec/rpcd/pirania +============================================================================== + #!/usr/bin/env lua + --[[ + Copyright 2018 Marcos Gutierrez + Copyright 2021 Santiago Piccinini + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-3.0 + ]]-- + 11 local ubus = require "ubus" + 11 local json = require 'luci.jsonc' + 11 local uci = require 'uci' + 11 local vouchera = require('voucher.vouchera') + 11 local utils = require('lime.utils') + 11 local config = require('lime.config') + 11 local portal = require('portal.portal') + + 11 vouchera.init() + + 11 local uci_cursor = config.get_uci_cursor() + + 11 local conn = ubus.connect() + 11 if not conn then +****0 error("Failed to connect to ubus") + end + + local function get_portal_config() + 1 local portal_config = portal.get_config() + 1 portal_config.status = 'ok' + 1 return utils.printJson(portal_config) + end + + local function set_portal_config(msg) + 2 local status, error_msg = portal.set_config(msg.activated, msg.with_vouchers) + 2 if not status then + 1 utils.printJson({ status = "error", message = error_msg }) + else + 1 utils.printJson({ status = "ok" }) + end + end + + local function get_portal_page_content(msg) +****0 utils.printJson(portal.get_page_content()) + end + + local function set_portal_page_content(msg) + 2 portal.set_page_content( + 1 msg.title, + 1 msg.main_text, + 1 msg.logo, + 1 msg.link_title, + 1 msg.link_url, + msg.background_color + 1 ) + 1 utils.printJson({ status = 'ok'}) + end + + local function show_url(msg) +****0 utils.printJson({ status = 'ok', url = uci_cursor:get("pirania", "base_config", "portal_url") }); + end + + local function change_url(msg) +****0 local url = msg.url +****0 uci_cursor:set("pirania", "base_config", "portal_url", url) +****0 uci_cursor:commit("pirania") +****0 utils.printJson({status = 'ok', url = url}); + end + + + local function add_vouchers(msg) + 4 local vouchers, errmsg = vouchera.create(msg.name, msg.qty, msg.duration_m, + 2 msg.activation_deadline, msg.permanent) + 2 if vouchers then + 2 return utils.printJson({ status = 'ok', vouchers = vouchers}) + else +****0 return utils.printJson({ status = 'error', message = errmsg}) + end + end + + local function rename(msg) + 1 local voucher = vouchera.rename(msg.id, msg.name) + 1 return utils.printJson({ status = voucher and 'ok' or 'error' }) + end + + local function invalidate(msg) + 2 local voucher = vouchera.invalidate(msg.id) + 2 return utils.printJson({ status = voucher and 'ok' or 'error' }) + end + + + local function list_vouchers(msg) + 1 local vouchers = vouchera.list() + 1 return utils.printJson({ status = vouchers and 'ok' or 'error', vouchers = vouchers }) + end + + 11 local methods = { + 11 get_portal_config = { no_params = 0 }, + 11 set_portal_config = { activated = 'bool', with_vouchers = 'bool' }, + 11 disable = { no_params = 0 }, + 11 show_url = { no_params = 0 }, + 11 change_url = { url = 'value' }, + 11 add_vouchers = { name='str', qty='int', duration_m='int', activation_deadline='timestamp', permanent='bool'}, + 11 list_vouchers = { no_params = 0 }, + 11 rename = { id = 'str', name = 'str' }, + 11 invalidate = { id = 'str' }, + 11 get_portal_page_content = { no_params = 0 }, + 11 set_portal_page_content = { + 11 title = 'value', + 11 main_text = 'value', + 11 logo = 'value', + 11 link_title = 'value', + 11 link_url = 'value', + 11 background_color = 'value' + 11 }, + } + + 11 if arg[1] == 'list' then + 1 utils.printJson(methods) + end + + 11 if arg[1] == 'call' then + 10 local msg = utils.rpcd_readline() + 10 msg = json.parse(msg) + 10 if arg[2] == 'get_portal_config' then get_portal_config(msg) + 9 elseif arg[2] == 'set_portal_config' then set_portal_config(msg) + 7 elseif arg[2] == 'disable' then disable(msg) + 7 elseif arg[2] == 'show_url' then show_url(msg) + 7 elseif arg[2] == 'change_url' then change_url(msg) + 7 elseif arg[2] == 'list_vouchers' then list_vouchers(msg) + 6 elseif arg[2] == 'add_vouchers' then add_vouchers(msg) + 4 elseif arg[2] == 'invalidate' then invalidate(msg) + 2 elseif arg[2] == 'rename' then rename(msg) + 1 elseif arg[2] == 'get_portal_page_content' then get_portal_page_content(msg) + 1 elseif arg[2] == 'set_portal_page_content' then set_portal_page_content(msg) +****0 else utils.printJson({ error = "Method not found" }) + end + end + +============================================================================== +packages/pirania/files/www/pirania-redirect/redirect +============================================================================== + #!/usr/bin/lua + 2 local utils = require('voucher.utils') + 2 local portal = require('portal.portal') + 2 local config = require('lime.config') + + + 2 local uci_cursor = config.get_uci_cursor() + + 2 function handle_request (env) + 2 local method = 'http://' + 2 local origin_url = utils.urlencode(method .. env.HTTP_HOST .. env.REQUEST_URI) + 2 local portal_domain = uci_cursor:get("pirania", "base_config", "portal_domain") + 2 local with_vouchers = portal.get_config().with_vouchers + 2 local redirect_path = '' + 2 if with_vouchers then + 1 redirect_path = uci_cursor:get("pirania", "base_config", "url_auth") + else + 1 redirect_path = uci_cursor:get("pirania", "read_for_access", "url_portal") + end + 2 local redirect_url = method .. portal_domain .. redirect_path .. "?prev=" .. origin_url + + 2 uhttpd.send("Status: 302 \r\n") + 2 uhttpd.send("Location: " .. redirect_url .. "\r\n") + 2 uhttpd.send("\r\n") --! indicate uhttpd to send the response + end + +============================================================================== +packages/safe-upgrade/files/usr/sbin/safe-upgrade +============================================================================== + #!/usr/bin/lua + --[[ + Copyright (C) 2019-2020 Santiago Piccinini + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + ]]-- + + 1 local io = require "io" + 1 local json = require "luci.jsonc" + 1 local utils = require "lime.utils" + + 1 local su = {} + + 1 su.version = '1.0' + 1 local firmware_size_bytes = 7936*1024 + -- Keep the fw addresses as strings beacause of https://gitlab.com/librerouter/librerouteros/-/issues/61 + 1 local fw1_addr = '0x9f050000' + 1 local fw2_addr = '0x9f810000' -- fw1_addr + firmware_size_bytes + + 1 su._supported_devices = {'librerouter-v1'} + 1 su._mtd_partitions_desc = '/proc/mtd' + + -- safe upgrade script, generated with bootscript.py, DO NOT edit here! + 1 local bootcmd = 'run preboot; boot_part=${stable_part}; if test ${testing_part} -ne 0; then echo Testing part ${testing_part}; boot_part=${testing_part}; set testing_part 0; saveenv; fi; if test ${boot_part} -eq 2; then fw_addr=${fw2_addr}; run boot_2; else fw_addr=${fw1_addr}; run boot_1; fi; run boot_1; bootm ${fw1_addr};' + + 1 su.STABLE_PARTITION_NAME = 'stable_part' + 1 su.TESTING_PARTITION_NAME = 'testing_part' + + 1 su.EXIT_ERROR_OK = 0 + 1 su.EXIT_STATUS_INVALID_FIRMWARE = 111 + 1 su.EXIT_STATUS_ALREADY_CONFIRMED = 113 + 1 su.EXIT_STATUS_FW_ENV_NOT_FOUND = 115 + 1 su.EXIT_STATUS_BOARD_NOT_SUPPORTED = 198 + 1 su.EXIT_STATUS_NOT_INSTALLED = 199 + 1 su.EXIT_STATUS_INSTALL_FROM_PART2 = 120 + 1 su.EXIT_STATUS_ALREADY_INSTALLED = 121 + 1 su.REBOOT_FILE_CONFIG_TIMEOUT_S = "/etc/safe_upgrade_auto_reboot_confirm_timeout_s" + + local safe_upgrade_auto_reboot_script = [[#!/bin/sh /etc/rc.common + + REBOOT_FILE_CONFIG_TIMEOUT_S="/etc/safe_upgrade_auto_reboot_confirm_timeout_s" + MINIMUM_REBOOT_TIMEOUT_S=60 + PIDFILE="/var/run/safe_upgrade_auto_reboot_script.pid" + CMD_FORCE_REBOOT="echo b > /proc/sysrq-trigger" # Immediately reboot the system without syncing or unmounting disks. + + START=11 + + start() { + if [ -s "$REBOOT_FILE_CONFIG_TIMEOUT_S" ]; then + read reboot_at_uptime_s < "$REBOOT_FILE_CONFIG_TIMEOUT_S" + else + exit 0 + fi + + # check that the reboot time is above the minimum to prevent infinite reboots + if [ "$reboot_at_uptime_s" -lt "$MINIMUM_REBOOT_TIMEOUT_S" ]; then + echo "safe-upgrade reboot: Less than minimum timeout! aborting" + exit 0 + fi + + (sleep "$reboot_at_uptime_s" && \ + if [ -s "$REBOOT_FILE_CONFIG_TIMEOUT_S" ]; then + reboot ; sleep 10 ; eval "$CMD_FORCE_REBOOT" + fi + ) & + + echo $! > "$PIDFILE" + } + + stop() { + rm "$REBOOT_FILE_CONFIG_TIMEOUT_S" + sync + kill -9 `cat "$PIDFILE"` + } + 1 ]] + + 1 function su.get_uboot_env(key) +****0 local value = utils.unsafe_shell("fw_printenv -n " .. key .. " 2>&1") + +****0 if value:find('## Error:') == nil then + -- remove EOL +****0 local value = value:sub(1, -2) +****0 return value + else +****0 return nil + end + end + + local function set_uboot_env(key, value) +****0 print("DEBUG: setting key:" .. key) +****0 print("DEBUG: value:" .. value) +****0 utils.unsafe_shell("fw_setenv " .. key .. " '" .. value .. "'") + end + + local function fw_env_configured() +****0 return utils.file_exists('/etc/fw_env.config') + end + + local function assert_fw_env_configured() +****0 if not fw_env_configured() then +****0 print('/etc/fw_env.confg does not exist, aborting') +****0 os.exit(su.EXIT_STATUS_FW_ENV_NOT_FOUND) + end + end + + local function get_current_cmdline() +****0 return utils.read_file('/proc/cmdline') + end + + 1 function su.get_current_partition() + 2 local data = utils.read_file(su._mtd_partitions_desc) + 2 if data:find("fw2") == nil then + 1 return 2 + else + 1 return 1 + end + end + + 1 function su.get_partitions() + 2 local p = {} + 2 p.current = su.get_current_partition() + 2 if p.current == 1 then + 1 p.other = 2 + else + 1 p.other = 1 + end + 2 p.stable = tonumber(su.get_uboot_env(su.STABLE_PARTITION_NAME)) + 2 p.testing = tonumber(su.get_uboot_env(su.TESTING_PARTITION_NAME)) + + 2 return p + end + + local function get_su_version() +****0 return su.get_uboot_env('su_version') + end + + local function is_su_installed() +****0 return get_su_version() ~= nil + end + + local function set_testing_partition(partition) +****0 set_uboot_env(su.TESTING_PARTITION_NAME, tostring(partition)) + end + + local function set_stable_partition(partition) +****0 set_uboot_env(su.STABLE_PARTITION_NAME, tostring(partition)) + end + + local function assert_su_installed() +****0 if not is_su_installed() then +****0 print('safe-upgrade is not installed, aborting') +****0 os.exit(su.EXIT_STATUS_NOT_INSTALLED) + end + end + + local function read_fw_metadata(path) +****0 local handle = io.popen("fwtool -i - " .. path) +****0 local metadata = json.parse(handle:read("*a")) +****0 handle:close() +****0 return metadata + end + + 1 function su.get_current_device() +****0 return utils.read_file("/tmp/sysinfo/board_name"):gsub("\n","") + end + + 1 function su.get_supported_devices() + 8 return su._supported_devices + end + + 1 function su.is_current_board_supported() + 4 local current_device = su.get_current_device() + 9 for _, supported_device in pairs(su.get_supported_devices()) do + 7 if string.find(current_device, utils.literalize(supported_device)) then + 2 return true + end + end + 2 return false + end + + 1 function su.is_firmware_valid(metadata) + 12 local current_device = su.get_current_device() + 12 if metadata ~= nil then + 10 local fw_devices = metadata['supported_devices'] + 10 if fw_devices ~= nil then + 14 for _, fw_device in pairs(fw_devices) do + 14 for _, supported_device in pairs(su.get_supported_devices()) do + --! Check that the firmware is supported by safe upgrade + --! and that the current board is among the firmware devices + 8 if string.find(fw_device, utils.literalize(supported_device)) and + 4 string.find(fw_device, utils.literalize(current_device)) then + 2 return true + end + end + end + end + end + 10 return false + end + + 1 function su.preserve_files_to_new_partition(args) + 3 os.execute("rm -rf /tmp/_to_sysupgradetgz/") + 3 os.execute("mkdir -p /tmp/_to_sysupgradetgz/etc/init.d/") + 3 os.execute("mkdir -p /tmp/_to_sysupgradetgz/etc/rc.d/") + 3 local f = io.open("/tmp/_to_sysupgradetgz/etc/init.d/safe_upgrade_auto_reboot", "w") + 3 f:write(safe_upgrade_auto_reboot_script) + 3 f:close() + + 3 os.execute("chmod +x /tmp/_to_sysupgradetgz/etc/init.d/safe_upgrade_auto_reboot") + 3 os.execute("ln -s ../init.d/safe_upgrade_auto_reboot /tmp/_to_sysupgradetgz/etc/rc.d/S11safe_upgrade_auto_reboot") + + 3 if not args.disable_reboot_safety then + 3 local f = io.open("/tmp/_to_sysupgradetgz/etc/safe_upgrade_auto_reboot_confirm_timeout_s", "w") + 3 f:write(args.reboot_safety_timeout) + 3 f:close() + end + + 3 if args.do_not_preserve_config then + 1 utils.log('Not preserving config.') + 2 elseif args.preserve_archive then + 1 utils.log('Preserving from archive') + 1 os.execute("tar xfz " .. args.preserve_archive .. " -C /tmp/_to_sysupgradetgz/") + else + 1 utils.log('Preserving libremesh minimal config.') + 1 local files = "" + 3 for _, file_name in pairs(utils.keep_on_upgrade_files()) do + 2 if utils.file_exists(file_name) then + 1 files = files .. " " .. file_name + end + end + 1 if files ~= '' then + --! using an intermediate tar file for simplicity + 1 os.execute("tar cf /tmp/_safe_upgrade_intermadiate.tar " .. files .. " 2> /dev/null") + 1 os.execute("tar xf /tmp/_safe_upgrade_intermadiate.tar -C /tmp/_to_sysupgradetgz/") + end + end + + 3 os.execute('find /tmp/_to_sysupgradetgz/ -type f -o -type l | sed "s|^\/tmp\/_to_sysupgradetgz\/||" | sort -u > /tmp/_to_persist') + 3 os.execute("tar cfz /tmp/sysupgrade.tgz -C /tmp/_to_sysupgradetgz/ -T /tmp/_to_persist 2>/dev/null") + + 3 utils.log('List of files that are being preserved:') + 3 local file_list = {} + 3 local out = io.popen("tar tfz /tmp/sysupgrade.tgz") + 14 for line in out:lines() do + --! skip directories + 11 if not utils.stringEnds(line, '/') then + 11 table.insert(file_list, line) + 11 utils.log('\t' .. line) + end + end + 3 out:close() + 3 return file_list + end + + local function bootstrap(args) +****0 if is_su_installed() then +****0 if args.force then +****0 print("Forcing the bootstrap.") + else +****0 print(string.format("safe-upgrade version '%s' is already installed, aborting", +****0 get_su_version())) +****0 os.exit(su.EXIT_STATUS_ALREADY_INSTALLED) + end + end + + +****0 if su.get_current_partition() ~= 1 then +****0 print("installing safe-upgrade from partition 2 is not supported yet") +****0 os.exit(su.EXIT_STATUS_INSTALL_FROM_PART2) + end + +****0 set_stable_partition(1) +****0 set_testing_partition(0) +****0 set_uboot_env('fw1_addr', fw1_addr) +****0 set_uboot_env('fw2_addr', fw2_addr) + + -- configure cmdline using the current cmdline config to not force + -- us to know here the correct cmdline bootargs of the running kernel +****0 local boot_1 = 'set bootargs ' .. get_current_cmdline() .. '; echo booting part 1; bootm ${fw_addr};' +****0 set_uboot_env('boot_1', boot_1) +****0 set_uboot_env('su_version', su.version) + + -- installing the script. Everything must be installed before this! +****0 set_uboot_env('bootcmd', bootcmd) +****0 print('succesfully bootstraped safe-upgrade') + end + + local function _verify(firmware) +****0 local fw_metadata = read_fw_metadata(firmware) +****0 local fw_valid = fw_metadata ~= nil and su.is_firmware_valid(fw_metadata) +****0 return fw_valid + end + + local function verify(args) +****0 if _verify(args.firmware) then +****0 os.exit(su.EXIT_STATUS_OK) + else +****0 print("Invalid firmware!") +****0 os.exit(su.EXIT_STATUS_INVALID_FIRMWARE) + end + end + + local function upgrade(args) +****0 assert_su_installed() +****0 local partitions = su.get_partitions() + +****0 if not _verify(args.firmware) then +****0 print("Invalid firmware!") +****0 if args.force then +****0 print("Forcing upgrade to continue as requested.") + else +****0 os.exit(su.EXIT_STATUS_INVALID_FIRMWARE) + end + end + +****0 su.preserve_files_to_new_partition(args) + + -- postpone 10m defarable-reboot +****0 os.execute("awk '{print $1 + 600}' /proc/uptime > /tmp/deferrable-reboot.defer") + +****0 print(string.format("erasing partition %d", partitions.other)) +****0 os.execute(string.format("mtd erase fw%d", partitions.other)) + + -- It is important that the mtd -j option to preserve a file is used + -- with the file /tmp/sysupgrade.tgz because there are hooks in place + -- to unpack this tar and install the files at boot +****0 print(string.format("writing partition %d", partitions.other)) +****0 os.execute(string.format("mtd -j /tmp/sysupgrade.tgz write '%s' fw%d", +****0 args.firmware, partitions.other)) + + -- TODO: load bootargs from acompaning image, here is hardcoded!! +****0 local fw_mtd_str = '' +****0 if partitions.other == 2 then +****0 fw_mtd_str = '7936k(fw1),7936k(firmware)' + else +****0 fw_mtd_str = '7936k(firmware),7936k(fw2)' + end +****0 local boot_script_tpl = 'set bootargs console=ttyS0,115200 board=LIBREROUTERV1 mtdparts=spi0.0:256k(u-boot),64k(u-boot-env),%s,128k(res),64k(art); echo booting part %d; bootm ${fw_addr};' +****0 local boot_script = string.format(boot_script_tpl, fw_mtd_str, partitions.other) +****0 set_uboot_env(string.format('boot_%d', partitions.other), boot_script) +****0 set_testing_partition(partitions.other) + +****0 if not args.no_reboot then +****0 print("Rebooting into the new firmware. Do the confirm step if everything is ok.") +****0 os.execute("reboot") + end + end + + local function confirm(args) +****0 assert_su_installed() +****0 local partitions = su.get_partitions() +****0 if partitions.current == partitions.stable then +****0 print(string.format('the current partition: %d is already the stable partition, aborting', partitions.current)) +****0 os.exit(su.EXIT_STATUS_ALREADY_CONFIRMED) + end + +****0 print("Canceling and disabling automatic reboot") +****0 os.execute("/etc/init.d/safe_upgrade_auto_reboot stop") +****0 os.execute("/etc/init.d/safe_upgrade_auto_reboot disable") + +****0 set_stable_partition(partitions.current) +****0 print(string.format('Confirmed partition %d as stable partition', partitions.current)) + end + + local function test_other_partition(args) +****0 assert_su_installed() +****0 local partitions = su.get_partitions() +****0 set_testing_partition(partitions.other) +****0 print(string.format('Next boot will run partition: %d. You may confirm it if you like after reboot.', partitions.other)) + end + + + local function parse_args() + local function validate_file_exists(filename) +****0 if utils.file_exists(filename) then +****0 return filename + else +****0 return nil, string.format("file %q does not exists", filename) + end + end + + local function validate_safety_timeout(value) +****0 local timeout = tonumber(value) +****0 if timeout == nil then +****0 return nil, string.format("invalid --reboot-safety-timeout value: %q", value) + end +****0 if timeout < 60 then +****0 return nil, string.format("--reboot-safety-timeout must be greater than 60 but was %q", timeout) + end +****0 return timeout + end + +****0 local argparse = require 'argparse' +****0 local parser = argparse('safe-upgrade', 'Safe upgrade mechanism for dual-boot systems') +****0 parser:command_target('command') + +****0 parser:command('show', 'Show the status of the system partitions.') + +****0 local verify = parser:command('verify', 'Verify that the firmware is valid and can be installed.') +****0 verify:argument("firmware", "firmware image (xxx-sysupgrade.bin)"):convert(validate_file_exists) + +****0 local upgrade = parser:command('upgrade', 'Upgrade firmware in a non permanent way.') +****0 upgrade:argument("firmware", "firmware image (xxx-sysupgrade.bin)"):convert(validate_file_exists) +****0 upgrade:flag("-n --do-not-preserve-config", "Do not save configuration to the new partition") +****0 upgrade:flag("--disable-reboot-safety", "Disable the automatic reboot safety mechanism") +****0 upgrade:flag("--force", "Upgrade even if firmware is not valid") +****0 upgrade:flag("--no-reboot", "Do not reboot automatically after flashing") +****0 upgrade:option("--preserve-archive", [[Specify the files to be preserved as a tar.gz (Like the sysupgrade -f). +****0 To preserve a full config you may use sysupgrade --create-backup and use this .tar.gz.]]) +****0 :convert(validate_file_exists) +****0 upgrade:option("--reboot-safety-timeout", +****0 "Set the timeout (in seconds) of the automatic reboot safety mechanism") +****0 :default('600'):convert(validate_safety_timeout) + +****0 parser:command('confirm', ('Confirm the current partition. Use when after an upgrade ' .. +****0 'or after running "test-other-partition".')) + +****0 local bootstrap = parser:command('bootstrap', 'Install the safe-upgrade mechanism') +****0 bootstrap:flag("--force", "Install even it is already installed.") +****0 parser:command('test-other-partition', 'Mark the other partition as testing partition.') + +****0 parser:command('board-supported', 'Exits with 0 if board is supported') +****0 parser:command('confirm-remaining', ('Return the remaining seconds to confirm until the ' .. +****0 'automatic reboot is done or -1 if not in a confirmable state.')) + +****0 local args = parser:parse() + + +****0 return args + end + + -- detect if this module is run as a library or as a script + 1 if pcall(debug.getlocal, 4, 1) then + -- Library mode + 1 return su + else + -- Main script mode + +****0 local args = parse_args() +****0 assert_fw_env_configured() +****0 if args.bootstrap then +****0 bootstrap(args) +****0 elseif args.upgrade then +****0 upgrade(args) +****0 elseif args.confirm then +****0 confirm(args) +****0 elseif args.verify then +****0 verify(args) +****0 elseif args['test-other-partition'] then +****0 test_other_partition(args) +****0 elseif args['board-supported'] then +****0 if su.is_current_board_supported() then +****0 os.exit(su.EXIT_STATUS_OK) + else +****0 print("This board is not supported") +****0 os.exit(su.EXIT_STATUS_BOARD_NOT_SUPPORTED) + end +****0 elseif args['confirm-remaining'] then +****0 local out = "-1" +****0 local f = io.open(su.REBOOT_FILE_CONFIG_TIMEOUT_S) +****0 if f then +****0 local total_time_s = tonumber(f:read('*l')) +****0 if total_time_s then +****0 out = string.format("%d", math.floor(total_time_s - utils.uptime_s())) + end +****0 f:close() + end +****0 io.write(out) +****0 elseif args.show then +****0 assert_su_installed() +****0 print('safe-upgrade version: ' .. get_su_version()) +****0 local partitions = su.get_partitions() + --TODO show labels of partitions (maybe store them when flashing from a metadata file) +****0 print(string.format('current partition: %d', partitions.current)) +****0 print(string.format('stable partition: %d', partitions.stable)) +****0 print(string.format('testing partition: %d', partitions.testing)) + end + end + +============================================================================== +packages/shared-state-babel_links_info/files/usr/share/shared-state/publishers/shared-state-publish_babel_links_info +============================================================================== + #!/usr/bin/lua + + -- ! LibreMesh + -- ! Copyright (c) 2023 Javier Jorge + -- ! Copyright (c) 2023 Instituto Nacional de Tecnología Industrial + -- ! Copyright (C) 2023 Asociación Civil Altermundi + -- ! + -- ! SPDX-License-Identifier: AGPL-3.0-only + + 1 local utils = require('lime.utils') + 1 local ubus = require "ubus" + 1 local shared_state_links_info = require ("shared_state_links_info") + + 1 local ifaceip = {} + + 1 function get_interface_ip(ifname) + 22 if ifaceip[ifname] == nil then + 18 ifaceip[ifname] = string.gsub(utils.unsafe_shell("ip -6 address show " + 6 .. ifname .. + 6 " | awk '{if ($1 == \"inet6\") print $2}' | grep fe80 | awk -F/ '{print $1}'"), + 12 "\n", "") + end + 22 return ifaceip[ifname] + end + + 1 function get_babel_links_info() + 2 local conn = ubus.connect() + 2 local links = {} + 2 babelneigt_obj = ubus.call(conn, "babeld", "get_neighbours", {}) + 2 if babelneigt_obj ~= nil then + 10 for key, value in pairs(babelneigt_obj.IPv6) do + 8 local key_table = {(string.gsub(get_interface_ip(value.dev),":","")),(string.gsub(key,":",""))} + 8 table.sort(key_table) + 8 links[table.concat(key_table)]= { + 8 src_ip = get_interface_ip(value.dev), + 8 dst_ip = key, + 8 iface = value.dev + 8 } + end + end + 2 return links + end + + 1 shared_state_links_info.insert_in_ss_with_location(get_babel_links_info(),"babel_links_info") + +============================================================================== +packages/shared-state-bat_hosts/files/usr/lib/lua/bat-hosts.lua +============================================================================== + 2 local JSON = require("luci.jsonc") + 2 local utils = require("lime.utils") + + 2 local bat_hosts = {} + + 2 function bat_hosts.bathost_deserialize(hostname_plus_iface) + 5 local partial_hostname = hostname_plus_iface + local iface + 30 for _, ifname in ipairs(utils.get_ifnames()) do + 25 local serialized_ifname = string.gsub(ifname, "%W", "_") + 25 serialized_ifname = utils.literalize(serialized_ifname) + 25 local replaced_hostname = hostname_plus_iface:gsub("_" .. serialized_ifname, "") + -- hostname don't have underscores see utils.is_valid_hostname + 25 replaced_hostname = replaced_hostname:gsub("_", "-") + 25 if #replaced_hostname < #partial_hostname then + 6 partial_hostname = replaced_hostname + 6 iface = ifname + end + end + 5 return partial_hostname, iface + end + + 2 function bat_hosts.get_bat_hosts_from_shared_state() +****0 return JSON.parse( +****0 io.popen("shared-state-async get bat-hosts 2> /dev/null", "r"):read("*all")) + end + + 2 function bat_hosts.get_bathost(mac, outgoing_iface) + 7 local bathosts = bat_hosts.get_bat_hosts_from_shared_state() + 7 local bathost = bathosts[mac:lower()] + 7 if bathost == nil then return end + 5 local hostname, iface = bat_hosts.bathost_deserialize(bathost) + 5 return { hostname = hostname, iface = iface } + end + + 2 return bat_hosts + +============================================================================== +packages/shared-state-bat_hosts/files/usr/libexec/rpcd/bat-hosts +============================================================================== + #!/usr/bin/env lua + --[[ + Copyright (C) 2013-2020 LibreMesh.org + This is free software, licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + + Copyright 2020 German Ferrero + ]]-- + + 4 local ubus = require "ubus" + 4 local json = require 'luci.jsonc' + 4 local utils = require 'lime.utils' + 4 local bat_hosts = require 'bat-hosts' + + 4 local conn = ubus.connect() + 4 if not conn then +****0 error("Failed to connect to ubus") + end + + local function get_bathost(msg) + 4 if not msg.mac or not utils.is_valid_mac(msg.mac) then + 1 utils.printJson({ status = "error", message = "invalid mac" }) + 1 return + end + + 3 if msg.outgoing_iface and not utils.has_value(utils.get_ifnames(), msg.outgoing_iface) then + 1 utils.printJson({ status = "error", message = "invalid outgoing interface" }) + 1 return + end + 2 local bathost = bat_hosts.get_bathost(msg.mac, msg.outgoing_iface) + 2 local result = {} + 2 if bathost.hostname ~= nil then + 2 result.status = "ok" + 2 result.bathost = bathost + else +****0 result.status = "error" +****0 result.error = "Couldn't retrieve hostname" + end + 2 utils.printJson(result) + end + + 4 local methods = { + 4 get_bathost = { mac = 'value', outgoing_iface = 'value'} + } + + 4 if arg[1] == 'list' then +****0 utils.printJson(methods) + end + + 4 if arg[1] == 'call' then + 4 local msg = utils.rpcd_readline() or '{}' + 4 msg = json.parse(msg) + 4 if arg[2] == 'get_bathost' then get_bathost(msg) +****0 else utils.printJson({ error = "Method not found" }) + end + end + +============================================================================== +packages/shared-state-bat_links_info/files/usr/share/shared-state/publishers/shared-state-publish_bat_links_info +============================================================================== + #!/usr/bin/lua + + --! LibreMesh + --! Copyright (C) 2023 Javier Jorge + --! Copyright (C) 2023 Asociación Civil Altermundi + --! + --! SPDX-License-Identifier: AGPL-3.0-only + + 1 local JSON = require("luci.jsonc") + 1 local utils = require('lime.utils') + 1 local network = require ("lime.network") + 1 local shared_state_links_info = require ("shared_state_links_info") + + 1 function get_bat_links_info() + 2 local bat_neighbors_obj={} + 2 local bat_originators_obj={} + 2 local bat_originators = utils.unsafe_shell("batctl oj") + 2 bat_originators_obj = JSON.parse(bat_originators) + + + 2 local bat_neighbors = utils.unsafe_shell("batctl nj") + 2 bat_neighbors = string.gsub(bat_neighbors,"neigh_address","dst_mac") + 2 bat_neighbors = string.gsub(bat_neighbors,"hard_ifname","iface") + 2 bat_neighbors_obj = JSON.parse(bat_neighbors) + 2 local kv_fromlinks = {} + 2 if bat_neighbors_obj then + 10 for key,neight_value in pairs (bat_neighbors_obj) do + 8 local macparts = network.get_mac(neight_value.iface) + 8 local src_macaddr = table.concat(macparts,":") + 8 neight_value.hard_ifindex=nil + 8 neight_value.src_mac=string.lower(src_macaddr) + 8 neight_value.dst_mac=string.lower(neight_value.dst_mac) + 136 for key,originator_value in pairs (bat_originators_obj) do + 128 if originator_value.hard_ifname == neight_value.iface and + 64 originator_value.neigh_address== originator_value.orig_address and + 16 originator_value.neigh_address== neight_value.dst_mac then + -- Batman "transmit link quality" (tq) is a byte that describes + -- the probability of a successful transmission towards a + -- neighbor node + 8 neight_value.tq = originator_value.tq + end + end + 8 local key_table = {(string.gsub(neight_value.src_mac,":","")),(string.gsub(neight_value.dst_mac,":",""))} + 8 table.sort(key_table) + 8 kv_fromlinks[table.concat(key_table)]=neight_value + end + end + 2 return kv_fromlinks + end + + 1 shared_state_links_info.insert_in_ss_with_location(get_bat_links_info(),"bat_links_info") + +============================================================================== +packages/shared-state-network_nodes/files/usr/lib/lua/network-nodes.lua +============================================================================== + 1 local shared_state = require("shared-state") + 1 local utils = require("lime.utils") + 1 local config = require("lime.config") + + 1 local network_nodes = {} + + 1 function network_nodes._node(hostname, member, fw_version, board, ipv4, ipv6) + 26 return {hostname=hostname, member=member, fw_version=fw_version, board=board, ipv4=ipv4, ipv6=ipv6} + end + + 1 function network_nodes._serialize_for_network_nodes(node) + 10 return {hostname=node.hostname, member=node.member, fw_version=node.fw_version, board=node.board, + 10 ipv4=node.ipv4, ipv6=node.ipv6} + end + + 1 function network_nodes._deserialize_from_network_nodes(data) + 17 return network_nodes._node(data.hostname, data.member, data.fw_version, data.board, data.ipv4, data.ipv6) + end + + 1 function network_nodes._nodes_from_db(db) + 6 local nodes = {} + 22 for hostname, value in pairs(db:get()) do + 16 nodes[hostname] = network_nodes._deserialize_from_network_nodes(value.data) + end + 6 return nodes + end + + 1 function network_nodes._create_node() + 2 local uci = config.get_uci_cursor() + + 2 local hostname = utils.hostname() + 2 local fw_version = utils.release_info()['DISTRIB_RELEASE'] + 2 local board = utils.current_board() + 2 local member = true + 2 local ipv4 = uci:get("network", "lan", "ipaddr") + 2 local ipv6 = uci:get("network", "lan", "ip6addr") + 2 if ipv6 then ipv6 = ipv6:gsub("/.*$", "") end -- remove the netmask info + 2 local node = network_nodes._node(hostname, member, fw_version, board, ipv4, ipv6) + 2 node.status = "recently_reachable" + + 2 return node + end + + --! Public API + + 1 function network_nodes.get_nodes() + 4 local network_nodes_db = shared_state.SharedStateMultiWriter:new("network_nodes") + 4 local node_and_links_db = shared_state.SharedState:new("nodes_and_links") + + 4 local nodes = {} + -- augment the node information from the network_nodes and the 'nodes_and_links' dbs + 14 for hostname, node in pairs(network_nodes._nodes_from_db(network_nodes_db)) do + 10 if node_and_links_db:get()[hostname] then + 2 node.status = "recently_reachable" + 8 elseif node.member then + 6 node.status = "unreachable" + else + 2 node.status = "gone" + end + 10 nodes[hostname] = node + end + 4 return nodes + end + + 1 function network_nodes.as_human_readable_table() + 1 local nodes = network_nodes.get_nodes() + 1 local tmpl = "%-26s %-16s %-30s %-20s %-30s %-40s\n" + 1 local out = string.format(tmpl, "hostname", "ipv4", "ipv6", "status", "board", "fw_version") + 4 for _, node in pairs(nodes) do + 3 if node.member then + 6 out = out .. string.format(tmpl, node.hostname, node.ipv4, node.ipv6, node.status, + 6 node.board, node.fw_version) + end + end + 1 return out + end + + 1 function network_nodes.publish() + 1 local node = network_nodes._create_node() + 1 local data = { + 1 [node.hostname] = network_nodes._serialize_for_network_nodes(node) + } + 1 network_nodes_db = shared_state.SharedStateMultiWriter:new("network_nodes") + 1 network_nodes_db:insert(data) + end + + 1 function network_nodes.mark_nodes_as_gone(hostnames) + 1 local network_nodes_db = shared_state.SharedStateMultiWriter:new("network_nodes") + 1 local nodes = network_nodes._nodes_from_db(network_nodes_db) + 1 local data = {} + 3 for _, hostname in pairs(hostnames or {}) do + 2 local node = nodes[hostname] + 2 if node then + 2 node.member = false + 2 data[hostname] = network_nodes._serialize_for_network_nodes(node) + end + end + 1 network_nodes_db:insert(data) + end + + 1 return network_nodes + +============================================================================== +packages/shared-state-node_info/files/usr/share/shared-state/publishers/shared-state-publish_node_info +============================================================================== + #!/usr/bin/lua + + --! LibreMesh + --! Copyright (C) 2023 Javier Jorge + --! Copyright (C) 2023 Asociación Civil Altermundi + --! + --! This program is free software: you can redistribute it and/or modify + --! it under the terms of the GNU Affero General Public License as + --! published by the Free Software Foundation, either version 3 of the + --! License, or (at your option) any later version. + --! + --! This program is distributed in the hope that it will be useful, + --! but WITHOUT ANY WARRANTY; without even the implied warranty of + --! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + --! GNU Affero General Public License for more details. + --! + --! You should have received a copy of the GNU Affero General Public License + --! along with this program. If not, see . + + 1 local JSON = require("luci.jsonc") + 1 local network = require ("lime.network") + 1 local location = require("lime.location") + 1 local utils = require('lime.utils') + 1 local config = require("lime.config") + + 1 local hostname = utils.hostname() + 1 function get_node_info() + 2 local uci = config.get_uci_cursor() + 2 local coords = location.get_node() or location.get_community() or + 2 {lat="FIXME", long="FIXME"} + 2 local fw_version = "no version" + 4 pcall(function () fw_version = utils.release_info()['DISTRIB_RELEASE'] end) + 2 local board = "no board" + 4 pcall(function () board = utils.current_board() end) + 2 local ipv4 = uci:get("network", "lan", "ipaddr") + 2 local ipv6 = uci:get("network", "lan", "ip6addr") + 2 if ipv6 then ipv6 = ipv6:gsub("/.*$", "") end + 2 local uptime = utils.uptime_s() + 2 local macs = network.get_own_macs("*") + 2 return {hostname=hostname, firmware_version=fw_version, board=board, + 2 ipv4=ipv4, ipv6=ipv6,coordinates=coords,macs= macs,uptime = uptime, + 2 device = "Router" } + end + + 1 local result = { [hostname] = get_node_info() } + 1 io.popen("shared-state-async insert node_info", "w"):write(JSON.stringify(result)) + +============================================================================== +packages/shared-state-ref_state_commons/files/usr/lib/lua/shared_state_links_info.lua +============================================================================== + #!/usr/bin/lua + + --! LibreMesh + --! Copyright (c) 2024 Javier Jorge + --! Copyright (c) 2024 Instituto Nacional de Tecnología Industrial + --! Copyright (C) 2024 Asociación Civil Altermundi + --! SPDX-License-Identifier: AGPL-3.0-only + + 3 local JSON = require("luci.jsonc") + 3 local location = require 'lime.location' + + 3 local shared_state_links_info = {} + + 3 function shared_state_links_info.add_dst_loc(links_info, shared_state_sample, hostname) + 5 if shared_state_sample ~= nil then + 6 for link, l_data in pairs(links_info.links) do + 12 for node, data in pairs(shared_state_sample) do + 8 if node ~= hostname and data.links ~= nil then + 2 local link_data = data.links[link] + 2 if link_data ~= nil and data.src_loc~= nil then + 1 l_data.dst_loc = {} + 1 l_data.dst_loc.lat = data.src_loc.lat + 1 l_data.dst_loc.long = data.src_loc.long + end + end + end + end + end + end + + 3 function shared_state_links_info.add_own_location_to_links(links) + 6 return { + 6 links = links, + -- we are not interested in the community location. + 6 src_loc = location.get_node() or { + 6 lat = "FIXME", + 6 long = "FIXME" + 6 } + 6 } + end + + 3 function shared_state_links_info.insert_in_ss_with_location(links,data_type_name) + 3 local hostname = io.input("/proc/sys/kernel/hostname"):read("*line") + 3 local links_info = shared_state_links_info.add_own_location_to_links(links) + 3 local shared_state_sample = JSON.parse(io.popen("shared-state-async get "..data_type_name, "r"):read('*all')) + 3 shared_state_links_info.add_dst_loc(links_info, shared_state_sample, hostname) + 3 local result = {[hostname] = links_info} + 3 io.popen("shared-state-async insert "..data_type_name, "w"):write(JSON.stringify(result)) + end + + 3 return shared_state_links_info + +============================================================================== +packages/shared-state-wifi_links_info/files/usr/share/shared-state/publishers/shared-state-publish_wifi_links_info +============================================================================== + #!/usr/bin/lua + + --! LibreMesh + --! Copyright (C) 2019 Gioacchino Mazzurco + --! Copyright (c) 2023 Javier Jorge + --! Copyright (c) 2023 Instituto Nacional de Tecnología Industrial + --! Copyright (C) 2023 Asociación Civil Altermundi + --! SPDX-License-Identifier: AGPL-3.0-only + + 1 local node_status = require("lime.node_status") + 1 local network = require("lime.network") + 1 local iwinfo = require ("iwinfo") + 1 local shared_state_links_info = require ("shared_state_links_info") + + 1 local data_type_name = "wifi_links_info" + + 1 function get_wifi_links_info() + 4 local stations = node_status.get_stations() + 4 local links = {} + 10 for _, station in ipairs(stations) do + 6 macparts = network.get_mac(station.iface) + 6 src_macaddr = string.lower(table.concat(macparts, ":")) + 6 local station_stats = node_status.get_station_stats(station) + 6 local freq = iwinfo.nl80211.frequency(station.iface) + 6 local chanenel = iwinfo.nl80211.channel(station.iface) + 6 local key_table = {string.lower(string.gsub(src_macaddr, ":", "")), + 6 string.lower(string.gsub(station.station_mac, ":", ""))} + 6 table.sort(key_table) + 6 links[table.concat(key_table)] = { + 6 src_mac = src_macaddr, + 6 dst_mac = string.lower(station.station_mac), + 6 signal = station_stats.signal, + 6 chains = station_stats.chains, + 6 iface = station.iface, + 6 rx_rate = station_stats.rx_rate, + 6 tx_rate = station_stats.tx_rate, + 6 freq = freq, + 6 channel = chanenel, + --dst_loc = {lat="FIXME",long="FIXME"} --if no location is found later no dst location will be informed + 6 } + end + 4 return links + end + + 1 shared_state_links_info.insert_in_ss_with_location(get_wifi_links_info(),data_type_name) + +============================================================================== +packages/shared-state/files/usr/lib/lua/shared-state.lua +============================================================================== + #!/usr/bin/lua + + --! Minimalistic CRDT-like shared state structure suitable for mesh networks + --! + --! Copyright (C) 2019-2020 Gioacchino Mazzurco + --! + --! This program is free software: you can redistribute it and/or modify + --! it under the terms of the GNU Affero General Public License version 3 as + --! published by the Free Software Foundation. + --! + --! This program is distributed in the hope that it will be useful, + --! but WITHOUT ANY WARRANTY; without even the implied warranty of + --! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + --! GNU Affero General Public License for more details. + --! + --! You should have received a copy of the GNU Affero General Public License + --! along with this program. If not, see . + + 10 local fs = require("nixio.fs") + 10 local JSON = require("luci.jsonc") + 10 local nixio = require("nixio") + 10 local uci = require("uci") + 10 local utils = require("lime.utils") + + 10 local shared_state = {} + 10 shared_state.DATA_DIR = '/var/shared-state/data/' + 10 shared_state.PERSISTENT_DATA_DIR = '/etc/shared-state/persistent-data/' + 10 shared_state.ERROR_LOCK_FAILED = 165 + 10 shared_state.CANDIDATE_NEIGHBORS_BIN = '/usr/bin/shared-state-get_candidates_neigh' + + 10 local SharedStateBase = {} + + 10 function SharedStateBase:load(mergeWithCurrentState) + 99 local onDiskData = JSON.parse(self.storageFD:readall()) or {} + 99 if mergeWithCurrentState then +****0 self:_merge(onDiskData) + else + 234 for key, value in pairs(onDiskData) do + 135 self.storage[key] = value + end + end + end + + 10 function SharedStateBase:lock(maxwait) + 101 if self.locked then return end + 101 maxwait = maxwait or 10 + 101 fs.mkdirr(fs.dirname(self.dataFile)) + 202 self.storageFD = nixio.open( + 202 self.dataFile, nixio.open_flags("rdwr", "creat") ) + + 101 for i=1,maxwait do + 101 if not self.storageFD:lock("tlock") then +****0 nixio.nanosleep(1) + else + 101 self.locked = true + 101 break + end + end + + 101 if not self.locked then +****0 self.log( "err", self.dataFile, "Failed acquiring lock on data!" ) +****0 os.exit(shared_state.ERROR_LOCK_FAILED) + end + end + + 10 function SharedStateBase:merge(stateSlice) + 5 self:lock() + 5 self:load() + 5 self:_merge(stateSlice) + 5 self:save() + 5 self:unlock() + 5 self:notifyHooks() + end + + 10 function SharedStateBase:notifyHooks() + 44 if self.changed then + 44 local jsonString = self:toJsonString() + 44 if not fs.dir(self.hooksDir) then return end +****0 for hook in fs.dir(self.hooksDir) do +****0 local cStdin = io.popen(self.hooksDir.."/"..hook, "w") +****0 cStdin:write(jsonString) +****0 cStdin:close() + end + end + end + + 10 function SharedStateBase:save() + 47 if self.changed then + 47 local outFd = io.open(self.dataFile, "w") + 47 outFd:write(self:toJsonString()) + 47 outFd:close() + 47 outFd = nil + end + end + + 10 function SharedStateBase:httpRequest(url, body) +****0 local tmpfname = os.tmpname() + +****0 local tmpfd = io.open(tmpfname, "w") +****0 tmpfd:write(body) +****0 tmpfd:close() +****0 tmpfd = nil + +****0 local cmd = "uclient-fetch --no-check-certificate -q -O- --timeout=3 " +****0 cmd = cmd.."--post-file='"..tmpfname.."' '"..url.."' ; " +****0 cmd = cmd.."rm -f '"..tmpfname.."'" +****0 local fd = io.popen(cmd) + +****0 local value = fd:read("*a") +****0 fd:close() + +****0 return value + end + + 10 function SharedStateBase:_sync(urls) +****0 urls = urls or {} + +****0 if #urls < 1 then +****0 local uci_cursor = uci:cursor() + local fixed_candidates = +****0 uci_cursor:get("shared-state", "options","candidates") or {} +****0 for _, line in pairs(fixed_candidates) do +****0 table.insert( + urls, +****0 line.."/"..self.dataType ) + end + +****0 io.input(io.popen(shared_state.CANDIDATE_NEIGHBORS_BIN)) +****0 for line in io.lines() do +****0 table.insert( + urls, +****0 self:getSyncUrl(line, self.dataType)) + end + end + +****0 for _,url in ipairs(urls) do +****0 local body = self:toJsonString() + +****0 local response = self:httpRequest(url, body) + +****0 if type(response) == "string" and response:len() > 1 then +****0 local parsedJson = JSON.parse(response) +****0 if parsedJson then self:_merge(parsedJson) end + else +****0 self.log( "debug", "error requesting "..url ) + end + end + end + + 10 function SharedStateBase:sync(urls) +****0 self:lock() +****0 self:load() +****0 self:unlock() +****0 self:_sync(urls) +****0 self:lock() +****0 self:load(true) -- Take in account changes happened during sync +****0 self:save() +****0 self:unlock() +****0 self:notifyHooks() + end + + 10 function SharedStateBase:toJsonString() + 91 return JSON.stringify(self.storage) + end + + 10 function SharedStateBase:get() + 52 self:lock() + 52 self:load() + 52 self:unlock() + 52 return self.storage + end + + 10 function SharedStateBase:unlock() + 99 if not self.locked then return end + 99 self.storageFD:lock("ulock") + 99 self.storageFD:close() + 99 self.storageFD = nil + 99 self.locked = false + end + + 10 function createSharedStateBase(dataType, logger, dataFile) + 60 local logger = (type(logger) == "function") and logger or function() end + 56 local newInstance = { + 56 dataType = dataType, + 56 log = logger, + --! Map + --! bleachTTL is the count of how much bleaching should occur before the + --! entry expires + --! author is the name of the host who generated that entry + --! data is the value of the entry + 56 storage={}, + --! true if self_storage has changed after loading + 56 changed=false, + -- File descriptor of the persistent file storage + 56 storageFD=nil, + --! true when persistent storage file is locked by this instance + 56 locked=false, + 56 dataFile = dataFile, + 56 hooksDir = "/etc/shared-state/hooks/"..dataType.."/" + } + 56 return newInstance + end + + 10 local SharedState = {} + 10 setmetatable(SharedState, {__index = SharedStateBase}) + + 10 function SharedState:new(dataType, logger) + 19 local dataFile = shared_state.DATA_DIR..dataType..".json" + 19 local newInstance = createSharedStateBase(dataType, logger, dataFile) + 19 setmetatable(newInstance, {__index = SharedState}) + 19 return newInstance + end + + 10 function SharedState:_bleach() + 4 local substancialChange = false + 10 for k,v in pairs(self.storage) do + 6 if(v.bleachTTL < 2) then + 1 self.storage[k] = nil + 1 substancialChange = true + else + 5 v.bleachTTL = v.bleachTTL-1 + end + 6 self.changed = true + end + 4 return substancialChange + end + + 10 function SharedState:bleach() + 4 self:lock() + 4 self:load() + 4 local shouldNotify = self:_bleach() + 4 self:save() + 4 self:unlock() + --! Avoid hooks being called if data hasn't substantially changed + 4 if(shouldNotify) then self:notifyHooks() end + end + + 10 function SharedState:_insert(key, data, bleachTTL) + 19 bleachTTL = bleachTTL or 30 + 19 self.storage[key] = { + 19 bleachTTL=bleachTTL, + 19 author=io.input("/proc/sys/kernel/hostname"):read("*line"), + 19 data=data + 19 } + 19 self.changed = true + end + + 10 function SharedState:insert(data, bleachTTL) + 12 self:lock() + 12 self:load() + 30 for key, lv in pairs(data) do self:_insert(key, lv, bleachTTL) end + 12 self:save() + 12 self:unlock() + 12 self:notifyHooks() + end + + 10 function SharedState:_merge(stateSlice) + 1 local stateSlice = stateSlice or {} + 4 for key,rv in pairs(stateSlice) do + 3 if rv.bleachTTL <= 0 then +****0 self.log( "debug", "sharedState:merge got expired entry" ) +****0 self.changed = true + else + 3 local lv = self.storage[key] + 3 if( lv == nil or lv.bleachTTL < rv.bleachTTL ) then + 4 self.log( "debug", "Updating entry for: "..key.." older: ".. + 2 (lv and lv.bleachTTL or 'no entry').." newer: "..rv.bleachTTL ) + 2 self.storage[key] = rv + 2 self.changed = true + end + end + end + end + + 10 function SharedState:_remove(key) + 1 if(self.storage[key] ~= nil and self.storage[key].data ~= nil) + 1 then self:_insert(key, nil) end + end + + 10 function SharedState:remove(keys) + 1 self:lock() + 1 self:load() + 2 for _,key in ipairs(keys) do self:_remove(key) end + 1 self:save() + 1 self:unlock() + 1 self:notifyHooks() + end + + 10 function SharedState:getSyncUrl(host) +****0 return "http://["..host.."]/cgi-bin/shared-state/"..self.dataType + end + + + 10 local SharedStateMultiWriter = {} + 10 setmetatable(SharedStateMultiWriter, {__index = SharedStateBase}) + + 10 function SharedStateMultiWriter:new(dataType, logger) + 37 local dataFile = shared_state.PERSISTENT_DATA_DIR..dataType..".json" + 37 local newInstance = createSharedStateBase(dataType, logger, dataFile) + 37 setmetatable(newInstance, {__index = SharedStateMultiWriter}) + 37 return newInstance + end + + + 10 function SharedStateMultiWriter:_merge(stateSlice) + --! Make merge based on an incremental counter (changes) and a random number (fortune) + 4 local stateSlice = stateSlice or {} + 8 for key,rv in pairs(stateSlice) do + 4 local lv = self.storage[key] + 4 if ( lv == nil or lv.changes < rv.changes or + 3 ( lv.changes == rv.changes and lv.fortune < rv.fortune )) then + 4 self.log( "debug", "Updating entry for: "..key.." older: ".. + 2 (lv and lv.changes or 'no entry') .." newer: "..rv.changes ) + 2 self.storage[key] = rv + 2 self.changed = true + end + end + end + + 10 function SharedStateMultiWriter:insert(data) + 25 self:lock() + 25 self:load() + 77 for key, lv in pairs(data) do self:_insert(key, lv) end + 25 self:save() + 25 self:unlock() + 25 self:notifyHooks() + end + + 10 function shared_state._getFortune() + 29 return math.random(1, 100000) + end + + 10 function SharedStateMultiWriter:_insert(key, data) + 52 local lv = self.storage[key] + 52 if (lv == nil or not utils.deepcompare(lv.data, data)) then + 48 local changes = lv and lv.changes + 1 or 0 + 48 self.storage[key] = { + 48 lastModified=os.time(), + 48 changes=changes, + 48 fortune=shared_state._getFortune(), + 48 author=io.input("/proc/sys/kernel/hostname"):read("*line"), + 48 data=data + 48 } + 48 self.changed = true + end + end + + 10 function SharedStateMultiWriter:getSyncUrl(host) +****0 return "http://["..host.."]/cgi-bin/shared-state-multiwriter/"..self.dataType + end + + 10 shared_state.SharedState = SharedState + 10 shared_state.SharedStateMultiWriter = SharedStateMultiWriter + 10 return shared_state + +============================================================================== +packages/shared-state/files/usr/libexec/rpcd/shared-state +============================================================================== + #!/usr/bin/env lua + + --! Shared State + --! Copyright (c) 2023 Javier Jorge + --! Copyright (c) 2023 Instituto Nacional de Tecnología Industrial + --! Copyright (C) 2023 Asociación Civil Altermundi + --! SPDX-License-Identifier: AGPL-3.0-only + + 15 local ubus = require "ubus" + 15 local utils = require('lime.utils') + 15 local shared_state = require("shared-state") + 15 local json = require 'luci.jsonc' + + 15 require("nixio.util") + + 15 local response_template = {data = {}, error = 500} + + local function showData(sharedState) + 8 local resultTable = sharedState:get() + 8 if next(resultTable) == nill then + 1 response_template.error = 404 + else + 26 for k, v in pairs(resultTable) do + 19 response_template.data[k] = v.data + end + 7 response_template.error = 0 + end + 8 utils.printJson(response_template) + end + + local function getFromSharedState(msg) + 2 local sharedState = shared_state.SharedState:new(msg.data_type, + 1 nixio.syslog) + 1 showData(sharedState) + end + + local function getFromSharedStateMultiWriter(msg) + 14 local sharedState = shared_state.SharedStateMultiWriter:new(msg.data_type, + 7 nixio.syslog) + 7 showData(sharedState) + end + + local function insertIntoSharedStateMultiWriter(msg) + 12 local sharedState = shared_state.SharedStateMultiWriter:new(msg.data_type, + 6 nixio.syslog) + 6 local inputTable = msg.json or {} + 6 sharedState:insert(inputTable) + end + + 15 local methods = { + 15 getFromSharedState = { + 15 data_type = 'value' + 15 }, + 15 getFromSharedStateMultiWriter = { + 15 data_type = 'value' + 15 }, + 15 insertIntoSharedStateMultiWriter = { + 15 data_type = 'value', + 15 json = 'value' + 15 } + } + + 15 if arg[1] == 'list' then + 1 utils.printJson(methods) + end + + 15 if arg[1] == 'call' then + 14 local msg = utils.rpcd_readline() + 14 msg = json.parse(msg) + 14 if arg[2] == 'getFromSharedState' then + 1 getFromSharedState(msg) + 13 elseif arg[2] == 'getFromSharedStateMultiWriter' then + 7 getFromSharedStateMultiWriter(msg) + 6 elseif arg[2] == 'insertIntoSharedStateMultiWriter' then + 6 insertIntoSharedStateMultiWriter(msg) + else +****0 utils.printJson({ +****0 error = "Method not found" + }) + end + end + +============================================================================== +packages/ubus-lime-location/files/usr/lib/lua/lime/location.lua +============================================================================== + #!/usr/bin/env lua + --[[ + Copyright 2017 Marcos Gutierrez + Copyright 2020 Santiago Piccinini + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-3.0 + ]]-- + + 5 local iwinfo = require "iwinfo" + 5 local json = require "luci.jsonc" + 5 local config = require "lime.config" + 5 local wireless = require "lime.wireless" + 5 local system = require "lime.system" + + 5 local location = {} + + 5 function location.is_valid_coordinate(value) + 32 return type(tonumber(value)) == "number" + end + + 5 function location.get_node() + 19 local uci = config.get_uci_cursor() + 19 local lat = uci:get("location", "settings", "node_latitude") + 19 local long = uci:get("location", "settings", "node_longitude") + + 19 if location.is_valid_coordinate(lat) and location.is_valid_coordinate(long) then + 3 return {lat=lat, long=long} + end + end + + 5 function location.get_community() + 8 local uci = config.get_uci_cursor() + 8 local lat = uci:get("location", "settings", "community_latitude") + 8 local long = uci:get("location", "settings", "community_longitude") + + 8 if location.is_valid_coordinate(lat) and location.is_valid_coordinate(long) then + 2 return {lat=lat, long=long} + end + end + + 5 function location.set(lat, long) + 2 local uci = config.get_uci_cursor() + 2 uci:set("location", "settings", "node_latitude", lat) + 2 uci:set("location", "settings", "node_longitude", long) + 2 uci:commit("location") + 2 local hostname = system.get_hostname() + 2 local data = {} + 2 data[hostname] = location.nodes_and_links() + 2 io.popen("shared-state insert nodes_and_links", "w"):write(json.stringify(data)) + end + + 5 function location.nodes_and_links() + 5 local hostname = io.input("/proc/sys/kernel/hostname"):read("*line") + 5 local macs = network.get_own_macs("wlan*") + + 5 local coords = location.get_node() or location.get_community() or {lat="FIXME", long="FIXME"} + local iface, currneigh, _, n + + 5 local interfaces = wireless.mesh_ifaces() + 5 local links = {} + 7 for _, iface in pairs(interfaces) do + 2 currneigh = iwinfo.nl80211.assoclist(iface) + 3 for mac, station in pairs(currneigh) do + 1 table.insert(links, string.lower(mac)) + end + end + 5 return {hostname=hostname, macs=macs, coordinates={lat=coords.lat, lon=coords.long}, links=links} + end + + 5 return location + +============================================================================== +packages/ubus-lime-location/files/usr/libexec/rpcd/lime-location +============================================================================== + #!/usr/bin/env lua + --[[ + Copyright 2017 Marcos Gutierrez + Copyright 2020 Santiago Piccinini + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-3.0 + ]]-- + + + 6 local ubus = require "ubus" + 6 local iwinfo = require "iwinfo" + 6 local json = require "luci.jsonc" + 6 local config = require("lime.config") + 6 local wireless = require "lime.wireless" + 6 local network = require 'lime.network' + 6 local utils = require 'lime.utils' + 6 local location = require 'lime.location' + + + 6 local conn = ubus.connect() + 6 if not conn then +****0 error("Failed to connect to ubus") + end + + local function _get_location(msg) + 3 local result = {location = {}, default = false} + + 3 local coords = location.get_node() + 3 if not coords then + 2 result.default = true + 2 coords = location.get_community() or {lat="FIXME", long="FIXME"} + end + + 3 result.location.lat = coords.lat + 3 result.location.lon = coords.long + 3 result.status = "ok" + 3 return result + end + + local function get_location(msg) + 3 utils.printJson(_get_location(msg)) + end + + local function set_location(msg) + 1 location.set(msg.lat, msg.lon) + 1 utils.printJson({ lat = msg.lat, lon = msg.lon, status = 'ok' }); + end + + local function nodes_and_links() + 1 local hostname = utils.hostname() + 1 local result = {} + 1 result[hostname] = location.nodes_and_links() + 1 utils.printJson(result) + end + + 6 function all_nodes_and_links(msg) +****0 print('{"result":'..utils.unsafe_shell("shared-state get nodes_and_links")..'}') + end + + 6 local methods = { + 6 all_nodes_and_links = { no_params = 0}, + 6 nodes_and_links = { no_params = 0}, + 6 get = { no_params = 0 }, + 6 set = { lat = 'value', lon = 'value' } + } + + 6 if arg[1] == 'list' then + 1 utils.printJson(methods) + end + + 6 if arg[1] == 'call' then + 5 local msg = utils.rpcd_readline() + 5 msg = json.parse(msg) + 5 if arg[2] == 'get' then get_location(msg) + 2 elseif arg[2] == 'set' then set_location(msg) + 1 elseif arg[2] == 'nodes_and_links' then nodes_and_links(msg) +****0 elseif arg[2] == 'all_nodes_and_links' then all_nodes_and_links(msg) +****0 else utils.printJson({ error = "Method not found" }) + end + end + +============================================================================== +packages/ubus-lime-metrics/files/usr/lib/lua/lime-metrics.lua +============================================================================== + #!/usr/bin/lua + + 1 local utils = require('lime-metrics.utils') + 1 local lutils = require("lime.utils") + 1 local json = require 'luci.jsonc' + + 1 local metrics = {} + + 1 function metrics.get_last_internet_path_filename() + 1 return "/etc/last_internet_path" + end + + 1 function metrics.get_metrics(target) + 2 local result = {} + 2 local node = target + 2 local loss = nil + 2 local shell_output = "" + + 2 if lutils.is_installed("lime-proto-bmx6") then +****0 loss = utils.get_loss(node..".mesh") +****0 shell_output = lutils.unsafe_shell("netperf -6 -l 10 -H "..node..".mesh| tail -n1| awk '{ print $5 }'") + 2 elseif lutils.is_installed("lime-proto-babeld") then + 1 loss = utils.get_loss(node) + 1 shell_output = lutils.unsafe_shell("netperf -l 10 -H "..node.."| tail -n1| awk '{ print $5 }'") + else + 1 return {status="error", error={msg="No lime-proto-bmx6 or lime-proto-babeld found", code="1"}} + end + 1 local bw = 0 + 1 if shell_output ~= "" and shell_output ~= nil then +****0 bw = shell_output:match("[%d.]+") + end + 1 result.loss = loss + 1 result.bandwidth = bw + 1 result.status = "ok" + 1 return result + end + + 1 function metrics.get_loss(target) +****0 local result = {} +****0 local node = target + local loss = nil + +****0 loss = utils.get_loss(node) +****0 result.loss = loss +****0 result.status = "ok" +****0 return result + end + + + 1 function metrics.get_gateway() + 2 local result = {} + 2 local gw = nil + + 2 local internet_path_file = io.open(metrics.get_last_internet_path_filename(), "r") + 2 if internet_path_file then + 2 local path_content = assert(internet_path_file:read("*a"), nil) + 2 internet_path_file:close() + 2 path = json.parse(path_content) or nil + 2 if lutils.tableLength(path) > 0 then + 1 return { status="ok", gateway=path[lutils.tableLength(path)] } + end + end + + 1 return {status="error", error={msg="Not found. No gateway available.", code="1"}} + end + + 1 function metrics.get_last_internet_path(msg) + 2 local internet_path_file = io.open(metrics.get_last_internet_path_filename(), "r") + 2 if internet_path_file then + 1 path_content = assert(internet_path_file:read("*a"), nil) + 1 internet_path_file:close() + 1 path = json.parse(path_content) or nil + 1 local result = {} + 1 if path ~= nil then + 1 result.path = path + 1 result.status = "ok" + 1 return result + end + else + 1 return {status="error", error={msg="Not found. No known Internet path.", code="1"}} + end + end + + 1 function metrics.get_internet_status( ) + 1 local result = {} + 1 local lossV4 = utils.get_loss("4.2.2.2") + 1 if lossV4 ~= "100" then + 1 result.IPv4 = { working=true } + else +****0 result.IPv4 = { working=false } + end + + 1 local lossV6 = utils.get_loss("2600::") + 1 if lossV6 ~= "100" then + 1 result.IPv6 = { working=true } + else +****0 result.IPv6 = { working=false } + end + 1 local lookup_output = utils.is_nslookup_working() + 1 if lookup_output ~= "" then + 1 result.DNS = { working=true } + else +****0 result.DNS = { working=false } + end + 1 result.status = "ok" + 1 return result + end + + 1 function metrics.get_station_traffic(msg) + 2 local iface = msg.iface + 2 local mac = msg.station_mac + 2 local result = {} + 2 local traffic = lutils.unsafe_shell("iw "..iface.." station get "..mac.." | grep bytes | awk '{ print $3}'") + 2 if traffic == "" or traffic == nil then + 1 return {status="error", error={msg="No interface found.", code="1"}} + end + 1 words = {} + 3 for w in traffic:gmatch("[^\n]+") do table.insert(words, w) end + 1 rx = words[1] + 1 tx = words[2] + 1 result.station = mac + 1 result.rx_bytes = tonumber(rx, 10) + 1 result.tx_bytes = tonumber(tx, 10) + 1 result.status = "ok" + 1 return result + end + + + 1 return metrics + +============================================================================== +packages/ubus-lime-metrics/files/usr/lib/lua/lime-metrics/utils.lua +============================================================================== + 1 local lutils = require("lime.utils") + + 1 local utils = {} + + 1 function utils.is_nslookup_working() +****0 local shell_output = lutils.unsafe_shell("nslookup google.com | grep Name -A2 | grep Address") +****0 return shell_output + end + + 1 function utils.get_loss(host) + 1 local shell_output = lutils.unsafe_shell("ping -q -i 0.1 -c4 -w2 "..host) + 1 local loss = "100" + 1 if shell_output ~= "" and shell_output ~= nil then +****0 loss = shell_output:match("(%d*)%% packet loss") + end + 1 return loss + end + + 1 return utils + +============================================================================== +packages/ubus-lime-utils/files/usr/lib/lua/lime-utils-admin.lua +============================================================================== + #!/usr/bin/env lua + --[[ + Copyright (C) 2020 LibreMesh.org + This is free software, licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + + Copyright 2020 Santiago Piccinini + ]]-- + + + 1 local utils = require 'lime.utils' + 1 local config = require 'lime.config' + 1 local upgrade = require 'lime.upgrade' + 1 local hotspot_wwan = require "lime.hotspot_wwan" + + + 1 local UPGRADE_METADATA_FILE = "/etc/upgrade_metadata" + + 1 local limeutilsadmin = {} + + 1 function limeutilsadmin.set_root_password(msg) + local result = nil + 1 if type(msg.password) ~= "string" then +****0 result = {status = 'error', msg = 'Password must be a string'} + else + 1 utils.set_shared_root_password(msg.password or '') + 1 result = {status = 'ok'} + end + 1 return result + end + + 1 function limeutilsadmin.set_hostname(msg) + 3 if msg.hostname ~= nil and utils.is_valid_hostname(msg.hostname) then + 1 local uci = config.get_uci_cursor() + 1 uci:set(config.UCI_NODE_NAME, 'system', 'hostname', msg.hostname) + 1 uci:commit(config.UCI_NODE_NAME) + 1 utils.unsafe_shell("lime-config") + 1 return { status = 'ok'} + else + local err + 2 if msg.hostname then + 1 err = 'Invalid hostname' + else + 1 err = 'Hostname not provided' + end + 2 return { status = 'error', msg = err } + end + end + + 1 function limeutilsadmin.is_upgrade_confirm_supported() + 2 local supported = upgrade.is_upgrade_confirm_supported() + 2 return {status = 'ok', supported = supported} + end + + + 1 function limeutilsadmin.firmware_upgrade(msg) + 2 local status, ret = upgrade.firmware_upgrade(msg.fw_path, msg.preserve_config, msg.metadata, msg.fw_type) + 2 if status then + 2 return {status = 'ok', metadata = ret} + else +****0 return {status = 'error', message = ret} + end + end + + 1 function limeutilsadmin.last_upgrade_metadata() + local metadata + 2 if utils.file_exists(UPGRADE_METADATA_FILE) then + 1 metadata = utils.read_obj_store(UPGRADE_METADATA_FILE) + 1 return {status = 'ok', metadata = metadata} + else + 1 return {status = 'error', message = 'No metadata available'} + end + end + + 1 function limeutilsadmin.firmware_confirm() + 1 local exit_code = os.execute("safe-upgrade confirm > /dev/null 2>&1") + 1 local status = 'error' + 1 if exit_code == 0 then + 1 status = 'ok' + end + 1 return {status = status, exit_code = exit_code} + end + + --! Creates a client connection to a wifi hotspot + 1 function limeutilsadmin.hotspot_wwan_enable(msg) + 3 local msg = msg or {} + 3 local status, errmsg = hotspot_wwan.safe_enable(msg.ssid, msg.password, msg.encryption, msg.radio) + 3 if status then + 3 return {status = 'ok'} + else +****0 return {status = 'error', message = errmsg} + end + end + + + 1 function limeutilsadmin.hotspot_wwan_disable(msg) + 1 local msg = msg or {} + 1 local status, errmsg = hotspot_wwan.disable(msg.radio) + 1 if status then + 1 return {status = 'ok'} + else +****0 return {status = 'error', message = errmsg} + end + end + + + 1 function limeutilsadmin.safe_reboot(msg) +****0 local result = {} + local function getStatus() +****0 local f = io.open('/overlay/upper/.etc.last-good.tgz', "rb") +****0 if f then f:close() end +****0 return f ~= nil + end + + -- Get safe-reboot status +****0 if msg.action == nil then return {error = true} end +****0 if msg.action == 'status' then result.status = getStatus() end + + -- Start safe-reboot +****0 if msg.action == 'start' then +****0 local args = '' +****0 if msg.value ~= nil then +****0 if msg.value.wait ~= nil then +****0 args = args .. ' -w ' .. msg.value.wait + end +****0 if msg.value.fallback ~= nil then +****0 args = args .. ' -f ' .. msg.value.fallback + end + end +****0 local sr = assert(io.popen('safe-reboot ' .. args)) +****0 sr:close() +****0 result.status = getStatus() +****0 if result.status == true then result.started = true end + end + + -- Rreboot now and wait for fallback timeout +****0 if msg.action == 'now' then +****0 local sr = assert(io.popen('safe-reboot now')) +****0 result.status = getStatus() +****0 result.now = result.status + end + + -- Keep changes and stop safe-reboot +****0 if msg.action == 'cancel' then +****0 result.status = true +****0 result.canceled = false +****0 local sr = assert(io.popen('safe-reboot cancel')) +****0 sr:close() +****0 if getStatus() == false then +****0 result.status = false +****0 result.canceled = true + end + end + + -- Discard changes - Restore previous state and reboot +****0 if msg.action == 'discard' then +****0 local sr = assert(io.popen('safe-reboot discard')) +****0 sr:close() +****0 result.status = getStatus() +****0 if result.status == true then result.started = true end + end + +****0 return result + end + + 1 return limeutilsadmin + + +============================================================================== +packages/ubus-lime-utils/files/usr/lib/lua/lime-utils.lua +============================================================================== + #!/usr/bin/env lua + + -- Used on lime-utils ubus script + 1 local limewireless = require 'lime.wireless' + 1 local utils = require 'lime.utils' + 1 local upgrade = require 'lime.upgrade' + 1 local node_status = require 'lime.node_status' + 1 local hotspot_wwan = require "lime.hotspot_wwan" + 1 local ubus = require "ubus" + + 1 local conn = ubus.connect() + 1 if not conn then error("Failed to connect to ubus") end + + 1 local limeutils = {} + + 1 function limeutils.get_cloud_nodes() + 2 local nodes = utils.unsafe_shell( + 1 "cat /tmp/bat-hosts | grep bat0 | cut -d' ' -f2 | sed 's/_bat0//' | sed 's/_/-/g' | sort | uniq") + 1 local result = {} + 1 result.nodes = {} + 6 for line in nodes:gmatch("[^\n]*") do + 5 if line ~= "" then table.insert(result.nodes, line) end + end + 1 result.status = "ok" + 1 return result + end + + + 1 function limeutils.get_mesh_ifaces() +****0 local result = {} +****0 result.ifaces = limewireless.mesh_ifaces() +****0 return result + end + + 1 function limeutils.get_node_status() + 2 local result = {} + 2 result.hostname = utils.hostname() + 2 result.ips = node_status.get_ips() + 2 result.most_active = node_status.get_most_active() + 2 result.switch_status = node_status.switch_status() + 2 result.uptime = tostring(utils.uptime_s()) + 2 result.status = "ok" + 2 return result + end + + 1 function limeutils.get_notes() + 3 local result = {} + 3 result.notes = utils.read_file('/etc/banner.notes') or '' + 3 result.status = "ok" + 3 return result + end + + 1 function limeutils.set_notes(msg) + 1 local banner = utils.write_file('/etc/banner.notes', msg.text) + 1 return limeutils.get_notes() + end + + 1 function limeutils.get_community_settings() +****0 local config = conn:call("uci", "get", {config = "lime-app"}).values +****0 if config ~= nil then +****0 for name, value in pairs(config) do + -- TODO: Find a best way to remove uci keys +****0 function table.removekey(table, key) +****0 local element = table[key] +****0 table[key] = nil +****0 return element + end +****0 table.removekey(value, ".name") +****0 table.removekey(value, ".index") +****0 table.removekey(value, ".anonymous") +****0 table.removekey(value, ".type") +****0 return value + end + else +****0 return {error = "config not found"} + end + end + + -- todo(kon): move to utility class?? + 1 function limeutils.get_channels() +****0 local devices = limewireless.scandevices() +****0 local phys = {} +****0 for k, radio in pairs(devices) do +****0 local phyIndex = radio[".name"].sub(radio[".name"], -1) +****0 phys[k] = {phy = "phy" .. phyIndex} +****0 if limewireless.is5Ghz(radio[".name"]) then +****0 phys[k].freq = '5ghz' + else +****0 phys[k].freq = '2.4ghz' + end + end +****0 local frequencies = {} +****0 for _, phy in pairs(phys) do +****0 local info = utils.unsafe_shell("iw " .. phy.phy .. +****0 " info | sed -n '/Frequencies:/,/valid/p' | sed '1d;$d' | grep -v radar | grep -v disabled | sed -e 's/.*\\[\\(.*\\)\\].*/\\1/'") +****0 frequencies[phy.freq] = utils.split(info, '\n') + end +****0 return frequencies + end + + 1 function limeutils.get_config() +****0 local result = conn:call("uci", "get", +****0 {config = "lime-autogen", section = "wifi"}) +****0 result.channels = limeutils.get_channels() +****0 return result + end + + 1 function limeutils.get_upgrade_info() + 1 local result = upgrade.get_upgrade_info() + 1 if not result then return {status = "error"} end + 1 result.status = 'ok' + 1 return result + end + + + 1 function limeutils.hotspot_wwan_get_status(msg) + 3 local msg = msg or {} + 3 local status, errmsg = hotspot_wwan.status(msg.radio) + 3 if status then + 3 return { + 3 status = 'ok', + 3 enabled = status.enabled, + 3 connected = status.connected, + 3 signal = status.signal + 3 } + else +****0 return {status = 'error', message = errmsg} + end + end + + 1 return limeutils + +============================================================================== +packages/ubus-lime-utils/files/usr/lib/lua/lime/hotspot_wwan.lua +============================================================================== + #!/usr/bin/env lua + --[[ + Copyright (C) 2021 LibreMesh.org + This is free software, licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + + Copyright 2021 Santiago Piccinini + ]]-- + + 3 local utils = require 'lime.utils' + 3 local config = require 'lime.config' + 3 local iwinfo = require "iwinfo" + 3 local wireless = require "lime.wireless" + + 3 local pkg = {} + + -- checkout ap_match_encryption for supported encryptions + 3 pkg.DEFAULT_ENCRYPTION = 'psk2' + 3 pkg.DEFAULT_SSID = 'internet' + 3 pkg.DEFAULT_PASSWORD = 'internet' + 3 pkg.DEFAULT_RADIO = 'radio0' + 3 pkg.GENERIC_SECTION_NAME = 'hotspot_wwan' + 3 pkg.IFACE_SECTION_NAME = 'lm_client_wwan' + 3 pkg.IFACE_NAME = 'client-wwan' + + 3 local gen_cfg = require 'lime.generic_config' + + 3 function pkg._apply_change() +****0 utils.execute_daemonized("lime-config && wifi reload") + end + + --! Create a client connection to a wifi hotspot + 3 function pkg.enable(ssid, password, encryption, radio) + 2 local uci = config.get_uci_cursor() + 2 local encryption = encryption or pkg.DEFAULT_ENCRYPTION + 2 local ssid = ssid or pkg.DEFAULT_SSID + 2 local password = password or pkg.DEFAULT_PASSWORD + 2 local radio = radio or pkg.DEFAULT_RADIO + + 2 uci:set(config.UCI_NODE_NAME, pkg.GENERIC_SECTION_NAME, "generic_uci_config") + 4 uci:set(config.UCI_NODE_NAME, pkg.GENERIC_SECTION_NAME, "uci_set", { + 2 "wireless." .. radio .. ".disabled=0", + 2 "wireless." .. radio .. ".channel=auto", + 2 "wireless." .. pkg.IFACE_SECTION_NAME .. "=wifi-iface", + 2 "wireless." .. pkg.IFACE_SECTION_NAME .. ".device=" .. radio, + 2 "wireless." .. pkg.IFACE_SECTION_NAME .. ".network=" .. pkg.IFACE_SECTION_NAME, + 2 "wireless." .. pkg.IFACE_SECTION_NAME .. ".mode=sta", + 2 "wireless." .. pkg.IFACE_SECTION_NAME .. ".ifname=" .. pkg.IFACE_NAME, + 2 "wireless." .. pkg.IFACE_SECTION_NAME .. ".ssid=" .. ssid, + 2 "wireless." .. pkg.IFACE_SECTION_NAME .. ".encryption=" .. encryption, + 2 "wireless." .. pkg.IFACE_SECTION_NAME .. ".key=" .. password, + 2 "network." .. pkg.IFACE_SECTION_NAME .. "=interface", + 2 "network." .. pkg.IFACE_SECTION_NAME .. ".proto=dhcp", + 2 } + ) + 2 uci:commit(config.UCI_NODE_NAME) + 2 pkg._apply_change() + 2 return true + end + + 3 function ap_match_encryption(ap, encryption) + 2 if encryption == 'psk2' then + 2 return ap.encryption.enabled and ap.encryption.wpa == 2 + end +****0 return false + end + + 3 function pkg._is_safe(ssid, encryption, radio) + 7 local ifaces = wireless.get_radio_ifaces(radio) + 7 if utils.tableLength(ifaces) == 0 then + 4 return true + end + 5 for _, iface in pairs(ifaces) do + 3 if wireless.is_mesh(iface) then + 1 return false, 'radio has mesh ifaces' + end + end + 2 ifname = ifaces[1].ifname + 2 iface_type = iwinfo.type(ifname) + 2 if iface_type ~= nil then + 2 scanlist = iwinfo[iface_type].scanlist(ifname) + 3 for _, ap in pairs(scanlist) do + 2 if (ap.ssid == ssid and ap_match_encryption(ap, encryption)) then + 1 return true + end + end + end + 1 return false, 'hotspot ap not found' + end + + 3 function pkg.safe_enable(ssid, password, encryption, radio) + -- Enables the hotpost client only if the hotpost is already available + -- in order to avoid clients from ap interfaces to be kicked out. + 2 local encryption = encryption or pkg.DEFAULT_ENCRYPTION + 2 local ssid = ssid or pkg.DEFAULT_SSID + 2 local radio = radio or pkg.DEFAULT_RADIO + + 2 local is_safe, reason = pkg._is_safe(ssid, encryption, radio) + 2 if is_safe then + 2 return pkg.enable(ssid, password, encryption, radio) + else +****0 return false, reason + end + end + + 3 function pkg.disable(radio) + 2 local uci = config.get_uci_cursor() + 2 local radio = radio or pkg.DEFAULT_RADIO + + 2 uci:delete(config.UCI_NODE_NAME, pkg.GENERIC_SECTION_NAME) + 2 uci:commit(config.UCI_NODE_NAME) + 2 uci:delete('system', 'hotspot_watchping') + 2 uci:commit('system') + 2 pkg._apply_change() + 2 return true + end + + 3 function pkg.status(radio) + 5 local uci = config.get_uci_cursor() + 5 local radio = radio or pkg.DEFAULT_RADIO + 5 local connected = false + local signal + + 5 local enabled = false + + 5 if uci:get(config.UCI_NODE_NAME, pkg.GENERIC_SECTION_NAME) then + 1 enabled = true + end + + 6 for mac, station in pairs(iwinfo.nl80211.assoclist(pkg.IFACE_NAME)) do + 1 connected = true + 1 signal = station['signal'] + end + + 5 return {connected = connected, signal = signal, enabled = enabled} + end + + + 3 return pkg + +============================================================================== +packages/ubus-lime-utils/files/usr/lib/lua/lime/node_status.lua +============================================================================== + 3 local limewireless = require 'lime.wireless' + 3 local iwinfo = require 'iwinfo' + 3 local utils = require 'lime.utils' + 3 local json = require("luci.jsonc") + + + -- Functions used by get_node_status + 3 local node_status = {} + + 3 function node_status.get_ips() + 2 local res = {} + 4 local ips = utils.unsafe_shell( + 2 "ip a s br-lan | grep inet | awk '{ print $1, $2 }'") + 2 for line in ips:gmatch("[^\n]+") do +****0 local words = {} +****0 for w in line:gmatch("%S+") do +****0 if w ~= "" then table.insert(words, w) end + end +****0 local version = words[1] +****0 local address = words[2] +****0 if version == "inet6" then +****0 table.insert(res, {version = "6", address = address}) + else +****0 table.insert(res, {version = "4", address = address}) + end + end + 2 return res + end + + 3 function node_status.get_stations() + 3 local res = {} + 3 local ifaces = limewireless.mesh_ifaces() + 3 for _, iface in ipairs(ifaces) do +****0 local iface_type = iwinfo.type(iface) +****0 local iface_stations = iface_type and iwinfo[iface_type].assoclist(iface) +****0 if iface_stations then +****0 for mac, station in pairs(iface_stations) do +****0 station['iface'] = iface +****0 station.station_mac = mac +****0 table.insert(res, station) + end + end + end + 3 return res + end + + 3 function node_status.get_station_stats(station) + 8 local iface = station.iface + 8 local mac = station.station_mac + 16 local iw_result = utils.unsafe_shell( + 8 "iw " .. iface .. " station get " .. mac) + 16 station.rx_bytes = tonumber( + 16 string.match(iw_result, "rx bytes:%s+(.-)\n"), 10) + 16 station.tx_bytes = tonumber( + 16 string.match(iw_result, "tx bytes:%s+(.-)\n"), 10) + 8 local signal_str = string.match(iw_result, "signal:%s+(.-)\n") + 8 local signal, chain = string.match(signal_str, "(%-?%d+)%s+%[(.-)%]") + 8 station.signal = tonumber(signal) + 8 station.chains = {} + --[[ + succesive calls to this function will lead to an error + /usr/bin/lua: /usr/lib/lua/lime/node_status.lua:60: bad argument #1 to 'gmatch' (string expected, got nil) + stack traceback: + [C]: in function 'gmatch' + /usr/lib/lua/lime/node_status.lua:60: in function 'get_station_stats' + ...tate/publishers/shared-state-publish_wifi_links_info:32: in function 'get_wifi_links_info' + ...tate/publishers/shared-state-publish_wifi_links_info:45: in main chunk + [C]: ? + ]]-- + 8 if chain ~= nil then + 24 for num in string.gmatch(chain, "%-?%d+") do + 16 table.insert(station.chains, tonumber(num)) + end + end + 8 return station + end + + 3 function node_status.get_most_active() + 3 local res = {} + 3 local stations = node_status.get_stations() + 3 if next(stations) ~= nil then + 1 local most_active = {} + 1 most_active.rx_bytes = 0 + 3 for _, station in ipairs(stations) do + 2 local station_stats = node_status.get_station_stats(station) + 2 if station_stats.rx_bytes > most_active.rx_bytes then + 1 most_active = station + end + end + 1 res = most_active + end + 3 return res + end + + 3 function node_status.switch_status() + 7 local response_ports = node_status.boardjson_get_ports() + 7 if #response_ports ~= 0 then + 7 if utils.is_dsa() then +****0 node_status.dsa_get_link_status(response_ports) + else + 7 node_status.swconfig_get_link_status(response_ports) + end + end + 7 return response_ports + end + + 3 function node_status.boardjson_get_ports() + 7 local response_ports = {} + 7 local board = utils.getBoardAsTable() + 7 if board['switch'] ~= nil and board['switch']['switch0'] ~= nil then -- legacy swconfig devices support + 21 for _, role in ipairs(board['switch']['switch0']['roles']) do + 48 for port_number in string.gmatch(role['ports'], "%S+") do + 34 if not tonumber(port_number) then + 14 local n = tonumber(string.match(port_number, "^%d+")) + 14 table.insert(response_ports, { num = n, role = "cpu", device = role['device']}) + else + 20 table.insert(response_ports, { num = tonumber(port_number), role = role['role'], device = role['device']}) + end + end + end +****0 elseif board['network'] ~= nil then -- DSA devices support +****0 for switch_name, switch in pairs(board['network']) do +****0 if switch['ports'] ~= nil then +****0 for _, port in ipairs(switch.ports) do +****0 table.insert(response_ports, { num = port, role = switch_name, device = switch_name}) + end + else +****0 table.insert(response_ports, { num = switch_name, role = switch_name, device = switch['device'] }) + end + end + end + 7 return response_ports + end + + 3 function node_status.dsa_get_link_status(ports) +****0 for _, port in ipairs(ports) do +****0 local dsa = utils.unsafe_shell("ip link show " .. port['num']) + -- Match ifindex, ifname, link (optional), and operstate +****0 local ifindex, ifname, link, operstate = dsa:match("^(%d+): ([^:@]+)@?([^:]*):.-state (%S+)") +****0 if ifindex and ifname and operstate then +****0 port['device'] = port['num'] +****0 port['num'] = tonumber(ifindex) +****0 port['role'] = link ~= "" and link or nil -- Handle optional link field +****0 if port['role'] == nil then +****0 port['role'] = ifname + end +****0 port['link'] = operstate +****0 if operstate == "LOWERLAYERDOWN" then +****0 port['link'] = "DOWN" + end + end + end +****0 return ports + end + + + 3 function node_status.swconfig_get_link_status(ports) + local function add_link_status(port_number, status) + 56 for x, obj in pairs(ports) do + 49 if obj.num == port_number then + 7 obj["link"] = status + end + end + end + + 7 local swconfig = utils.unsafe_shell("swconfig dev switch0 show") + 7 local lines = {} + 85 for line in swconfig:gmatch("[^\r\n]+") do + 78 table.insert(lines, line) + end + + local port_number + 85 for i, line in ipairs(lines) do + 78 if line:match("Port %d:") then + 18 port_number = tonumber(line:match("Port (%d):")) + end + 78 if string.find(line, "link:up") then + 4 add_link_status(port_number, "up") + 74 elseif string.find(line, "link:down") then + 3 add_link_status(port_number, "down") + end + end + 7 return ports + end + + 3 return node_status + + +============================================================================== +packages/ubus-lime-utils/files/usr/lib/lua/lime/upgrade.lua +============================================================================== + #!/usr/bin/env lua + --[[ + Copyright (C) 2020 LibreMesh.org + This is free software, licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + + Copyright 2020 Santiago Piccinini + ]]-- + + 3 local json = require 'luci.jsonc' + 3 local utils = require 'lime.utils' + + 3 local pkg = {} + + 3 pkg.UPGRADE_INFO_CACHE_FILE = '/tmp/upgrade_info_cache' + + 3 pkg.UPGRADE_STATUS_DEFAULT = 'NOT_STARTED' + 3 pkg.UPGRADE_STATUS_UPGRADING = 'UPGRADING' + 3 pkg.UPGRADE_STATUS_FAILED = 'FAILED' + 3 pkg.LIME_SYSUPGRADE_BACKUP_EXTRA_DIR = "/tmp/lime-sysupgrade/preserve" + 3 pkg.UPGRADE_METADATA_FILE = "/etc/upgrade_metadata" + + 3 function pkg.safe_upgrade_confirm_remaining_s() +****0 local remaining_s = tonumber(utils.unsafe_shell("safe-upgrade confirm-remaining")) +****0 if not remaining_s then +****0 remaining_s = -1 + end +****0 return remaining_s + end + + 3 function pkg.is_upgrade_confirm_supported() + 6 local exit_value = os.execute("safe-upgrade board-supported > /dev/null 2>&1") + 6 return exit_value == 0 + end + + 3 function pkg.get_upgrade_status() + 2 local info = utils.read_obj_store(pkg.UPGRADE_INFO_CACHE_FILE) + 2 if info.status == nil then +****0 return pkg.UPGRADE_STATUS_DEFAULT + else + 2 return info.status + end + end + + 3 function pkg.set_upgrade_status(status) + 4 return utils.write_obj_store_var(pkg.UPGRADE_INFO_CACHE_FILE, 'status', status) + end + + 3 function pkg.get_upgrade_info() + 3 local result = utils.read_obj_store(pkg.UPGRADE_INFO_CACHE_FILE) + 3 if result.is_upgrade_confirm_supported == nil then + 3 result.is_upgrade_confirm_supported = pkg.is_upgrade_confirm_supported() + end + 3 if not result.is_upgrade_confirm_supported then + 2 result.safe_upgrade_confirm_remaining_s = -1 + else + 1 result.safe_upgrade_confirm_remaining_s = pkg.safe_upgrade_confirm_remaining_s() + end + 3 utils.write_obj_store(pkg.UPGRADE_INFO_CACHE_FILE, result) + 3 return result + end + + 3 function pkg.firmware_verify(fw_path) + local command + 2 if pkg.is_upgrade_confirm_supported() then + 2 command = "safe-upgrade verify " + else +****0 command = "sysupgrade --test " + end + 2 command = command .. fw_path .. " > /dev/null 2>&1" + 2 local exit_value = os.execute(command) + 2 return exit_value == 0 + end + + + 3 function pkg.firmware_upgrade(fw_path, preserve_config, metadata, fw_type) + 2 if not fw_path then +****0 return nil, "Firmware file needed" + end + 2 if not utils.file_exists(fw_path) then +****0 return nil, "Firmware file not found" + end + 2 if pkg.get_upgrade_status() == pkg.UPGRADE_STATUS_UPGRADING then +****0 return nil, "There is an upgrade in progress" + end + + 2 metadata = metadata or {} + + 2 if not fw_type then + 2 if utils.stringEnds(fw_path, ".bin") then + 2 fw_type = 'sysupgrade' +****0 elseif utils.stringEnds(fw_path, ".sh") then +****0 fw_type = 'installer' + else +****0 return nil, "Unsupported firmware type" + end + end + + 2 if fw_type == 'sysupgrade' then + 2 if not pkg.firmware_verify(fw_path) then +****0 return nil, "Invalid firmware" + end + end + + 2 local backup = "" + 2 if preserve_config == nil then + 1 preserve_config = true + end + 2 if not preserve_config then +****0 backup = "DO_NOT_BACKUP=1" + end + + 2 metadata['config_preserved'] = preserve_config or false + + -- store info of the current firmware + 2 local current_fw_description = utils.release_info()["DISTRIB_DESCRIPTION"] + 2 if current_fw_description then + 2 metadata['old_release_description'] = current_fw_description + end + + 2 metadata['local_timestamp'] = os.time() + + --! Use the BACKUP_EXTRA_DIR function of lime-sysupgrade to store the medatada file + 2 utils.unsafe_shell("mkdir -p " .. pkg.LIME_SYSUPGRADE_BACKUP_EXTRA_DIR .. "/etc") + 2 local meta_file_path = pkg.LIME_SYSUPGRADE_BACKUP_EXTRA_DIR .. pkg.UPGRADE_METADATA_FILE + 2 if not utils.write_file(meta_file_path, json.stringify(metadata)) then +****0 return nil, "Can't write " .. meta_file_path + end + + 2 pkg.set_upgrade_status(pkg.UPGRADE_STATUS_UPGRADING) + 2 if fw_type == 'sysupgrade' then + --! Give some time so the response can be returned to the client + 2 local cmd = "sleep 3; FORCE=1 " .. backup .. " lime-sysupgrade " .. fw_path + --! stdin must be /dev/null because of a tar bug when using gzip that tries to read from stin and fails + --! if it is closed + 2 utils.execute_daemonized(cmd, "/tmp/lime-sysupgrade.log", "/dev/null") +****0 elseif fw_type == 'installer' then +****0 utils.unsafe_shell("chmod +x " .. fw_path) +****0 utils.execute_daemonized(fw_path, "/tmp/upgrade-installer.log", "/dev/null") + --! give the installer some time and try to collect the status up to that moment +****0 utils.unsafe_shell("sleep 10s") +****0 local progress_status = utils.read_file("/tmp/upgrade-installer-status") or 'unknown' +****0 if progress_status == 'failed' then +****0 pkg.set_upgrade_status(pkg.UPGRADE_STATUS_FAILED) +****0 return nil, utils.read_file("/tmp/upgrade-installer-error-mesage") or 'Installer failed without error message' + end + end + 2 return true, metadata + end + + 3 return pkg + +============================================================================== +packages/ubus-lime-utils/files/usr/lib/lua/lime/wireless_service.lua +============================================================================== + 2 local utils = require('lime.utils') + 2 local config = require('lime.config') + 2 local wireless = require('lime.wireless') + + 2 local wireless_service = {} + 2 wireless_service.AP_BAND = '2ghz' -- TODO: grab from uci config + + + local function get_node_ap_data(is_admin) + 5 local result = {} + 5 local cfg = wireless.get_band_config(wireless_service.AP_BAND) + 5 result.enabled = utils.has_value(cfg.modes, 'apname') + 5 result.has_password = (cfg.apname_encryption and + 5 cfg.apname_encryption ~= "none") + 5 result.password = is_admin and cfg.apname_key or nil + 5 result.ssid = wireless.resolve_ssid(cfg.apname_ssid) + 5 return result + end + + local function get_community_ap_data() + 5 local result = {} + 5 local cfg = wireless.get_band_config(wireless_service.AP_BAND) + 5 local community_cfg = wireless.get_community_band_config(wireless_service.AP_BAND) + 5 result.enabled = utils.has_value(cfg.modes, 'ap') + 5 result.ssid = wireless.resolve_ssid(cfg.ap_ssid) + 5 result.community = {} + 5 result.community.enabled = utils.has_value(community_cfg.modes, 'ap') + 5 return result + end + + 2 function wireless_service.get_access_points_data(is_admin) + 5 local result = {} + 5 result.node_ap = get_node_ap_data(is_admin) + 5 result.community_ap = get_community_ap_data() + 5 return result + end + + 2 function wireless_service.set_node_ap(has_password, password) + 2 local config = {} + 2 config.apname_encryption = has_password and 'psk2' or 'none' + 2 config.apname_key = password or nil + 2 wireless.set_band_config(wireless_service.AP_BAND, config) + end + + 2 function wireless_service.set_community_ap(enabled) + 2 if enabled then + 1 wireless.add_band_mode(wireless_service.AP_BAND, 'ap') + else + 1 wireless.remove_band_mode(wireless_service.AP_BAND, 'ap') + end + end + + 2 return wireless_service + +============================================================================== +packages/ubus-lime-utils/files/usr/libexec/rpcd/lime-utils-admin +============================================================================== + #!/usr/bin/env lua + --[[ + Copyright (C) 2020 LibreMesh.org + This is free software, licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + + Copyright 2020 Santiago Piccinini + ]]-- + + 16 local ubus = require "ubus" + 16 local json = require 'luci.jsonc' + 16 local utils = require 'lime.utils' + + 16 local limeutilsadmin = require 'lime-utils-admin' + + 16 local conn = ubus.connect() + 16 if not conn then +****0 error("Failed to connect to ubus") + end + + 16 local UPGRADE_METADATA_FILE = "/etc/upgrade_metadata" + + + local function set_root_password(msg) + 1 local result = limeutilsadmin.set_root_password(msg) + 1 return utils.printJson(result) + end + + local function set_hostname(msg) + 3 local result = limeutilsadmin.set_hostname(msg) + 3 return utils.printJson(result) + end + + local function is_upgrade_confirm_supported() + 2 local result = limeutilsadmin.is_upgrade_confirm_supported() + 2 return utils.printJson(result) + end + + local function firmware_upgrade(msg) + 2 local result = limeutilsadmin.firmware_upgrade(msg) + 2 return utils.printJson(result) + end + + local function last_upgrade_metadata() + 2 local result = limeutilsadmin.last_upgrade_metadata() + 2 return utils.printJson(result) + end + + local function firmware_confirm() + 1 local result = limeutilsadmin.firmware_confirm() + 1 return utils.printJson(result) + end + + --! Creates a client connection to a wifi hotspot + local function hotspot_wwan_enable(msg) + 3 local result = limeutilsadmin.hotspot_wwan_enable(msg) + 3 return utils.printJson(result) + end + + + local function hotspot_wwan_disable(msg) + 1 local result = limeutilsadmin.hotspot_wwan_disable(msg) + 1 return utils.printJson(result) + end + + local function safe_reboot(msg) +****0 local result = limeutilsadmin.safe_reboot(msg) +****0 utils.printJson(result) + end + + 16 local methods = { + 16 set_root_password = { password = 'value'}, + 16 set_hostname = { hostname = 'value'}, + 16 is_upgrade_confirm_supported = { no_params = 0 }, + 16 firmware_upgrade = { fw_path = 'value', preserve_config = 'value', metadata = 'value', fw_type = 'value'}, + 16 last_upgrade_metadata = { no_params = 0 }, + 16 firmware_confirm = { no_params = 0 }, + 16 hotspot_wwan_enable = { radio = 'value', ssid = 'value', password = 'value', encryption = 'value'}, + 16 hotspot_wwan_disable = { radio = 'value' }, + 16 safe_reboot = {action = 'value', value = 'value'}, + } + + 16 if arg[1] == 'list' then + 1 utils.printJson(methods) + end + + 16 if arg[1] == 'call' then + 15 local msg = utils.rpcd_readline() + 15 msg = json.parse(msg) + 15 if arg[2] == 'set_root_password' then set_root_password(msg) + 14 elseif arg[2] == 'set_hostname' then set_hostname(msg) + 11 elseif arg[2] == 'is_upgrade_confirm_supported' then is_upgrade_confirm_supported(msg) + 9 elseif arg[2] == 'firmware_upgrade' then firmware_upgrade(msg) + 7 elseif arg[2] == 'last_upgrade_metadata' then last_upgrade_metadata(msg) + 5 elseif arg[2] == 'firmware_confirm' then firmware_confirm(msg) + 4 elseif arg[2] == 'hotspot_wwan_enable' then hotspot_wwan_enable(msg) + 1 elseif arg[2] == 'hotspot_wwan_disable' then hotspot_wwan_disable(msg) +****0 elseif arg[2] == 'safe_reboot' then safe_reboot(msg) +****0 else utils.printJson({ error = "Method not found" }) + end + end + +============================================================================== +packages/ubus-lime-utils/files/usr/libexec/rpcd/wireless-service +============================================================================== + #!/usr/bin/env lua + --[[ + Copyright (C) 2021 LibreMesh.org + This is free software, licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + + Copyright 2021 German Ferrero + ]]-- + + 1 local ubus = require "ubus" + 1 local json = require 'luci.jsonc' + 1 local utils = require 'lime.utils' + 1 local wireless = require 'lime.wireless_service' + + 1 local conn = ubus.connect() + 1 if not conn then +****0 error("Failed to connect to ubus") + end + + local function get_access_points_data() + 1 local data = wireless.get_access_points_data() + 1 data.status = "ok" + 1 return utils.printJson(data) + end + + 1 local methods = { + 1 get_access_points_data = { no_params = 0 }, + } + + 1 if arg[1] == 'list' then +****0 utils.printJson(methods) + end + + 1 if arg[1] == 'call' then + 1 local msg = utils.rpcd_readline() + 1 msg = json.parse(msg) + 1 if arg[2] == 'get_access_points_data' then get_access_points_data(msg) +****0 else utils.printJson({ error = "Method not found" }) + end + end + +============================================================================== +packages/ubus-lime-utils/files/usr/libexec/rpcd/wireless-service-admin +============================================================================== + #!/usr/bin/env lua + --[[ + Copyright (C) 2021 LibreMesh.org + This is free software, licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + + Copyright 2021 German Ferrero + ]]-- + + 3 local ubus = require "ubus" + 3 local json = require 'luci.jsonc' + 3 local utils = require 'lime.utils' + 3 local wireless = require 'lime.wireless_service' + + 3 local conn = ubus.connect() + 3 if not conn then +****0 error("Failed to connect to ubus") + end + + local function get_access_points_data() + 1 local data = wireless.get_access_points_data(true) + 1 data.status = "ok" + 1 return utils.printJson(data) + end + + local function set_node_ap(msg) + 1 wireless.set_node_ap(msg.has_password, msg.password) + 1 return utils.printJson({status = 'ok'}) + end + + local function set_community_ap(msg) + 1 wireless.set_community_ap(msg.enabled) + 1 return utils.printJson({status = 'ok'}) + end + + 3 local methods = { + 3 get_access_points_data = { no_params = 0 }, + 3 set_node_ap = { has_password = 'value', password = 'value' }, + 3 set_community_ap = { enabled = 'value' } + } + + 3 if arg[1] == 'list' then +****0 utils.printJson(methods) + end + + 3 if arg[1] == 'call' then + 3 local msg = utils.rpcd_readline() + 3 msg = json.parse(msg) + 3 if arg[2] == 'get_access_points_data' then get_access_points_data(msg) + 2 elseif arg[2] == 'set_node_ap' then set_node_ap(msg) + 1 elseif arg[2] == 'set_community_ap' then set_community_ap(msg) +****0 else utils.printJson({ error = "Method not found" }) + end + end + +============================================================================== +packages/ubus-tmate/files/usr/lib/lua/tmate.lua +============================================================================== + #!/usr/bin/env lua + --[[ Copyright (C) 2013-2020 LibreMesh.org + This is free software, licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + + ]]-- + + 2 local utils = require 'lime.utils' + + + 2 local TMATE_SOCK = "/tmp/tmate.sock" + 2 local TMATE_CONFIG = "/etc/tmate/tmate.conf" + + 2 local tmate = {} + + 2 function tmate.cmd_as_str(cmd) +****0 final_cmd = "tmate -f "..TMATE_CONFIG.." -S "..TMATE_SOCK.." "..cmd +****0 return utils.unsafe_shell(final_cmd) + end + + local function unix_socket_listening(name) + 1 return "" ~= utils.unsafe_shell("netstat -xl | grep "..TMATE_SOCK.." 2>/dev/null") + end + + 2 function tmate.session_running() + 1 return unix_socket_listening(TMATE_SOCK) + end + + 2 function tmate.get_rw_session() + 3 return tmate.cmd_as_str("display -p '#{tmate_ssh}'"):sub(1, -2) + end + + 2 function tmate.get_ro_session() + 3 return tmate.cmd_as_str("display -p '#{tmate_ssh_ro}'"):sub(1, -2) + end + + 2 function tmate.get_connected_clients() + 2 return tmate.cmd_as_str("display -p '#{tmate_num_clients}'"):sub(1, -2) + end + + 2 function tmate.open_session() + 3 tmate.cmd_as_str("new-session -d") + 3 tmate.cmd_as_str("send-keys C-c") + end + + 2 function tmate.wait_session_ready() + 3 tmate.cmd_as_str("wait tmate-ready") + end + + 2 function tmate.close_session() + 3 tmate.cmd_as_str("kill-session -t 0") + end + + 2 return tmate + +============================================================================== +packages/ubus-tmate/files/usr/libexec/rpcd/tmate +============================================================================== + #!/usr/bin/env lua + --[[ Copyright (C) 2013-2020 LibreMesh.org + This is free software, licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3 + + ]]-- + + 4 local ubus = require "ubus" + 4 local json = require 'luci.jsonc' + 4 local utils = require 'lime.utils' + 4 local tmate = require 'tmate' + + 4 local conn = ubus.connect() + 4 if not conn then +****0 error("Failed to connect to ubus") + end + + local function get_session(msg) + 2 local session = "no session" + + 2 if tmate.session_running() then + 1 local rw_ssh = tmate.get_rw_session() + 1 local ro_ssh = tmate.get_ro_session() + 1 local clients = tmate.get_connected_clients() + + 1 session = { rw_ssh = rw_ssh, ro_ssh = ro_ssh, clients = clients } + end + + 2 utils.printJson({ status = "ok", session = session }) + end + + local function open_session(msg) + 1 tmate.open_session() + 1 tmate.wait_session_ready() + + 1 utils.printJson({status = "ok"}) + end + + local function close_session(params) +****0 if tmate.session_running() then +****0 tmate.close_session() + end + +****0 utils.printJson({ status = "ok" }) + end + + 4 local methods = { + 4 get_session = { no_params = 0 }, + 4 open_session = { no_params = 0 }, + 4 close_session = { no_params = 0 }, + } + + 4 if arg[1] == 'list' then + 1 utils.printJson(methods) + end + + 4 if arg[1] == 'call' then + 3 local msg = utils.rpcd_readline() + 3 msg = json.parse(msg) + 3 if arg[2] == 'get_session' then get_session(msg) + 1 elseif arg[2] == 'open_session' then open_session(msg) +****0 elseif arg[2] == 'close_session' then close_session(msg) +****0 else utils.printJson({ error = "Method not found" }) + end + end + + +============================================================================== +packages/wifi-unstuck-wa/files/usr/lib/lua/lime/wifi_unstuck_wa.lua +============================================================================== + 2 local config = require "lime.config" + 2 local utils = require "lime.utils" + 2 local iwinfo = require "iwinfo" + 2 local nixio = require 'nixio' + 2 local wu = {} + + -- so we always scan in at least one different frequency + 2 wu.FREQ_2GHZ_LIST = "2412 2462" + 2 wu.FREQ_5GHZ_LIST = "5180 5240" + + -- if iw runs for 5 min, it is likely hanging + 2 wu.TIMEOUT = tonumber( config.get("wifi", "unstuck_timeout", 300 )) + + 2 function wu.get_stickable_ifaces() + 3 local uci = config.get_uci_cursor() + 3 local ifaces = {} + 3 local devices = {} + + 6 uci:foreach("wireless", "wifi-iface", function(entry) + 15 if (entry.mode == 'mesh' or entry.mode == 'adhoc' or + 6 entry.mode == 'sta' or entry.mode == 'ap') then + 15 local device_path = uci:get("wireless", entry.device, "path") + 15 local device_disabled = uci:get("wireless", entry.device, "disabled") + --! get only one interface per radio and check that the radio is not disabled + 15 if device_path and device_disabled == '0' and devices[device_path] == nil then + 9 table.insert(ifaces, entry.ifname) + 9 devices[device_path] = 1 + end + end + end) + 3 return ifaces + end + + 2 function wu.wait_and_kill_on_timeout(pid_time_started) + 2 local pid_done = {} + + 4 for pid,time_started in pairs(pid_time_started) do + 2 pid_done[pid]=false + end + + repeat + -- wait for 100ms + 301 nixio.nanosleep(0,100e6) + + -- see if something changed + while true do + 305 pid,state,code = nixio.waitpid(nil,"nohang") + 305 if not pid then break end + 4 if pid == 0 then break end + 4 pid_done[pid] = true + end + + -- see if time is up + 301 now = os.time() + 901 for pid,time_started in pairs(pid_time_started) do + 600 time_is_up = now - time_started > wu.TIMEOUT + 600 if not pid_done[pid] and time_is_up then + -- time is up. send SIGTERM + 1 nixio.kill(pid,15) + -- we don't care any longer about processes we signaled + 1 pid_done[pid] = true + end + end + + -- see if there are remaining processes + 301 all_done = true + 904 for pid,done in pairs(pid_done) do + 603 all_done = all_done and done + end + 301 until all_done + + end + + 2 function wu.do_workaround() + 2 local ifaces = wu.get_stickable_ifaces() + 2 local pid_time_started = {} + + 8 for _, iface in pairs(ifaces) do + 6 local cmd = "iw dev " .. iface .. " scan freq " + 6 local freq = iwinfo.nl80211.frequency(iface) + 6 if freq ~= nil then + 4 if freq < 3000 then + 2 cmd = cmd .. wu.FREQ_2GHZ_LIST + else + 2 cmd = cmd .. wu.FREQ_5GHZ_LIST + end + 4 utils.log(cmd) + + -- we can not use os.popen here, because it does not give us the + -- pid + 4 pid = nixio.fork() + 4 if pid == 0 then + 2 nixio.exec('/bin/sh','-c',cmd..' >/dev/null') + 2 os.exit(1) + else + 2 pid_time_started[pid] = os.time() + end + + 4 nixio.nanosleep(1) + end + end + + 2 wu.wait_and_kill_on_timeout(pid_time_started) + end + + 2 function wu.configure() + 1 interval = tonumber( config.get("wifi", "unstuck_interval", -1) ) + + 1 if interval and interval > 0 then + --! use sed to replace interval in /etc/crontabs/root + 2 io.popen("sed -i 's/\\*\\/\\d\\+ \\* \\* \\* \\* ((wifi-unstuck &> \\/dev\\/".. + 1 "null)&)/*\\/"..interval.." * * * * ((wifi-unstuck \\&> \\/dev\\/null)\\&)/g".. + 1 "' /etc/crontabs/root") + end + end + + 2 function wu.clean() + -- nothing to clean, but needs to be declared to comply with the API + end + + 2 return wu + +============================================================================== +tests/fakes/iwinfo.lua +============================================================================== + 18 local iwinfo = {} + 18 iwinfo.nl80211 = {} + 18 iwinfo.fake = {} + 18 iwinfo.mocks = {} + + 18 iwinfo.mocks.iw_station_get_result_wlan1 = [[ + Station c0:4a:00:be:7b:0a (on wlan1-mesh) + inactive time: 50 ms + rx bytes: 503044 + rx packets: 3976 + tx bytes: 545116 + tx packets: 1237 + tx retries: 9 + tx failed: 0 + rx drop misc: 3 + signal: -14 [-17, -16] dBm + signal avg: -12 [-14, -15] dBm + Toffset: 46408315 us + tx bitrate: 300.0 MBit/s MCS 15 40MHz short GI + rx bitrate: 300.0 MBit/s MCS 15 40MHz short GI + rx duration: 0 us + expected throughput: 58.43Mbps + mesh llid: 5944 + mesh plid: 1241 + mesh plink: ESTAB + mesh local PS mode: ACTIVE + mesh peer PS mode: ACTIVE + mesh non-peer PS mode: ACTIVE + authorized: yes + authenticated: yes + associated: yes + preamble: long + WMM/WME: yes + MFP: no + TDLS peer: no + DTIM period: 2 + beacon interval:100 + short slot time:yes + connected time: 139 seconds + 18 ]] + + + 18 iwinfo.mocks.iw_station_get_result_wlan0 = [[ + Station c0:4a:00:be:7b:09 (on wlan0-mesh) + inactive time: 140 ms + rx bytes: 3116498 + rx packets: 31613 + tx bytes: 1166333 + tx packets: 4462 + tx retries: 2448 + tx failed: 15 + rx drop misc: 938 + signal: -14 [-17, -18] dBm + signal avg: -14 [-17, -18] dBm + Toffset: 18446744073577465064 us + tx bitrate: 6.5 MBit/s MCS 0 + rx bitrate: 39.0 MBit/s MCS 10 + rx duration: 0 us + expected throughput: 2.197Mbps + mesh llid: 63041 + mesh plid: 61249 + mesh plink: ESTAB + mesh local PS mode: ACTIVE + mesh peer PS mode: ACTIVE + mesh non-peer PS mode: ACTIVE + authorized: yes + authenticated: yes + associated: yes + preamble: long + WMM/WME: yes + MFP: no + TDLS peer: no + DTIM period: 2 + beacon interval:100 + short slot time:yes + connected time: 5070 seconds + 18 ]] + + + 18 iwinfo.mocks.get_stations = { + 18 [1] = { + 18 ["rx_short_gi"] = false, + 18 ["station_mac"] = "C0:4A:00:BE:7B:09", + 18 ["rx_vht"] = false, + 18 ["rx_mhz"] = 20, + 18 ["rx_40mhz"] = false, + 18 ["tx_packets"] = 1574, + 18 ["tx_mhz"] = 20, + 18 ["rx_packets"] = 16879, + 18 ["rx_ht"] = true, + 18 ["tx_mcs"] = 9, + 18 ["noise"] = -95, + 18 ["rx_mcs"] = 1, + 18 ["tx_ht"] = true, + 18 ["iface"] = "wlan0-mesh", + 18 ["tx_rate"] = 26000, + 18 ["inactive"] = 1390, + 18 ["tx_short_gi"] = false, + 18 ["tx_40mhz"] = false, + 18 ["expected_throughput"] = 11437, + 18 ["tx_vht"] = false, + 18 ["rx_rate"] = 13000, + 18 ["signal"] = 13 + 18 }, + 18 [2] = { + 18 ["rx_short_gi"] = true, + 18 ["station_mac"] = "C0:4A:00:BE:7B:0A", + 18 ["rx_vht"] = false, + 18 ["rx_mhz"] = 40, + 18 ["rx_40mhz"] = true, + 18 ["tx_packets"] = 7078, + 18 ["tx_mhz"] = 40, + 18 ["rx_packets"] = 54294, + 18 ["rx_ht"] = true, + 18 ["tx_mcs"] = 15, + 18 ["noise"] = -91, + 18 ["rx_mcs"] = 15, + 18 ["tx_ht"] = true, + 18 ["iface"] = "wlan1-mesh", + 18 ["tx_rate"] = 300000, + 18 ["inactive"] = 70, + 18 ["tx_short_gi"] = true, + 18 ["tx_40mhz"] = true, + 18 ["expected_throughput"] = 59437, + 18 ["tx_vht"] = false, + 18 ["rx_rate"] = 300000, + 18 ["signal"] = -13 + 18 } + 18 } + + 18 iwinfo.mocks.wlan1_mesh_mac = {'C0', '00', '00', '01', '01', '01'} + 18 iwinfo.mocks.wlan0_mesh_mac = {'C0', '00', '00', '00', '00', '00'} + + 18 OP_MODES = { + 18 "Unknown", "Master", "Ad-Hoc", "Client", "Monitor", "Master (VLAN)", + 18 "WDS", "Mesh Point", "P2P Client", "P2P Go" + 18 } + + 18 HT_MODES = {"HT20", "HT40", "VHT20", "VHT40", "VHT80", "VHT80+80", "VHT160"} + + 18 iwinfo.fake._scanlists = {} + 18 iwinfo.fake._channels = {} + 18 iwinfo.fake._assoclists = {} + 18 iwinfo.fake._hwmodelists = {} + + 36 function iwinfo.fake.set_scanlist(phy_id, scanlist) + 11 iwinfo.fake._scanlists[phy_id] = scanlist + end + + 36 function iwinfo.fake.scanlist_gen_station(ssid, channel, signal, mac, mode, quality) + 1 local utils = require("lime.utils") + 1 assert(utils.has_value(OP_MODES, mode)) + + 1 local station = { + 1 ["encryption"] = { + 1 ["enabled"] = false, + 1 ["auth_algs"] = { } , + 1 ["description"] = None, + 1 ["wep"] = false, + 1 ["auth_suites"] = { } , + 1 ["wpa"] = 0, + 1 ["pair_ciphers"] = { } , + 1 ["group_ciphers"] = { } , + 1 } , + 1 ["quality_max"] = 70, + 1 ["ssid"] = ssid, + 1 ["channel"] = channel, + 1 ["signal"] = signal, + 1 ["bssid"] = bssid, + 1 ["mode"] = mode, + 1 ["quality"] = quality, + } + 1 return station + end + + + 36 function iwinfo.nl80211.scanlist(phy_id) + 12 return iwinfo.fake._scanlists[phy_id] or {} + end + + 36 function iwinfo.fake.set_channel(phy_id, channel) + 19 iwinfo.fake._channels[phy_id] = channel + end + + 36 function iwinfo.nl80211.channel(phy_id) + 10 return iwinfo.fake._channels[phy_id] + end + + 36 function iwinfo.fake.set_assoclist(radio, assoclist) + 4 iwinfo.fake._assoclists[radio] = assoclist + end + + 36 function iwinfo.nl80211.assoclist(radio) + 10 return iwinfo.fake._assoclists[radio] or {} + end + + 18 function iwinfo.type(phy_id) + 8 return 'nl80211' + end + + 36 function iwinfo.fake.gen_assoc_station(rx_ht_mode, tx_ht_mode, signal, quality, inactive_ms, + tx_packets, rx_packets) + 3 local utils = require("lime.utils") + + -- VHT modes not yet supported + 3 assert(utils.has_value({"HT20", "HT40"}, rx_ht_mode)) + 3 assert(utils.has_value({"HT20", "HT40"}, tx_ht_mode)) + + 3 local rx_vht = false + 3 local tx_vht = false + 3 local rx_ht = false + 3 local tx_ht = false + 3 local rx_mhz = 20 + 3 local tx_mhz = 20 + 3 local tx_40mhz = false + + 3 if rx_ht_mode == "HT40" then +****0 rx_ht = true +****0 rx_mhz = 40 + end + + 3 if tx_ht_mode == "HT40" then + 3 tx_ht = true + 3 tx_40mhz = true + 3 tx_mhz = 40 + end + + 3 local r = { + 3 ["rx_ht"] = rx_ht, + 3 ["rx_vht"] = rx_vht, + 3 ["rx_mhz"] = rx_mhz, + 3 ["rx_rate"] = rx_rate, + + 3 ["tx_ht"] = tx_ht, + 3 ["tx_vht"] = tx_vht, + 3 ["tx_40mhz"] = tx_40mhz, + 3 ["tx_mhz"] = tx_mhz, + 3 ["tx_mcs"] = tx_mcs, + 3 ["tx_rate"] = tx_rate, + 3 ["tx_short_gi"] = true, + + 3 ["tx_packets"] = tx_packets, + 3 ["rx_packets"] = rx_packets, + 3 ["noise"] = noise, + 3 ["inactive"] = inactive_ms, + 3 ["expected_throughput"] = throughtput, + 3 ["signal"] = signal + } + 3 return r + end + + 18 iwinfo.fake.HWMODE = { + 18 ["HW_2GHZ_N"] = { ["a"] = false, ["b"] = true, ["ac"] = false, ["g"] = true, ["n"] = true}, + 18 ["HW_5GHZ_N"] = { ["a"] = true, ["b"] = false, ["ac"] = false, ["g"] = false, ["n"] = true} + 18 } + + 36 function iwinfo.fake.set_hwmodelist(radio_or_phy, hwmodelist) + 26 iwinfo.fake._hwmodelists[radio_or_phy] = hwmodelist + end + + 36 function iwinfo.nl80211.hwmodelist(radio_or_phy) + 10 return iwinfo.fake._hwmodelists[radio_or_phy] + end + + 36 function iwinfo.fake.load_from_uci(uci_cursor) + 11 function create_device(dev) + local hwmode + 17 if dev.band == '5g' then + 15 hwmode = iwinfo.fake.HWMODE.HW_5GHZ_N + 2 elseif dev.band == '2g' then + 2 hwmode = iwinfo.fake.HWMODE.HW_2GHZ_N + else +****0 assert(0, 'posibility not supported yet, please add support!') + end + 17 iwinfo.fake.set_hwmodelist(dev[".name"], hwmode) + 17 iwinfo.fake.set_channel(dev[".name"], dev.channel) + end + 28 uci_cursor:foreach("wireless", "wifi-device", function(dev) create_device(dev) end) + end + + 18 return iwinfo + + +============================================================================== +tests/fakes/ubus.lua +============================================================================== + 10 local ubus = {} + + 10 function ubus.connect() + 51 local conn = {} + + 51 function conn.call() +****0 return {} + end + + 51 return conn + end + + 10 return ubus + + +============================================================================== +Summary +============================================================================== + +File Hits Missed Coverage +----------------------------------------------------------------------------------------------------------------------------------------- +./packages/pirania/tests/pirania_test_utils.lua 9 0 100.00% +./tests/utils.lua 87 1 98.86% +packages/check-internet/files/usr/libexec/rpcd/check-internet 18 2 90.00% +packages/deferrable-reboot/files/usr/lib/lua/deferrable_reboot.lua 33 2 94.29% +packages/eupgrade/files/usr/lib/lua/eupgrade.lua 83 25 76.85% +packages/first-boot-wizard/files/usr/lib/lua/firstbootwizard.lua 191 108 63.88% +packages/first-boot-wizard/files/usr/lib/lua/firstbootwizard/functools.lua 24 51 32.00% +packages/first-boot-wizard/files/usr/lib/lua/firstbootwizard/utils.lua 45 61 42.45% +packages/lime-eth-config/files/usr/lib/lua/lime-eth-config.lua 54 17 76.06% +packages/lime-eth-config/files/usr/libexec/rpcd/lime-eth-config 24 7 77.42% +packages/lime-hwd-ground-routing/files/usr/lib/lua/lime/hwd/ground_routing.lua 19 41 31.67% +packages/lime-hwd-openwrt-wan/files/usr/lib/lua/lime/hwd/openwrt_wan.lua 38 0 100.00% +packages/lime-hwd-usbradio/files/usr/lib/lua/lime/hwd/usbradio.lua 15 38 28.30% +packages/lime-hwd-watchcat/files/usr/lib/lua/lime/hwd/watchcat.lua 16 17 48.48% +packages/lime-proto-babeld/files/usr/lib/lua/lime/proto/babeld.lua 72 11 86.75% +packages/lime-proto-batadv/files/usr/lib/lua/lime/proto/batadv.lua 53 20 72.60% +packages/lime-proto-bmx7/files/usr/lib/lua/lime/proto/bmx7.lua 118 48 71.08% +packages/lime-proto-olsr/files/usr/lib/lua/lime/proto/olsr.lua 44 0 100.00% +packages/lime-proto-olsr2/files/usr/lib/lua/lime/proto/olsr2.lua 49 0 100.00% +packages/lime-proto-olsr6/files/usr/lib/lua/lime/proto/olsr6.lua 39 4 90.70% +packages/lime-proto-wan/files/usr/lib/lua/lime/proto/wan.lua 24 0 100.00% +packages/lime-system/files/usr/bin/migrate-wifi-bands-cfg 40 1 97.56% +packages/lime-system/files/usr/lib/lua/lime/config.lua 129 3 97.73% +packages/lime-system/files/usr/lib/lua/lime/firewall.lua 16 9 64.00% +packages/lime-system/files/usr/lib/lua/lime/generic_config.lua 74 1 98.67% +packages/lime-system/files/usr/lib/lua/lime/hardware_detection.lua 18 0 100.00% +packages/lime-system/files/usr/lib/lua/lime/mode/ap.lua 6 0 100.00% +packages/lime-system/files/usr/lib/lua/lime/mode/apname.lua 6 0 100.00% +packages/lime-system/files/usr/lib/lua/lime/mode/apup.lua 9 8 52.94% +packages/lime-system/files/usr/lib/lua/lime/mode/client.lua 5 0 100.00% +packages/lime-system/files/usr/lib/lua/lime/mode/ieee80211s.lua 5 0 100.00% +packages/lime-system/files/usr/lib/lua/lime/network.lua 238 113 67.81% +packages/lime-system/files/usr/lib/lua/lime/proto/ieee80211s.lua 15 0 100.00% +packages/lime-system/files/usr/lib/lua/lime/proto/lan.lua 46 11 80.70% +packages/lime-system/files/usr/lib/lua/lime/system.lua 39 7 84.78% +packages/lime-system/files/usr/lib/lua/lime/utils.lua 305 54 84.96% +packages/lime-system/files/usr/lib/lua/lime/wireless.lua 191 8 95.98% +packages/pirania/files/usr/lib/lua/portal/portal.lua 35 11 76.09% +packages/pirania/files/usr/lib/lua/read_for_access/cgi_handlers.lua 17 0 100.00% +packages/pirania/files/usr/lib/lua/read_for_access/read_for_access.lua 41 2 95.35% +packages/pirania/files/usr/lib/lua/voucher/cgi_handlers.lua 44 4 91.67% +packages/pirania/files/usr/lib/lua/voucher/config.lua 8 0 100.00% +packages/pirania/files/usr/lib/lua/voucher/hooks.lua 6 8 42.86% +packages/pirania/files/usr/lib/lua/voucher/store.lua 45 2 95.74% +packages/pirania/files/usr/lib/lua/voucher/utils.lua 46 16 74.19% +packages/pirania/files/usr/lib/lua/voucher/vouchera.lua 174 19 90.16% +packages/pirania/files/usr/libexec/rpcd/pirania 71 9 88.75% +packages/pirania/files/www/pirania-redirect/redirect 17 0 100.00% +packages/safe-upgrade/files/usr/sbin/safe-upgrade 97 159 37.89% +packages/shared-state-babel_links_info/files/usr/share/shared-state/publishers/shared-state-publish_babel_links_info 26 0 100.00% +packages/shared-state-bat_hosts/files/usr/lib/lua/bat-hosts.lua 22 2 91.67% +packages/shared-state-bat_hosts/files/usr/libexec/rpcd/bat-hosts 25 5 83.33% +packages/shared-state-bat_links_info/files/usr/share/shared-state/publishers/shared-state-publish_bat_links_info 31 0 100.00% +packages/shared-state-network_nodes/files/usr/lib/lua/network-nodes.lua 66 0 100.00% +packages/shared-state-node_info/files/usr/share/shared-state/publishers/shared-state-publish_node_info 24 0 100.00% +packages/shared-state-ref_state_commons/files/usr/lib/lua/shared_state_links_info.lua 29 0 100.00% +packages/shared-state-wifi_links_info/files/usr/share/shared-state/publishers/shared-state-publish_wifi_links_info 30 0 100.00% +packages/shared-state/files/usr/lib/lua/shared-state.lua 173 51 77.23% +packages/shared-state/files/usr/libexec/rpcd/shared-state 45 2 95.74% +packages/ubus-lime-location/files/usr/lib/lua/lime/location.lua 41 0 100.00% +packages/ubus-lime-location/files/usr/libexec/rpcd/lime-location 40 4 90.91% +packages/ubus-lime-metrics/files/usr/lib/lua/lime-metrics.lua 76 12 86.36% +packages/ubus-lime-metrics/files/usr/lib/lua/lime-metrics/utils.lua 9 3 75.00% +packages/ubus-lime-utils/files/usr/lib/lua/lime-utils-admin.lua 52 39 57.14% +packages/ubus-lime-utils/files/usr/lib/lua/lime-utils.lua 55 35 61.11% +packages/ubus-lime-utils/files/usr/lib/lua/lime/hotspot_wwan.lua 84 3 96.55% +packages/ubus-lime-utils/files/usr/lib/lua/lime/node_status.lua 80 35 69.57% +packages/ubus-lime-utils/files/usr/lib/lua/lime/upgrade.lua 62 23 72.94% +packages/ubus-lime-utils/files/usr/lib/lua/lime/wireless_service.lua 36 0 100.00% +packages/ubus-lime-utils/files/usr/libexec/rpcd/lime-utils-admin 46 5 90.20% +packages/ubus-lime-utils/files/usr/libexec/rpcd/wireless-service 16 3 84.21% +packages/ubus-lime-utils/files/usr/libexec/rpcd/wireless-service-admin 24 3 88.89% +packages/ubus-tmate/files/usr/lib/lua/tmate.lua 22 2 91.67% +packages/ubus-tmate/files/usr/libexec/rpcd/tmate 27 6 81.82% +packages/wifi-unstuck-wa/files/usr/lib/lua/lime/wifi_unstuck_wa.lua 66 0 100.00% +tests/fakes/iwinfo.lua 158 3 98.14% +tests/fakes/ubus.lua 6 1 85.71% +----------------------------------------------------------------------------------------------------------------------------------------- +Total 4169 1135 78.60% diff --git a/packages/lime-docs/Makefile b/packages/lime-docs/Makefile index d22883f26..e06a4d5f0 100644 --- a/packages/lime-docs/Makefile +++ b/packages/lime-docs/Makefile @@ -4,18 +4,22 @@ include $(TOPDIR)/rules.mk -GIT_COMMIT_DATE:=$(shell git log -n 1 --pretty=%ad --date=short . | sed 's|-|.|g' ) -GIT_COMMIT_TSTAMP:=$(shell git log -n 1 --pretty=%at . ) +# Pinned upstream docs content. Do not derive PKG_VERSION from *this* +# repository's git history for packages/lime-docs: every Makefile commit would +# change PKG_SOURCE_SUBDIR and invalidate PKG_MIRROR_HASH under OpenWrt 24.10 +# reproducible tarball rules (and gh-action-sdk runs `make package/.../check`). PKG_NAME:=lime-docs -PKG_VERSION=$(GIT_COMMIT_DATE)~$(GIT_COMMIT_TSTAMP) +PKG_SOURCE_VERSION:=3b580071fde1a817837a596fd41ae4bf7797ab1d +# Commit date of PKG_SOURCE_VERSION on libremesh.github.io + short sha suffix. +PKG_VERSION:=2026.04.07~3b58007 PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION) PKG_SOURCE_URL:=https://github.com/libremesh/libremesh.github.io/ PKG_SOURCE_PROTO:=git -PKG_SOURCE_VERSION:=HEAD PKG_SOURCE_SUBMODULES:=skip -PKG_MIRROR_HASH:=skip +PKG_MIRROR_HASH:=6975fe2a33bea711631c5b4e5311186b6b32a0cc3164b1ccc9d89b840361bcb1 + PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) include $(INCLUDE_DIR)/package.mk diff --git a/packages/lime-proto-batadv/files/usr/lib/lua/lime/proto/batadv.lua b/packages/lime-proto-batadv/files/usr/lib/lua/lime/proto/batadv.lua index 4ee156970..1722239a1 100644 --- a/packages/lime-proto-batadv/files/usr/lib/lua/lime/proto/batadv.lua +++ b/packages/lime-proto-batadv/files/usr/lib/lua/lime/proto/batadv.lua @@ -133,8 +133,25 @@ function batadv.runOnDevice(linuxDev, args) --! TODO: as of today ubus silently fails to properly setting up a linux network --! device for batman ADV usage dinamycally work around it by using --! shell commands instead - network.createStatic(devName) - utils.unsafe_shell("batctl if add "..devName) + local id = utils.get_id(devName) + local original_mac = network.get_mac(devName) + local vMacaddr = { } + -- Set first byte to 02 (locally administered unicast) + vMacaddr[1] = "02" + -- Use id[2] and id[3] from interface name for next bytes + vMacaddr[2] = id[2] + vMacaddr[3] = id[3] + -- Use last 3 bytes from main interface MAC + vMacaddr[4] = original_mac[4] + vMacaddr[5] = original_mac[5] + vMacaddr[6] = original_mac[6] + macaddr = table.concat(vMacaddr, ":") + + utils.unsafe_shell("ip link set dev "..devName.." address "..macaddr) + local ifnames = network.createStatic(devName) + utils.log("batadv created vlan "..devName.." with address:".. macaddr .." and static ".. ifnames) + --this seems not to be needed + --utils.unsafe_shell("batctl if add "..devName) end return batadv diff --git a/packages/lime-proto-batadv/tests/test_bat_apup.sh b/packages/lime-proto-batadv/tests/test_bat_apup.sh new file mode 100755 index 000000000..179639357 --- /dev/null +++ b/packages/lime-proto-batadv/tests/test_bat_apup.sh @@ -0,0 +1,113 @@ +#!/bin/bash +# configure at least three routers with apup and limed, only leave one of them +# on and turn the rest off. +# run this script on a computer connected to the node to start limed and u-bus +# listen. +# turn on the other nodes and wait for them to connect + +NODE_IP="thisnode.info" +SSH_USER="root" +SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + +ssh $SSH_OPTS "$SSH_USER@$NODE_IP" <&1 | while IFS= read -r line; do + echo "[\$(date +'%Y-%m-%d %H:%M:%S')] [limed] \$line" + done & + LIMED_PID=\$! + + ubus listen 2>&1 | while IFS= read -r line; do + echo "[\$(date +'%Y-%m-%d %H:%M:%S')] [ubus] \$line" + done & + UBUS_PID=\$! + + sleep 2 + echo "------------------------------------------" + sleep 2 + echo "------------------------------------------" + sleep 2 + + echo "[3/4] Bringing Wi-Fi up..." + wifi up + + echo "[4/4] Now you should be able to see new connections. Press Ctrl+C to stop." + wait \$LIMED_PID \$UBUS_PID +) +EOF + + +###### sample output +# [1/4] Bringing Wi-Fi down... +# [2/4] Starting limed and ubus listen ... +# ------------------------------------------ +# [2025-07-31 21:35:06] [limed] 'hostapd' namespace exists... +# [2025-07-31 21:35:07] [ubus] { "ubus.object.remove": {"id":1399214265,"path":"hostapd.wlan1-apup"} } +# ------------------------------------------ +# ------------------------------------------ +# [3/4] Bringing Wi-Fi up... +# [4/4] Now you should be able to see new connections. Press Ctrl+C to stop. +# [2025-07-31 21:35:16] [ubus] { "ubus.object.add": {"id":-1307156726,"path":"hostapd.wlan1-apup"} } + + +# [2025-07-31 21:36:02] [limed] Subscribing: hostapd.wlan1-apup +# [2025-07-31 21:36:03] [limed] peerSubscriber type: apup-newpeer data.ifname: wlan1-peer1 +# [2025-07-31 21:36:03] [ubus] { "ubus.object.add": {"id":565721540,"path":"network.interface.lm_net_lm_net_wlan1_peer1_static"} } +# [2025-07-31 21:36:03] [limed] lime.network.runProtocols(wlan1-peer1, ...) +# [2025-07-31 21:36:03] [limed] lime.proto.batadv.runOnDevice(wlan1-peer1, ...) +# [2025-07-31 21:36:03] [limed] lime.network.createVlan(wlan1-peer1, ...) +# [2025-07-31 21:36:03] [limed] vid = 29 +# [2025-07-31 21:36:03] [limed] type = 8021ad +# [2025-07-31 21:36:03] [limed] name = wlan1-peer1_29 +# [2025-07-31 21:36:03] [ubus] { "ubus.object.add": {"id":-1500836794,"path":"network.interface.lm_net_wlan1-peer1_batadv"} } +# [2025-07-31 21:36:03] [limed] ifname = wlan1-peer1 +# [2025-07-31 21:36:04] [ubus] { "network.interface": {"action":"ifup","interface":"lm_net_lm_net_wlan1_peer1_29_static"} } +# [2025-07-31 21:36:04] [ubus] { "ubus.object.add": {"id":-1232818779,"path":"network.interface.lm_net_lm_net_wlan1_peer1_29_static"} } +# [2025-07-31 21:36:04] [ubus] { "network.interface": {"action":"ifup","interface":"lm_net_wlan1-peer1_batadv"} } +# [2025-07-31 21:36:04] [limed] batadv createdwlan1-peer1_29 with address:02:58:11:22:55:bc and static lm_net_lm_net_wlan1_peer1_29_static +# [2025-07-31 21:36:04] [limed] lime.proto.babeld.runOnDevice(wlan1-peer1, ...) +# [2025-07-31 21:36:04] [limed] lime.network.createVlan(wlan1-peer1, ...) +# [2025-07-31 21:36:04] [limed] vid = 17 +# [2025-07-31 21:36:04] [limed] type = 8021ad +# [2025-07-31 21:36:04] [limed] name = wlan1-peer1_17 +# [2025-07-31 21:36:04] [limed] ifname = wlan1-peer1 +# [2025-07-31 21:36:04] [ubus] { "network.interface": {"action":"ifup","interface":"lm_net_lm_net_wlan1_peer1_17_static"} } +# [2025-07-31 21:36:04] [ubus] { "ubus.object.add": {"id":-1137476629,"path":"network.interface.lm_net_lm_net_wlan1_peer1_17_static"} } +# [2025-07-31 21:36:05] [limed] ip: RTNETLINK answers: File exists +# [2025-07-31 21:36:05] [ubus] { "ubus.object.add": {"id":-366401286,"path":"network.interface.lm_net_lm_net_wlan1_peer2_static"} } +# [2025-07-31 21:36:06] [ubus] { "ubus.object.add": {"id":-82673599,"path":"network.interface.lm_net_wlan1-peer2_batadv"} } +# [2025-07-31 21:36:06] [ubus] { "network.interface": {"action":"ifup","interface":"lm_net_lm_net_wlan1_peer2_29_static"} } +# [2025-07-31 21:36:06] [ubus] { "ubus.object.add": {"id":-1777215837,"path":"network.interface.lm_net_lm_net_wlan1_peer2_29_static"} } +# [2025-07-31 21:36:07] [ubus] { "network.interface": {"action":"ifup","interface":"lm_net_wlan1-peer2_batadv"} } +# [2025-07-31 21:36:07] [ubus] { "network.interface": {"action":"ifup","interface":"lm_net_lm_net_wlan1_peer2_17_static"} } +# [2025-07-31 21:36:07] [ubus] { "ubus.object.add": {"id":-1538210722,"path":"network.interface.lm_net_lm_net_wlan1_peer2_17_static"} } +# [2025-07-31 21:36:08] [limed] peerSubscriber type: apup-newpeer data.ifname: wlan1-peer2 +# [2025-07-31 21:36:08] [limed] ip: RTNETLINK answers: File exists +# [2025-07-31 21:36:08] [limed] lime.network.runProtocols(wlan1-peer2, ...) +# [2025-07-31 21:36:08] [limed] lime.proto.batadv.runOnDevice(wlan1-peer2, ...) +# [2025-07-31 21:36:08] [limed] lime.network.createVlan(wlan1-peer2, ...) +# [2025-07-31 21:36:08] [limed] vid = 29 +# [2025-07-31 21:36:08] [limed] type = 8021ad +# [2025-07-31 21:36:08] [limed] name = wlan1-peer2_29 +# [2025-07-31 21:36:08] [limed] ifname = wlan1-peer2 +# [2025-07-31 21:36:08] [limed] ip: RTNETLINK answers: File exists +# [2025-07-31 21:36:08] [limed] ip: RTNETLINK answers: File exists +# [2025-07-31 21:36:09] [limed] batadv createdwlan1-peer2_29 with address:02:f4:ce:22:55:bc and static lm_net_lm_net_wlan1_peer2_29_static +# [2025-07-31 21:36:09] [limed] lime.proto.babeld.runOnDevice(wlan1-peer2, ...) +# [2025-07-31 21:36:09] [limed] lime.network.createVlan(wlan1-peer2, ...) +# [2025-07-31 21:36:09] [limed] vid = 17 +# [2025-07-31 21:36:09] [limed] type = 8021ad +# [2025-07-31 21:36:09] [limed] name = wlan1-peer2_17 +# [2025-07-31 21:36:09] [limed] ifname = wlan1-peer2 +# [2025-07-31 21:36:09] [limed] ip: RTNETLINK answers: File exists +# [2025-07-31 21:36:09] [limed] ip: RTNETLINK answers: File exists diff --git a/packages/lime-system/files/usr/bin/limed b/packages/lime-system/files/usr/bin/limed index 31f47dcea..93ff67414 100755 --- a/packages/lime-system/files/usr/bin/limed +++ b/packages/lime-system/files/usr/bin/limed @@ -46,8 +46,27 @@ local peerSubscriber = { notify = function(nData, nType) if nType ~= "apup-newpeer" then return end - utils.log("peerSubscriber:", nType, nData.ifname) + utils.log("peerSubscriber type: %s data.ifname: %s ", nType, nData.ifname) + network.createStatic(nData.ifname) +--! interfaces should exist, but just in case we need to check, +--! to avoid +--! [2025-07-31 20:06:09] [limed] ip: SIOCGIFFLAGS: No such device +--! [2025-07-31 20:06:10] [limed] ip: can't find device 'wlan1-peer2' +--! [2025-07-31 20:06:10] [limed] lime.network.runProtocols(wlan1-peer2, ...) +--! [2025-07-31 20:06:10] [limed] lime.proto.batadv.runOnDevice(wlan1-peer2, ...) +--! [2025-07-31 20:06:11] [limed] lime.network.createVlan(wlan1-peer2, ...) +--! [2025-07-31 20:06:11] [limed] vid = 29 +--! [2025-07-31 20:06:11] [limed] type = 8021ad +--! [2025-07-31 20:06:11] [limed] name = wlan1-peer2_29 +--! [2025-07-31 20:06:11] [limed] ifname = wlan1-peer2 +--! [2025-07-31 20:06:12] [limed] ip: can't find device 'wlan1-peer2' +--! [2025-07-31 20:06:12] [ubus] { "ubus.object.add": +--{"id":-1478746778,"path":"network.interface.lm_net_wlan1-peer2_batadv"} } + if not network.device_exists(nData.ifname) then + utils.log("Device does not exist, skipping:", nData.ifname) + return + end network.runProtocols(nData.ifname) end } @@ -61,7 +80,7 @@ local apupSubscriber = { local evPath = "hostapd." .. apupDev - utils.log("Subscribing:", evPath) + utils.log("Subscribing: %s", evPath) ubus:subscribe(evPath, peerSubscriber) end } @@ -78,7 +97,7 @@ function limed.waitForHostapd() end if not found then utils.log("Waiting for 'hostapd' namespace...") - os.execute("sleep 1") -- Wait for 1 second before retrying + os.execute("sleep 1") else utils.log(" 'hostapd' namespace exists...") end diff --git a/packages/lime-system/files/usr/lib/lua/lime/network.lua b/packages/lime-system/files/usr/lib/lua/lime/network.lua index c70eca9af..7fc52a0a8 100644 --- a/packages/lime-system/files/usr/lib/lua/lime/network.lua +++ b/packages/lime-system/files/usr/lib/lua/lime/network.lua @@ -29,9 +29,14 @@ network.protoParamsSeparator=":" network.protoVlanSeparator="_" network.limeIfNamePrefix="lm_net_" - +--! Retuns the mac address of the interface or nill if it does not exist function network.get_mac(ifname) local _, macaddr = next(network.get_own_macs(ifname)) + --! this is to avoid the error: + --! ...ackages/lime-system/files/usr/lib/lua/lime/utils.lua:53: attempt to index local 'string' (a nil value) + if macaddr == nil then + return nil + end return utils.split(macaddr, ":") end @@ -598,6 +603,14 @@ function network.createStatic(linuxBaseIfname) return ifaceConf.name end +--! Check if a device exists in the system +function network.device_exists(dev) + local handle = io.popen("ip link show " .. dev .. " 2>/dev/null") + local result = handle:read("*a") + handle:close() + return result ~= nil and result ~= "" +end + --! Create a vlan at runtime via ubus function network.createVlan(linuxBaseIfname, vid, vlanProtocol) local vlanConf = { diff --git a/packages/lime-system/files/usr/lib/lua/lime/utils.lua b/packages/lime-system/files/usr/lib/lua/lime/utils.lua index 65179208a..051781ea4 100644 --- a/packages/lime-system/files/usr/lib/lua/lime/utils.lua +++ b/packages/lime-system/files/usr/lib/lua/lime/utils.lua @@ -50,6 +50,12 @@ end function utils.split(string, sep) local ret = {} + if sep == nil or sep == '' then + sep = '%s' --! default to whitespace + end + if string == nil or string == '' then + return ret + end for token in string.gmatch(string, "[^"..sep.."]+") do table.insert(ret, token) end return ret end diff --git a/packages/lime-system/tests/test_lime_network.lua b/packages/lime-system/tests/test_lime_network.lua index a9139df16..68b213783 100644 --- a/packages/lime-system/tests/test_lime_network.lua +++ b/packages/lime-system/tests/test_lime_network.lua @@ -28,6 +28,10 @@ describe('LiMe Network tests', function() assert.are.same({'00', '00', '00', '00', '00', '00'}, network.get_mac('lo')) end) + it('test get_mac for ethernet', function() + assert.is_nil (network.get_mac('nonexistent-interface')) + end) + it('test primary_interface configured interface', function() config.set('network', 'lime') config.set('network', 'primary_interface', 'test0') @@ -161,6 +165,14 @@ describe('LiMe Network tests', function() assert.are.Not.same(network.get_own_macs("wlan0"), network.get_own_macs("lo")) end) + it('test device_exists returns true for existing device', function() + assert.is_true(network.device_exists('lo')) + end) + + it('test device_exists returns false for non-existing device', function() + assert.is_false(network.device_exists('definitelynotadevicename')) + end) + before_each('', function() uci = test_utils.setup_test_uci() end) diff --git a/packages/test-packet-franco/files/etc/lime_dev_test_franco.txt b/packages/test-packet-franco/files/etc/lime_dev_test_franco.txt new file mode 100644 index 000000000..b8b4a4e2a --- /dev/null +++ b/packages/test-packet-franco/files/etc/lime_dev_test_franco.txt @@ -0,0 +1 @@ +hola \ No newline at end of file diff --git a/tools/ci/build_feed.sh b/tools/ci/build_feed.sh new file mode 100755 index 000000000..18292a471 --- /dev/null +++ b/tools/ci/build_feed.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Local reproducer of the CI feed build (same logic as openwrt/gh-action-sdk@v9). +# Requires Docker (and QEMU/binfmt if the SDK arch differs from the host). +set -euo pipefail + +usage() { + echo "Usage: $0 " >&2 + echo " opkg_arch: OpenWrt package arch, e.g. aarch64_cortex-a53 or mips_24kc" >&2 + echo " artifacts_dir: empty or writable directory; receives bin/packages///" >&2 + echo "Optional env: SDK_ARCH (default: -openwrt-24.10), FEEDNAME (default: lime_packages)," >&2 + echo " SDK_PACKAGES or PACKAGES (default: empty -> compile whole feed), BUILD, BUILD_LOG, V" >&2 + exit 1 +} + +if [[ $# -ne 2 ]]; then + usage +fi + +ARCH_OPKG="$1" +ARTIFACTS_DIR="$(realpath -m "$2")" +FEEDNAME="${FEEDNAME:-lime_packages}" +PACKAGES="${SDK_PACKAGES:-${PACKAGES:-}}" +SDK_ARCH="${SDK_ARCH:-${ARCH_OPKG}-openwrt-24.10}" + +mkdir -p "${ARTIFACTS_DIR}" + +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "${TMP_DIR}"' EXIT + +git clone --depth 1 --branch v9 https://github.com/openwrt/gh-action-sdk "${TMP_DIR}/gh-action-sdk" + +docker build \ + --build-arg "ARCH=${SDK_ARCH}" \ + -t local/openwrt-gh-action-sdk:v9 \ + "${TMP_DIR}/gh-action-sdk" + +docker run --rm \ + --env ARCH="${SDK_ARCH}" \ + --env BUILD="${BUILD:-1}" \ + --env BUILD_LOG="${BUILD_LOG:-1}" \ + --env FEEDNAME="${FEEDNAME}" \ + --env IGNORE_ERRORS="n m y" \ + --env INDEX="${INDEX:-0}" \ + --env "NO_REFRESH_CHECK=${NO_REFRESH_CHECK:-}" \ + --env "NO_SHFMT_CHECK=${NO_SHFMT_CHECK:-}" \ + --env PACKAGES="${PACKAGES}" \ + --env "V=${V:-}" \ + -v "$(pwd):/feed" \ + -v "${ARTIFACTS_DIR}:/artifacts" \ + local/openwrt-gh-action-sdk:v9 + +echo ">>> Feed output: ${ARTIFACTS_DIR}/bin/packages/${ARCH_OPKG}/${FEEDNAME}/" diff --git a/tools/ci/build_image.sh b/tools/ci/build_image.sh new file mode 100755 index 000000000..9cdcd5231 --- /dev/null +++ b/tools/ci/build_image.sh @@ -0,0 +1,843 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 5 ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +IMAGEBUILDER="$1" +PROFILE="$2" +OPENWRT_RELEASE="$3" +FEED_DIR="$(realpath -m "$4")" +OUTPUT_DIR="$(realpath -m "$5")" + +ARCH="${ARCH:?ARCH env var is required}" +FEED_BRANCH="${FEED_BRANCH:?FEED_BRANCH env var is required}" +PACKAGES="${PACKAGES:?PACKAGES env var is required}" + +# BUILD_INITRAMFS=1 repacks the ImageBuilder rootfs into a RAM-bootable +# kernel+CPIO image and ships THAT as the firmware artifact. Otherwise the +# squashfs-sysupgrade is uploaded for IPK validation only. +BUILD_INITRAMFS="${BUILD_INITRAMFS:-0}" + +# fit -> kernel + DTB + CPIO under one FIT config (mediatek/filogic). +# multi-uimage -> legacy IH_TYPE_MULTI uImage (ath79 boards w/o FIT). +# x86-combined -> ImageBuilder's GRUB+kernel+ext4 disk image (QEMU x86_64). +# dual-tftp -> kernel.bin + rootfs.uimage (uImage-wrapped CPIO) as TWO +# separate files (ath79 LibreRouter: U-Boot TFTP-loads each +# to a distinct RAM address; `bootm `). +IMAGE_FORMAT="${IMAGE_FORMAT:-fit}" +case "${IMAGE_FORMAT}" in + fit|multi-uimage|x86-combined|dual-tftp) ;; + *) + echo "ERROR: invalid IMAGE_FORMAT=${IMAGE_FORMAT} (expected: fit | multi-uimage | x86-combined | dual-tftp)" >&2 + exit 1 + ;; +esac + +FIT_ARCH="${FIT_ARCH:-}" +FIT_KERNEL_LOADADDR="${FIT_KERNEL_LOADADDR:-}" +FIT_DTS="${FIT_DTS:-}" +FIT_CONFIG="${FIT_CONFIG:-config-1}" +# CRITICAL: FIT_BOOTARGS must NOT contain `root=...`; the upstream +# mediatek/filogic chosen/bootargs has `root=/dev/fit0 rootwait ubi.block=0,fit` +# which would mount the on-flash squashfs over our initramfs. +FIT_BOOTARGS="${FIT_BOOTARGS:-console=ttyS0,115200n1 pci=pcie_bus_perf}" + +# Patch the FIT-shipped DTB to inject local-mac-address (workaround for +# openwrt#22858) on boards whose OEM MAC lives in a UBI factory volume. +DTB_PATCH_NVMEM_MAC="${DTB_PATCH_NVMEM_MAC:-0}" + +# Rewrite SPI-NAND partitioning to the legacy 23.05 layout (separate +# bl2/fip/factory/ubi MTDs) on Belkin RT3200 layout 1.0 units; without it +# the kernel UBI MTD overwrites BL31/FIP and bricks the device. +DTB_FORCE_LEGACY_PARTITIONS="${DTB_FORCE_LEGACY_PARTITIONS:-0}" + +if [[ "${BUILD_INITRAMFS}" == "1" && "${IMAGE_FORMAT}" == "x86-combined" ]]; then + echo "ERROR: BUILD_INITRAMFS=1 is incompatible with IMAGE_FORMAT=x86-combined" >&2 + exit 1 +fi + +if [[ "${BUILD_INITRAMFS}" == "1" ]]; then + # dual-tftp ships kernel.bin + rootfs.cpio as separate TFTP artifacts; + # no FIT/uImage repacking, so FIT_* vars are not required. + if [[ "${IMAGE_FORMAT}" != "dual-tftp" ]]; then + required_vars=(FIT_ARCH FIT_KERNEL_LOADADDR FIT_CONFIG FIT_BOOTARGS) + # ath79 (multi-uimage) fuses the DTB into kernel-bin so FIT_DTS is empty. + if [[ "${IMAGE_FORMAT}" == "fit" ]]; then + required_vars+=(FIT_DTS) + fi + for var in "${required_vars[@]}"; do + if [[ -z "${!var}" ]]; then + echo "ERROR: BUILD_INITRAMFS=1 (IMAGE_FORMAT=${IMAGE_FORMAT}) requires ${var} env var" >&2 + exit 1 + fi + done + fi +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DTB_PATCHER_HOST="${SCRIPT_DIR}/patch_dtb_local_mac.py" +DTB_PARTITIONS_PATCHER_HOST="${SCRIPT_DIR}/patch_dtb_partitions.py" +if [[ "${DTB_PATCH_NVMEM_MAC}" == "1" && ! -f "${DTB_PATCHER_HOST}" ]]; then + echo "ERROR: DTB_PATCH_NVMEM_MAC=1 but ${DTB_PATCHER_HOST} not found" >&2 + exit 1 +fi +if [[ "${DTB_FORCE_LEGACY_PARTITIONS}" == "1" && ! -f "${DTB_PARTITIONS_PATCHER_HOST}" ]]; then + echo "ERROR: DTB_FORCE_LEGACY_PARTITIONS=1 but ${DTB_PARTITIONS_PATCHER_HOST} not found" >&2 + exit 1 +fi + +if [[ ! -d "${FEED_DIR}/lime_packages" ]]; then + echo "ERROR: Feed dir must contain lime_packages/: ${FEED_DIR}/lime_packages" >&2 + exit 1 +fi + +# 24.10.x = .ipk + opkg + Packages[.gz]; 25.12.x = .apk + apk-tools + +# packages.adb. Branches downstream config and `make image` flags. +case "${OPENWRT_RELEASE}" in + 24.10.*) PKG_FORMAT=ipk ;; + *) PKG_FORMAT=apk ;; +esac +echo ">>> Package format for ${OPENWRT_RELEASE}: ${PKG_FORMAT}" + +WORK_DIR="$(mktemp -d)" +trap 'rm -rf "$WORK_DIR"' EXIT + +mkdir -p "${WORK_DIR}/out" "${WORK_DIR}/keys" "${OUTPUT_DIR}" +chmod 0755 "${WORK_DIR}" + +# Stage the DTB patchers in WORK_DIR; the in-container repack step +# only sees /work and /feed. +if [[ -f "${DTB_PATCHER_HOST}" ]]; then + cp "${DTB_PATCHER_HOST}" "${WORK_DIR}/patch_dtb_local_mac.py" + chmod 0755 "${WORK_DIR}/patch_dtb_local_mac.py" +fi +if [[ -f "${DTB_PARTITIONS_PATCHER_HOST}" ]]; then + cp "${DTB_PARTITIONS_PATCHER_HOST}" "${WORK_DIR}/patch_dtb_partitions.py" + chmod 0755 "${WORK_DIR}/patch_dtb_partitions.py" +fi + +# Repositories snippet appended to the IB's repos config. +# ipk -> repositories.conf (one `src/gz ` per line). +# apk -> repositories (one URL per line, file:// required). +if [[ "${PKG_FORMAT}" == "ipk" ]]; then + cat > "${WORK_DIR}/repositories.snippet" < "${WORK_DIR}/repositories.snippet" < "${WORK_DIR}/keys/a71b3c8285abd28b" <<'EOF' +untrusted comment: signed by libremesh.org key a71b3c8285abd28b +RWSnGzyChavSiyQ+vLk3x7F0NqcLa4kKyXCdriThMhO78ldHgxGljM/8 +EOF + +IMAGE_TAG="ghcr.io/openwrt/imagebuilder:${IMAGEBUILDER}-v${OPENWRT_RELEASE}" +echo ">>> Building ${PROFILE} with ${IMAGE_TAG} (BUILD_INITRAMFS=${BUILD_INITRAMFS})" + +# Make bind-mounted files readable by the container `buildbot` user (uid 1000). +chmod -R a+rX "${WORK_DIR}" "${FEED_DIR}" +chmod a+w "${WORK_DIR}/out" + +# Export so docker `-e VAR` (no =value) forwards real values to the in-container script. +export BUILD_INITRAMFS IMAGE_FORMAT FIT_ARCH FIT_KERNEL_LOADADDR FIT_DTS \ + FIT_CONFIG FIT_BOOTARGS DTB_PATCH_NVMEM_MAC \ + DTB_FORCE_LEGACY_PARTITIONS PROFILE PACKAGES \ + ARCH OPENWRT_RELEASE PKG_FORMAT + +# apk needs to (re)generate packages.adb in /feed; ipk's index is static. +if [[ "${PKG_FORMAT}" == "apk" ]]; then + FEED_MOUNT_FLAGS=":rw" +else + FEED_MOUNT_FLAGS=":ro" +fi + +docker run --rm \ + --user root \ + -e BUILD_INITRAMFS \ + -e IMAGE_FORMAT \ + -e FIT_ARCH \ + -e FIT_KERNEL_LOADADDR \ + -e FIT_DTS \ + -e FIT_CONFIG \ + -e FIT_BOOTARGS \ + -e DTB_PATCH_NVMEM_MAC \ + -e DTB_FORCE_LEGACY_PARTITIONS \ + -e PROFILE \ + -e PACKAGES \ + -e ARCH \ + -e OPENWRT_RELEASE \ + -e PKG_FORMAT \ + -v "${WORK_DIR}:/work" \ + -v "${FEED_DIR}:/feed${FEED_MOUNT_FLAGS}" \ + "${IMAGE_TAG}" \ + sh -lc ' + set -e + cp /work/keys/* keys/ 2>/dev/null || true + + if [ "${PKG_FORMAT}" = "ipk" ]; then + # opkg-lede treats `option check_signature` (any value) as enabled. + sed -i "/^option check_signature/d" repositories.conf + cat /work/repositories.snippet >> repositories.conf + + echo "=== final repositories.conf ===" + cat repositories.conf + echo "=== mounted feed contents (/feed/lime_packages) ===" + ls -la /feed/lime_packages/ | head -40 + feed_ipks=$(find /feed/lime_packages -maxdepth 1 -name "*.ipk" | wc -l) + feed_pkgs=$(grep -c "^Package:" /feed/lime_packages/Packages 2>/dev/null || echo 0) + echo "Feed has ${feed_ipks} IPKs and ${feed_pkgs} Packages entries" + + mkdir -p /tmp/preflight/tmp /tmp/preflight-lists + /builder/staging_dir/host/bin/opkg \ + --offline-root /tmp/preflight \ + --add-arch all:100 \ + --add-arch "${ARCH}:200" \ + -f /builder/repositories.conf \ + --cache /tmp/preflight-cache \ + --lists-dir /tmp/preflight-lists \ + update >/tmp/preflight.log 2>&1 || true + echo "=== opkg pre-flight update (last 25 lines) ===" + tail -n 25 /tmp/preflight.log + if ! /builder/staging_dir/host/bin/opkg \ + --offline-root /tmp/preflight \ + --add-arch all:100 \ + --add-arch "${ARCH}:200" \ + -f /builder/repositories.conf \ + --cache /tmp/preflight-cache \ + --lists-dir /tmp/preflight-lists \ + list 2>/dev/null | grep -q "^lime-system "; then + echo "ERROR: opkg cannot see lime-system in any configured feed" >&2 + /builder/staging_dir/host/bin/opkg \ + --offline-root /tmp/preflight \ + --add-arch all:100 \ + --add-arch "${ARCH}:200" \ + -f /builder/repositories.conf \ + --cache /tmp/preflight-cache \ + --lists-dir /tmp/preflight-lists \ + list 2>/dev/null | grep -E "^(lime|shared-state|babeld-auto|check-date|batctl)" >&2 || true + exit 1 + fi + echo "=== Pre-flight OK: local feed is visible to opkg ===" + + make image PROFILE="${PROFILE}" BIN_DIR=/work/out PACKAGES="${PACKAGES}" + + else + cat /work/repositories.snippet >> repositories + + # The IB Makefile drops `--allow-untrusted` from apk invocations + # when CONFIG_SIGNATURE_CHECK=y; our local feed is unsigned. + if grep -q "^CONFIG_SIGNATURE_CHECK=y" .config; then + echo ">>> Disabling CONFIG_SIGNATURE_CHECK in IB .config (local apk feed is unsigned)" + sed -i "s/^CONFIG_SIGNATURE_CHECK=y/# CONFIG_SIGNATURE_CHECK is not set/" .config + fi + + echo "=== final repositories ===" + cat repositories + echo "=== mounted feed contents (/feed/lime_packages) ===" + ls -la /feed/lime_packages/ | head -40 + feed_apks=$(find /feed/lime_packages -maxdepth 1 -name "*.apk" | wc -l) + echo "Feed has ${feed_apks} APKs" + + APK="" + for cand in \ + /builder/staging_dir/host/usr/bin/apk \ + /builder/staging_dir/host/bin/apk \ + /builder/staging_dir/hostpkg/usr/bin/apk \ + /usr/bin/apk \ + $(command -v apk 2>/dev/null || true) + do + if [ -n "$cand" ] && [ -x "$cand" ]; then + APK="$cand"; break + fi + done + if [ -z "$APK" ]; then + echo "ERROR: cannot locate apk binary in the IB" >&2 + find /builder/staging_dir/host -maxdepth 4 -name apk 2>/dev/null >&2 || true + exit 1 + fi + echo "Using apk binary: $APK" + + # apk 3.x: `--allow-untrusted` is a global flag (before sub-command). + if [ ! -f /feed/lime_packages/packages.adb ]; then + echo ">>> packages.adb missing - generating with the IB-host apk" + cd /feed/lime_packages + help_text=$("$APK" --help 2>&1 || true) + if printf "%s\n" "$help_text" | grep -qw mkndx; then + "$APK" --allow-untrusted mkndx --output packages.adb -- *.apk + elif printf "%s\n" "$help_text" | grep -qw index; then + "$APK" --allow-untrusted index --output packages.adb *.apk + else + "$APK" --allow-untrusted mkndx --output packages.adb -- *.apk + fi + cd /builder + fi + + # apk pre-flight: list lime-system against an empty offline root. + mkdir -p /tmp/preflight + "$APK" \ + --root /tmp/preflight \ + --no-cache \ + --allow-untrusted \ + --repositories-file /builder/repositories \ + --repository /feed/lime_packages/packages.adb \ + update >/tmp/preflight.log 2>&1 || true + echo "=== apk pre-flight update (last 25 lines) ===" + tail -n 25 /tmp/preflight.log || true + if ! "$APK" \ + --root /tmp/preflight \ + --no-cache \ + --allow-untrusted \ + --repositories-file /builder/repositories \ + --repository /feed/lime_packages/packages.adb \ + list lime-system 2>/dev/null | grep -q "^lime-system-"; then + echo "WARNING: apk pre-flight could not list lime-system" >&2 + echo "(non-fatal: apk-tools list semantics are looser than opkg; we proceed and let make image surface any real error)" >&2 + else + echo "=== Pre-flight OK: local feed is visible to apk ===" + fi + + make image \ + PROFILE="${PROFILE}" \ + BIN_DIR=/work/out \ + PACKAGES="${PACKAGES}" \ + APK_FLAGS="--allow-untrusted --repository file:///feed/lime_packages/packages.adb" + fi + + echo "=== /work/out contents (post make image) ===" + ls -la /work/out/ + find /work/out -type f -printf "%p (%s bytes)\n" + + # IB does not regenerate the staging initramfs FIT under + # CONFIG_TARGET_ROOTFS_INITRAMFS=y; it carries vanilla OpenWrt + # without our packages. Assemble it from kernel-bin + DTB + CPIO. + if [ "${BUILD_INITRAMFS:-0}" = "1" ]; then + echo "=== Repacking initramfs FIT (RAM-bootable, embedded LibreMesh) ===" + + ARCH_DIR="$(echo /builder/build_dir/target-*_musl)" + if [ ! -d "${ARCH_DIR}" ]; then + echo "ERROR: cannot locate target-_musl under /builder/build_dir" >&2 + ls -la /builder/build_dir >&2 || true + exit 1 + fi + LINUX_DIR="$(ls -d ${ARCH_DIR}/linux-*/ 2>/dev/null | head -n 1 | sed "s|/$||")" + # Skip root.orig- (IB pristine snapshot, no packages). + ROOT_DIR="$(ls -d ${ARCH_DIR}/root-*/ 2>/dev/null \ + | grep -v "/root\.orig-" \ + | head -n 1 | sed "s|/$||")" + if [ ! -d "${LINUX_DIR}" ] || [ ! -d "${ROOT_DIR}" ]; then + echo "ERROR: cannot locate linux-* / root-* under ${ARCH_DIR}" >&2 + ls -la "${ARCH_DIR}" >&2 || true + exit 1 + fi + echo " linux build dir: ${LINUX_DIR}" + echo " rootfs dir : ${ROOT_DIR}" + + # Catch the "vanilla OpenWrt CPIO" failure mode at build time. + ROOTFS_FILES="$(find "${ROOT_DIR}" -mindepth 1 2>/dev/null | wc -l)" + ROOTFS_BYTES="$(du -sb "${ROOT_DIR}" 2>/dev/null | cut -f1)" + ROOTFS_HUMAN="$(du -sh "${ROOT_DIR}" 2>/dev/null | cut -f1)" + echo " rootfs entries : ${ROOTFS_FILES}" + echo " rootfs size : ${ROOTFS_HUMAN} (${ROOTFS_BYTES} bytes)" + echo " /lib/modules/* :" + ls -la "${ROOT_DIR}/lib/modules" 2>/dev/null | head -10 || \ + echo " (no /lib/modules - kmod packages were not installed!)" + echo " kmod .ko count :" + ko_count=$(find "${ROOT_DIR}/lib/modules" -name "*.ko" 2>/dev/null | wc -l) + echo " ${ko_count} .ko files" + echo " /etc/banner :" + head -3 "${ROOT_DIR}/etc/banner" 2>/dev/null || echo " (no banner)" + echo " /etc/uci-defaults entries:" + ls "${ROOT_DIR}/etc/uci-defaults" 2>/dev/null | head -20 || \ + echo " (no uci-defaults dir)" + echo " lime-system traces:" + find "${ROOT_DIR}" -maxdepth 5 -path "*/lime/*" 2>/dev/null | head -10 || true + # Bounds catch an obviously broken rootfs (LibreMesh baseline ~1100/120). + if [ "${ROOTFS_FILES:-0}" -lt 800 ] || [ "${ko_count}" -lt 30 ]; then + echo "ERROR: rootfs at ${ROOT_DIR} is implausibly small (${ROOTFS_FILES} entries, ${ko_count} .ko)" >&2 + exit 1 + fi + + KERNEL_BIN="${LINUX_DIR}/${PROFILE}-kernel.bin" + if [ ! -f "${KERNEL_BIN}" ]; then + echo "ERROR: missing ${KERNEL_BIN}" >&2 + ls -la "${LINUX_DIR}" >&2 | head -40 + exit 1 + fi + echo " kernel-bin : ${KERNEL_BIN} ($(stat -c%s "${KERNEL_BIN}") bytes)" + # mediatek/filogic: gzipped vmlinux (1f8b0800). + # ath79: legacy uImage wrapping lzma kernel+DTB (27051956). + kernel_magic=$(od -An -tx1 -N4 "${KERNEL_BIN}" | tr -d " ") + case "${IMAGE_FORMAT}" in + fit) + if [ "${kernel_magic}" != "1f8b0800" ]; then + echo "ERROR: ${KERNEL_BIN} is not a gzip stream (magic=${kernel_magic})" >&2 + exit 1 + fi + ;; + multi-uimage|dual-tftp) + if [ "${kernel_magic}" != "27051956" ]; then + echo "ERROR: ${KERNEL_BIN} is not a uImage (magic=${kernel_magic})" >&2 + exit 1 + fi + ;; + esac + + # ath79 fuses DTB into kernel-bin; DTB + dtc lookup is FIT-only. + DTB_FILE="" + DTC_BIN="" + DTC_DIR="" + if [ "${IMAGE_FORMAT}" = "fit" ]; then + DTB_FILE="${LINUX_DIR}/image-${FIT_DTS}.dtb" + if [ ! -f "${DTB_FILE}" ]; then + echo "ERROR: missing ${DTB_FILE}" >&2 + ls "${LINUX_DIR}" 2>/dev/null | grep -E "\.dtb$" >&2 | head -40 + exit 1 + fi + DTC_BIN="$(find ${LINUX_DIR} -name dtc -type f -executable 2>/dev/null | head -n 1)" + if [ -z "${DTC_BIN}" ] || [ ! -x "${DTC_BIN}" ]; then + echo "ERROR: cannot find dtc under ${LINUX_DIR}" >&2 + find "${LINUX_DIR}" -name dtc -type f 2>/dev/null >&2 || true + exit 1 + fi + DTC_DIR="$(dirname "${DTC_BIN}")" + echo " using dtc : ${DTC_BIN}" + else + echo " IMAGE_FORMAT : ${IMAGE_FORMAT} (no separate DTB; ath79 appends DTB to kernel-bin)" + fi + + REPACK_DIR="/tmp/initramfs-repack" + rm -rf "${REPACK_DIR}" + mkdir -p "${REPACK_DIR}" + + # Optional FIT-only DTB transforms (each gated, share dtc round-trip): + # DTB_PATCH_NVMEM_MAC=1 -> inject local-mac-address into + # GMAC/WAN nodes (openwrt#22858). + # DTB_FORCE_LEGACY_PARTITIONS=1 -> rewrite SPI-NAND partitions to + # 23.05 layout for Belkin RT3200 + # layout 1.0 units. + # NOTE: this block runs under `sh -lc`, so no apostrophes in comments. + DTB_NEEDS_PATCH=0 + if [ "${DTB_PATCH_NVMEM_MAC:-0}" = "1" ]; then DTB_NEEDS_PATCH=1; fi + if [ "${DTB_FORCE_LEGACY_PARTITIONS:-0}" = "1" ]; then DTB_NEEDS_PATCH=1; fi + + if [ "${IMAGE_FORMAT}" = "fit" ] && [ "${DTB_NEEDS_PATCH}" = "1" ]; then + if ! command -v python3 >/dev/null 2>&1; then + echo "ERROR: DTB patching requires python3 inside the ImageBuilder container" >&2 + exit 1 + fi + DTB_DTS_ORIG="${REPACK_DIR}/$(basename "${DTB_FILE}").orig.dts" + DTB_DTS_STAGE1="${REPACK_DIR}/$(basename "${DTB_FILE}").stage1.dts" + DTB_DTS_STAGE2="${REPACK_DIR}/$(basename "${DTB_FILE}").stage2.dts" + DTB_PATCHED="${REPACK_DIR}/$(basename "${DTB_FILE}")" + "${DTC_BIN}" -I dtb -O dts -q -o "${DTB_DTS_ORIG}" "${DTB_FILE}" + # Stage 1: local-mac-address injection. --require-patch makes the + # build fail if the GMAC/WAN nodes the patcher targets are gone + # (e.g. an upstream kernel rename) instead of shipping a no-op patch. + if [ "${DTB_PATCH_NVMEM_MAC:-0}" = "1" ]; then + if [ ! -f /work/patch_dtb_local_mac.py ]; then + echo "ERROR: DTB_PATCH_NVMEM_MAC=1 but /work/patch_dtb_local_mac.py is missing" >&2 + ls -la /work >&2 || true + exit 1 + fi + echo "=== Patching DTB stage 1: local-mac-address (workaround openwrt#22858) ===" + python3 /work/patch_dtb_local_mac.py "${PROFILE}" \ + --in "${DTB_DTS_ORIG}" --out "${DTB_DTS_STAGE1}" --require-patch + else + cp "${DTB_DTS_ORIG}" "${DTB_DTS_STAGE1}" + fi + if [ "${DTB_FORCE_LEGACY_PARTITIONS:-0}" = "1" ]; then + if [ ! -f /work/patch_dtb_partitions.py ]; then + echo "ERROR: DTB_FORCE_LEGACY_PARTITIONS=1 but /work/patch_dtb_partitions.py is missing" >&2 + ls -la /work >&2 || true + exit 1 + fi + echo "=== Patching DTB stage 2: legacy 23.05 SPI-NAND partitioning ===" + python3 /work/patch_dtb_partitions.py \ + --in "${DTB_DTS_STAGE1}" --out "${DTB_DTS_STAGE2}" + else + cp "${DTB_DTS_STAGE1}" "${DTB_DTS_STAGE2}" + fi + "${DTC_BIN}" -I dts -O dtb -q -o "${DTB_PATCHED}" "${DTB_DTS_STAGE2}" + echo " patched DTB : ${DTB_PATCHED} ($(stat -c%s "${DTB_PATCHED}") bytes; original $(stat -c%s "${DTB_FILE}") bytes)" + # Stage 1 only adds properties; stage 2 may legitimately shrink. + orig_sz=$(stat -c%s "${DTB_FILE}") + new_sz=$(stat -c%s "${DTB_PATCHED}") + if [ "${DTB_FORCE_LEGACY_PARTITIONS:-0}" != "1" ] && [ "${new_sz}" -lt "${orig_sz}" ]; then + echo "ERROR: patched DTB shrunk from ${orig_sz} to ${new_sz} bytes - refusing to ship" >&2 + exit 1 + fi + if [ "${new_sz}" -lt 1024 ]; then + echo "ERROR: patched DTB is implausibly small (${new_sz} bytes) - dtc likely dropped data" >&2 + exit 1 + fi + DTB_FILE="${DTB_PATCHED}" + elif [ "${IMAGE_FORMAT}" != "fit" ] && [ "${DTB_NEEDS_PATCH}" = "1" ]; then + echo "ERROR: DTB patching is incompatible with IMAGE_FORMAT=${IMAGE_FORMAT}" >&2 + echo " DTB_PATCH_NVMEM_MAC=${DTB_PATCH_NVMEM_MAC:-0} DTB_FORCE_LEGACY_PARTITIONS=${DTB_FORCE_LEGACY_PARTITIONS:-0}" >&2 + exit 1 + else + echo "=== Skipping DTB patch (DTB_PATCH_NVMEM_MAC=${DTB_PATCH_NVMEM_MAC:-0} DTB_FORCE_LEGACY_PARTITIONS=${DTB_FORCE_LEGACY_PARTITIONS:-0}) ===" + fi + + # Initramfs CPIO must be uncompressed: IB reuses the sysupgrade + # kernel without RD_GZIP/LZ4/XZ. A gzipped CPIO falls back to the + # on-flash root silently. Raw newc magic 070701 is consumed directly. + BUILD_MARKER="ci-${OPENWRT_RELEASE:-unknown}-${PROFILE}-$(date -u +%Y%m%dT%H%M%SZ)" + mkdir -p "${ROOT_DIR}/etc" + printf "%s\n" "${BUILD_MARKER}" > "${ROOT_DIR}/etc/lime-build-marker" + echo " build marker : ${BUILD_MARKER}" + + # Kernel mountpoints: preinit needs /proc, /sys, /dev, /tmp to exist + # as directories so mount -t proc/sysfs/tmpfs succeeds. base-files + # creates them, but verify anyway. + for mp in proc sys dev tmp; do + if [ ! -d "${ROOT_DIR}/${mp}" ]; then + echo " creating missing /${mp} mountpoint" + mkdir -p "${ROOT_DIR}/${mp}" + fi + done + + # /init script: the kernel runs /init as PID 1. For initramfs boots + # this MUST export INITRAMFS=1 so that /lib/preinit/80_mount_root + # skips do_mount_root (which would pivot_root into the flash overlay + # and break /proc). This matches OpenWrt upstream target/linux/generic/ + # other-files/init. Without it, mount_root finds rootfs_data on flash, + # pivot_root fails on rootfs, ramoverlay loses /proc, lime-config + # never runs, and the device boots as root@(none). + rm -f "${ROOT_DIR}/init" + cat > "${ROOT_DIR}/init" <&2 + exit 1 + fi + if [ ! -e "${ROOT_DIR}/sbin/init" ] && [ ! -L "${ROOT_DIR}/sbin/init" ]; then + echo "ERROR: ${ROOT_DIR}/sbin/init does not exist; /init exec will fail" >&2 + ls -la "${ROOT_DIR}/sbin/" 2>/dev/null | head -20 >&2 || true + exit 1 + fi + + echo " packing rootfs CPIO from ${ROOT_DIR}" + ( cd "${ROOT_DIR}" && \ + find . | /builder/staging_dir/host/bin/cpio -o -H newc 2>/dev/null ) \ + > "${REPACK_DIR}/rootfs.cpio" + ls -la "${REPACK_DIR}/rootfs.cpio" + cpio_bytes=$(stat -c%s "${REPACK_DIR}/rootfs.cpio") + echo " cpio size : $((cpio_bytes / 1024 / 1024)) MiB (${cpio_bytes} bytes) vs rootfs ${ROOTFS_HUMAN}" + cpio_magic=$(head -c 6 "${REPACK_DIR}/rootfs.cpio") + if [ "${cpio_magic}" != "070701" ]; then + echo "ERROR: rootfs.cpio has unexpected magic \"${cpio_magic}\" (expected 070701 newc)" >&2 + head -c 16 "${REPACK_DIR}/rootfs.cpio" | od -c | head >&2 + exit 1 + fi + cpio_min=$((ROOTFS_BYTES * 80 / 100)) + if [ "${cpio_bytes}" -lt "${cpio_min}" ]; then + echo "ERROR: cpio size ${cpio_bytes} is <80% of rootfs ${ROOTFS_BYTES} - likely truncated/compressed" >&2 + exit 1 + fi + echo " cpio file types (top 10):" + /builder/staging_dir/host/bin/cpio -tv < "${REPACK_DIR}/rootfs.cpio" 2>/dev/null \ + | awk "{print \$1}" | sort | uniq -c | sort -rn | head -10 || true + + # `cpio -t` (no -v): GNU cpio strips leading ./ in -tv listings. + echo " /init in CPIO :" + init_paths="$(/builder/staging_dir/host/bin/cpio -t < "${REPACK_DIR}/rootfs.cpio" 2>/dev/null \ + | grep -E "^(\\./)?init$" || true)" + if [ -z "${init_paths}" ]; then + echo "ERROR: /init is missing from rootfs.cpio" >&2 + echo " Without /init the kernel will fall through to prepare_namespace()" >&2 + echo " and panic with \"Unable to mount root fs on unknown-block(0,0)\"." >&2 + echo " cpio -t entries ending in init:" >&2 + /builder/staging_dir/host/bin/cpio -t < "${REPACK_DIR}/rootfs.cpio" 2>/dev/null \ + | grep -E "(^|/)init$" | head -20 >&2 || true + exit 1 + fi + echo " ${init_paths}" + + if [ "${IMAGE_FORMAT}" = "dual-tftp" ]; then + # dual-tftp (ath79 LibreRouter): ship kernel.bin and a ramdisk + # uImage wrapping the rootfs CPIO. U-Boot TFTP-loads each to a + # distinct RAM address; `bootm ` makes U-Boot + # pass initrd_start/initrd_end to the kernel natively via the + # MIPS boot params (not via kernel command line / DT bootargs). + echo "=== dual-tftp: shipping kernel.bin + rootfs ramdisk uImage ===" + KERNEL_OUT="/work/out/openwrt-${OPENWRT_RELEASE:-}-${PROFILE}-kernel.bin" + RAMDISK_OUT="/work/out/openwrt-${OPENWRT_RELEASE:-}-${PROFILE}-rootfs.uimage" + cp "${KERNEL_BIN}" "${KERNEL_OUT}" + /builder/staging_dir/host/bin/mkimage \ + -A mips -O linux -T ramdisk -C none \ + -a 0 -e 0 \ + -n "LibreMesh rootfs ${PROFILE}" \ + -d "${REPACK_DIR}/rootfs.cpio" "${RAMDISK_OUT}" + echo " kernel : ${KERNEL_OUT} ($(stat -c%s "${KERNEL_OUT}") bytes)" + echo " ramdisk : ${RAMDISK_OUT} ($(stat -c%s "${RAMDISK_OUT}") bytes)" + /builder/staging_dir/host/bin/mkimage -l "${RAMDISK_OUT}" || true + elif [ "${IMAGE_FORMAT}" = "fit" ]; then + PATH="${DTC_DIR}:${PATH}" /builder/scripts/mkits.sh \ + -A "${FIT_ARCH}" \ + -C gzip \ + -a "${FIT_KERNEL_LOADADDR}" \ + -e "${FIT_KERNEL_LOADADDR}" \ + -c "${FIT_CONFIG}" \ + -v "OpenWrt LibreMesh ${PROFILE}" \ + -k "${KERNEL_BIN}" \ + -D "${PROFILE}" \ + -d "${DTB_FILE}" \ + -i "${REPACK_DIR}/rootfs.cpio" \ + -o "${REPACK_DIR}/initramfs.its" + + # mkits.sh has no `bootargs` flag inside `configurations`. Without + # an explicit override U-Boot falls back to chosen/bootargs which + # has `root=/dev/fit0 ...` and ignores our initramfs. + echo "=== Injecting bootargs=\"${FIT_BOOTARGS}\" into FIT config ${FIT_CONFIG} ===" + sed -i "/^[[:space:]]*${FIT_CONFIG} {[[:space:]]*\$/,/^[[:space:]]*};[[:space:]]*\$/ s|^\\([[:space:]]*\\)};[[:space:]]*\$|\\1 bootargs = \"${FIT_BOOTARGS}\";\\n\\1};|" "${REPACK_DIR}/initramfs.its" + + bootargs_count=$(grep -c "bootargs = \"${FIT_BOOTARGS}\";" "${REPACK_DIR}/initramfs.its" || true) + if [ "${bootargs_count}" -ne 1 ]; then + echo "ERROR: bootargs injection produced ${bootargs_count} matches in initramfs.its (expected exactly 1)" >&2 + echo "----- initramfs.its (configurations section) -----" >&2 + sed -n "/configurations {/,/^};/p" "${REPACK_DIR}/initramfs.its" >&2 || true + exit 1 + fi + echo " bootargs injected: ${bootargs_count} occurrence(s) (expected 1) - OK" + echo "----- initramfs.its configurations block -----" + sed -n "/configurations {/,/^};/p" "${REPACK_DIR}/initramfs.its" + + PATH="${DTC_DIR}:${PATH}" /builder/staging_dir/host/bin/mkimage \ + -f "${REPACK_DIR}/initramfs.its" \ + "${REPACK_DIR}/initramfs-libremesh.itb" + + INITRAMFS_OUT="/work/out/openwrt-${OPENWRT_RELEASE:-}-${PROFILE}-initramfs-libremesh.itb" + cp "${REPACK_DIR}/initramfs-libremesh.itb" "${INITRAMFS_OUT}" + echo "=== Initramfs FIT generated (FIT_CONFIG=${FIT_CONFIG}) ===" + ls -la "${INITRAMFS_OUT}" + /builder/staging_dir/host/bin/mkimage -l "${INITRAMFS_OUT}" \ + | grep -E "Image |Type:|Compression:|Data Size|Architecture|Load Address|Entry Point|Configuration |Kernel:|FDT:|Init Ramdisk:" || true + if ! grep -q "default = \"${FIT_CONFIG}\"" "${REPACK_DIR}/initramfs.its"; then + echo "ERROR: initramfs.its has no default config = \"${FIT_CONFIG}\"" >&2 + head -80 "${REPACK_DIR}/initramfs.its" >&2 || true + exit 1 + fi + else + # multi-uimage (ath79): IH_TYPE_MULTI image with sub0=kernel.lzma, + # sub1=rootfs.cpio. DTB is fused into kernel-bin upstream. + echo "=== Building multi-uimage (kernel.lzma + rootfs.cpio) for ath79 ===" + # mkimage prepends a 64-byte image_header; strip it. + KERNEL_LZMA="${REPACK_DIR}/kernel.lzma" + dd if="${KERNEL_BIN}" of="${KERNEL_LZMA}" bs=1 skip=64 status=none + kernel_lzma_size=$(stat -c%s "${KERNEL_LZMA}") + uimage_data_line=$(/builder/staging_dir/host/bin/mkimage -l "${KERNEL_BIN}" 2>/dev/null \ + | awk -F"[: ]+" "/Data Size/ {print \$3; exit}") + if [ -n "${uimage_data_line}" ] && [ "${kernel_lzma_size}" -ne "${uimage_data_line}" ]; then + echo "ERROR: stripped lzma size ${kernel_lzma_size} != uImage data length ${uimage_data_line}" >&2 + exit 1 + fi + # OpenWrt lzma uses `-lc1 -lp2 -pb2` so the first byte is 0x6d + # (not 0x5d). Property bytes are <= 0xe0; we reject competing magics. + kernel_lzma_first=$(od -An -tx1 -N1 "${KERNEL_LZMA}" | tr -d " ") + kernel_lzma_first_dec=$(od -An -tu1 -N1 "${KERNEL_LZMA}" | tr -d " ") + case "${kernel_lzma_first}" in + 1f|fd|28|42) + echo "ERROR: kernel payload after uImage strip starts with 0x${kernel_lzma_first} (gzip/xz/zstd/bzip2 magic, not lzma1)" >&2 + echo " OpenWrt ath79 expects an lzma1-compressed kernel (kernel-bin then append-dtb then lzma then uImage lzma)." >&2 + exit 1 + ;; + esac + if [ "${kernel_lzma_first_dec}" -gt 224 ]; then + echo "ERROR: kernel payload first byte 0x${kernel_lzma_first} (${kernel_lzma_first_dec}) exceeds lzma1 property byte max (224)" >&2 + exit 1 + fi + echo " kernel.lzma : ${KERNEL_LZMA} (${kernel_lzma_size} bytes, first byte 0x${kernel_lzma_first})" + + UIMAGE_OUT="${REPACK_DIR}/initramfs-libremesh.uimage" + /builder/staging_dir/host/bin/mkimage \ + -A "${FIT_ARCH}" \ + -O linux \ + -T multi \ + -C lzma \ + -a "${FIT_KERNEL_LOADADDR}" \ + -e "${FIT_KERNEL_LOADADDR}" \ + -n "OpenWrt LibreMesh ${PROFILE} initramfs" \ + -d "${KERNEL_LZMA}:${REPACK_DIR}/rootfs.cpio" \ + "${UIMAGE_OUT}" + + echo "=== multi-uimage generated ===" + ls -la "${UIMAGE_OUT}" + /builder/staging_dir/host/bin/mkimage -l "${UIMAGE_OUT}" \ + | grep -E "Image |Type:|Compression:|Data Size|Architecture|Load Address|Entry Point|Image [0-9]+:" || true + + if ! /builder/staging_dir/host/bin/mkimage -l "${UIMAGE_OUT}" \ + | grep -qE "(Image Type|Type:).*Multi[- ]File"; then + echo "ERROR: ${UIMAGE_OUT} is not a multi-file uImage" >&2 + /builder/staging_dir/host/bin/mkimage -l "${UIMAGE_OUT}" >&2 || true + exit 1 + fi + + INITRAMFS_OUT="/work/out/openwrt-${OPENWRT_RELEASE:-}-${PROFILE}-initramfs-libremesh.uimage" + cp "${UIMAGE_OUT}" "${INITRAMFS_OUT}" + echo "=== Initramfs multi-uimage exported ===" + ls -la "${INITRAMFS_OUT}" + fi + else + echo "=== Skipping initramfs repack (BUILD_INITRAMFS=0) ===" + echo "Target will ship the squashfs-sysupgrade artifact and is filtered out of the test-firmware matrix." + fi + ' + +echo "=== Selecting firmware artifact for ${PROFILE} ===" +ls -la "${WORK_DIR}/out/" || true + +# Verify the produced image actually carries LibreMesh by grepping the +# manifest. `make image` can silently drop PACKAGES on dep conflicts and +# ship a vanilla OpenWrt artifact otherwise. +MANIFEST_FILE="$(compgen -G "${WORK_DIR}/out/*${PROFILE}*.manifest" 2>/dev/null | head -n 1 || true)" +if [[ -z "${MANIFEST_FILE}" || ! -f "${MANIFEST_FILE}" ]]; then + echo "::error::ImageBuilder produced no .manifest for ${PROFILE} - cannot verify LibreMesh content" >&2 + find "${WORK_DIR}/out" -maxdepth 1 -type f -printf ' %p (%s bytes)\n' >&2 || true + exit 1 +fi +echo "=== Manifest: ${MANIFEST_FILE} ===" +echo "Total packages in manifest: $(wc -l < "${MANIFEST_FILE}")" +required_pkgs=(lime-system lime-proto-batadv lime-proto-anygw batctl-default) +missing=() +for pkg in "${required_pkgs[@]}"; do + if ! grep -qE "^${pkg} " "${MANIFEST_FILE}"; then + missing+=("${pkg}") + fi +done +if (( ${#missing[@]} > 0 )); then + echo "::error::Image manifest is missing required LibreMesh packages: ${missing[*]}" >&2 + echo "This means make image silently dropped them despite the opkg pre-flight pass." >&2 + echo "=== Manifest contents (lime/shared-state/batctl entries) ===" >&2 + grep -E '^(lime|shared-state|batctl|babeld|firewall)' "${MANIFEST_FILE}" >&2 || true + echo "=== First 40 manifest lines ===" >&2 + head -n 40 "${MANIFEST_FILE}" >&2 || true + exit 1 +fi +echo ">>> Manifest validation OK: lime-system + lime-proto-batadv + lime-proto-anygw + batctl-default present" +grep -E '^(lime-|shared-state-|batctl|babeld|firewall4)' "${MANIFEST_FILE}" || true + +# Pick the artifact to ship to test-firmware. +# dual-tftp -> kernel.bin + rootfs.cpio (two files). +# x86-combined -> gunzip the *-ext4-combined.img.gz disk image. +# BUILD_INITRAMFS=1 -> *-initramfs-libremesh.{itb,uimage} (FIT or multi-uimage). +# BUILD_INITRAMFS=0 -> *-squashfs-sysupgrade.{itb,bin} (IPK validation only). +DEVICE_NAME="${DEVICE_NAME:-${PROFILE}}" +SOURCE_FILE="" +if [[ "${IMAGE_FORMAT}" == "dual-tftp" ]]; then + KERNEL_SRC="$(compgen -G "${WORK_DIR}/out/*${PROFILE}-kernel.bin" 2>/dev/null | head -n 1 || true)" + RAMDISK_SRC="$(compgen -G "${WORK_DIR}/out/*${PROFILE}-rootfs.uimage" 2>/dev/null | head -n 1 || true)" + if [[ -z "${KERNEL_SRC}" || -z "${RAMDISK_SRC}" ]]; then + echo "::error::dual-tftp: expected *-kernel.bin + *-rootfs.uimage under ${WORK_DIR}/out" >&2 + find "${WORK_DIR}/out" -type f -printf ' %p (%s bytes)\n' >&2 || true + exit 1 + fi + cp "${KERNEL_SRC}" "${OUTPUT_DIR}/firmware-${DEVICE_NAME}.bin" + cp "${RAMDISK_SRC}" "${OUTPUT_DIR}/firmware-${DEVICE_NAME}.uimage" + MANIFEST_TARGET="${OUTPUT_DIR}/firmware-${DEVICE_NAME}.manifest" + cp "${MANIFEST_FILE}" "${MANIFEST_TARGET}" + echo ">>> dual-tftp kernel : ${OUTPUT_DIR}/firmware-${DEVICE_NAME}.bin ($(stat -c%s "${OUTPUT_DIR}/firmware-${DEVICE_NAME}.bin") bytes)" + echo ">>> dual-tftp ramdisk : ${OUTPUT_DIR}/firmware-${DEVICE_NAME}.uimage ($(stat -c%s "${OUTPUT_DIR}/firmware-${DEVICE_NAME}.uimage") bytes)" + echo ">>> Manifest output : ${MANIFEST_TARGET} ($(wc -l < "${MANIFEST_TARGET}") packages)" + echo ">>> Kernel sha256 : $(sha256sum "${OUTPUT_DIR}/firmware-${DEVICE_NAME}.bin" | cut -d' ' -f1)" + echo ">>> Ramdisk sha256 : $(sha256sum "${OUTPUT_DIR}/firmware-${DEVICE_NAME}.uimage" | cut -d' ' -f1)" + exit 0 +elif [[ "${IMAGE_FORMAT}" == "x86-combined" ]]; then + combined_gz="$(compgen -G "${WORK_DIR}/out/*ext4-combined.img.gz" 2>/dev/null | head -n 1 || true)" + if [[ -z "${combined_gz}" ]]; then + echo "::error::IMAGE_FORMAT=x86-combined: no *ext4-combined.img.gz under ${WORK_DIR}/out." >&2 + echo " Confirm the x86-64/generic ImageBuilder profile produced the combined recipe." >&2 + find "${WORK_DIR}/out" -type f -printf ' %p (%s bytes)\n' >&2 || true + exit 1 + fi + combined_img="${combined_gz%.gz}" + if [[ ! -f "${combined_img}" ]]; then + # OpenWrt pads the combined image past the gzip stream, so gunzip + # exits 2 with a "trailing garbage ignored" warning. The bytes are + # correct; treat 0 and 2 as success. + set +e + gunzip -kc "${combined_gz}" > "${combined_img}" 2>/tmp/gunzip.err + gz_rc=$? + set -e + if [[ $gz_rc -ne 0 && $gz_rc -ne 2 ]]; then + echo "::error::gunzip failed on ${combined_gz} (rc=$gz_rc)" >&2 + cat /tmp/gunzip.err >&2 || true + exit 1 + fi + if [[ ! -s "${combined_img}" ]]; then + echo "::error::gunzip produced empty output for ${combined_gz}" >&2 + exit 1 + fi + if [[ -s /tmp/gunzip.err ]]; then + echo ">>> gunzip notice (non-fatal): $(cat /tmp/gunzip.err)" + fi + fi + SOURCE_FILE="${combined_img}" + echo ">>> Matched x86-combined image: ${combined_gz} -> ${SOURCE_FILE} (gunzip)" +elif [[ "${BUILD_INITRAMFS}" == "1" ]]; then + case "${IMAGE_FORMAT}" in + fit) initramfs_patterns=("*${PROFILE}-initramfs-libremesh.itb" "*${PROFILE}*initramfs-libremesh.itb") ;; + multi-uimage) initramfs_patterns=("*${PROFILE}-initramfs-libremesh.uimage" "*${PROFILE}*initramfs-libremesh.uimage") ;; + dual-tftp) echo "BUG: dual-tftp should have exited earlier" >&2; exit 1 ;; + esac + for pattern in "${initramfs_patterns[@]}"; do + match="$(compgen -G "${WORK_DIR}/out/${pattern}" 2>/dev/null | head -n 1 || true)" + if [[ -n "${match}" && -f "${match}" ]]; then + SOURCE_FILE="${match}" + echo ">>> Matched initramfs pattern '${pattern}' -> ${SOURCE_FILE}" + break + fi + done + if [[ -z "${SOURCE_FILE}" ]]; then + echo "::error::BUILD_INITRAMFS=1 (IMAGE_FORMAT=${IMAGE_FORMAT}) was set but no initramfs artifact found in ${WORK_DIR}/out." >&2 + echo "The mkimage repack step likely failed silently. /work/out contents:" >&2 + find "${WORK_DIR}/out" -type f -printf ' %p (%s bytes)\n' >&2 || true + exit 1 + fi +else + for pattern in \ + "*${PROFILE}*-squashfs-sysupgrade.itb" \ + "*${PROFILE}*-squashfs-sysupgrade.bin" \ + "*${PROFILE}*-sysupgrade.bin"; do + match="$(compgen -G "${WORK_DIR}/out/${pattern}" 2>/dev/null | head -n 1 || true)" + if [[ -n "${match}" && -f "${match}" ]]; then + SOURCE_FILE="${match}" + echo ">>> Matched sysupgrade pattern '${pattern}' -> ${SOURCE_FILE} (build-image-only target, not TFTP-booted)" + break + fi + done + if [[ -z "${SOURCE_FILE}" ]]; then + echo "::error::No sysupgrade artifact found for ${PROFILE} despite a successful make image." >&2 + find "${WORK_DIR}/out" -type f -printf ' %p (%s bytes)\n' >&2 || true + exit 1 + fi +fi + +if [[ "${IMAGE_FORMAT}" == "x86-combined" ]]; then + EXTENSION="img" +else + EXTENSION="${SOURCE_FILE##*.}" +fi +TARGET_FILE="${OUTPUT_DIR}/firmware-${DEVICE_NAME}.${EXTENSION}" +cp "${SOURCE_FILE}" "${TARGET_FILE}" + +MANIFEST_TARGET="${OUTPUT_DIR}/firmware-${DEVICE_NAME}.manifest" +cp "${MANIFEST_FILE}" "${MANIFEST_TARGET}" + +echo ">>> Firmware output: ${TARGET_FILE} ($(stat -c%s "${TARGET_FILE}") bytes)" +echo ">>> Manifest output: ${MANIFEST_TARGET} ($(wc -l < "${MANIFEST_TARGET}") packages)" +echo ">>> Firmware sha256: $(sha256sum "${TARGET_FILE}" | cut -d' ' -f1)" diff --git a/tools/ci/build_summary.sh b/tools/ci/build_summary.sh new file mode 100755 index 000000000..ff4a5480f --- /dev/null +++ b/tools/ci/build_summary.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# +# Render the workflow summary to $GITHUB_STEP_SUMMARY and enforce the +# CI gate: exit non-zero if any critical job failed or was cancelled. +# +# All matrices and job results are passed as opaque env vars (instead of +# `${{ ... }}` interpolation) so the JSON values are not parsed as bash +# tokens. + +set -euo pipefail + +OUT="${GITHUB_STEP_SUMMARY:-/dev/stdout}" + +{ + printf '## Firmware build summary\n\n' + printf -- '- Trigger: `%s`\n' "${TRIGGER:-}" + printf -- '- Targets matrix: `%s`\n' "${TARGETS_MATRIX:-}" + printf -- '- Arch matrix: `%s`\n' "${ARCHS_MATRIX:-}" + printf -- '- Physical test matrix: `%s`\n' "${TEST_TARGETS_MATRIX:-}" + printf -- '- Mesh test matrix: `%s`\n' "${MESH_TEST_MATRIX:-}" + printf -- '- Mesh pairs matrix (daily): `%s`\n' "${MESH_PAIRS_MATRIX:-}" + printf -- '- QEMU single-node matrix: `%s`\n' "${QEMU_SINGLE_MATRIX:-}" + printf -- '- QEMU mesh matrix: `%s`\n' "${QEMU_MESH_MATRIX:-}" + printf -- '- Feed stage result: `%s`\n' "${BUILD_FEED_RESULT:-}" + printf -- '- Image stage result: `%s`\n' "${BUILD_IMAGE_RESULT:-}" + printf -- '- test-firmware result: `%s`\n' "${TEST_FIRMWARE_RESULT:-}" + printf -- '- test-mesh result: `%s`\n' "${TEST_MESH_RESULT:-}" + printf -- '- test-mesh-pairs result: `%s`\n' "${TEST_MESH_PAIRS_RESULT:-}" + printf -- '- test-firmware-qemu-single result: `%s`\n' "${TEST_FIRMWARE_QEMU_SINGLE_RESULT:-}" + printf -- '- test-mesh-qemu result: `%s`\n' "${TEST_MESH_QEMU_RESULT:-}" +} >> "$OUT" + +# --- CI gate --- +# `skipped` is acceptable (job condition not met for this event type). +# `failure` or `cancelled` on any stage blocks the merge. +gate_ok=true +declare -A job_results=( + [build-feed]="${BUILD_FEED_RESULT:-}" + [build-image]="${BUILD_IMAGE_RESULT:-}" + [test-firmware]="${TEST_FIRMWARE_RESULT:-}" + [test-mesh]="${TEST_MESH_RESULT:-}" + [test-mesh-pairs]="${TEST_MESH_PAIRS_RESULT:-}" + [test-firmware-qemu-single]="${TEST_FIRMWARE_QEMU_SINGLE_RESULT:-}" + [test-mesh-qemu]="${TEST_MESH_QEMU_RESULT:-}" +) + +{ + printf '\n## CI gate\n\n' + printf '| Job | Result | Pass |\n' + printf '|-----|--------|------|\n' +} >> "$OUT" + +for job in build-feed build-image test-firmware test-mesh test-mesh-pairs \ + test-firmware-qemu-single test-mesh-qemu; do + result="${job_results[$job]}" + case "$result" in + success|skipped) icon="✅" ;; + *) icon="❌"; gate_ok=false ;; + esac + printf '| %s | `%s` | %s |\n' "$job" "$result" "$icon" >> "$OUT" +done + +if $gate_ok; then + printf '\n**CI gate: PASS** — all jobs succeeded or were legitimately skipped.\n' >> "$OUT" +else + printf '\n**CI gate: FAIL** — one or more jobs failed or were cancelled.\n' >> "$OUT" + exit 1 +fi diff --git a/tools/ci/enable_kvm.sh b/tools/ci/enable_kvm.sh new file mode 100755 index 000000000..905517b7d --- /dev/null +++ b/tools/ci/enable_kvm.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# +# Make /dev/kvm world-accessible on the GitHub-hosted runner so the QEMU +# mesh launcher can use KVM acceleration. udev reload + trigger applies the +# rule to the existing device node, not just to future hot-plugs. + +set -euo pipefail + +echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \ + | sudo tee /etc/udev/rules.d/99-kvm-allow-all.rules >/dev/null +sudo udevadm control --reload-rules +sudo udevadm trigger --name-match=kvm +ls -l /dev/kvm +test -r /dev/kvm && test -w /dev/kvm diff --git a/tools/ci/lab_stage_firmware.sh b/tools/ci/lab_stage_firmware.sh new file mode 100755 index 000000000..7beb5800c --- /dev/null +++ b/tools/ci/lab_stage_firmware.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# +# Stage a single-node firmware artifact for a labgrid place. +# +# Renames `firmware-.*` under fw/ to `firmware-.*` so parallel +# jobs against units sharing the same hardware profile do not race on the +# labgrid TFTP symlink. Exports `LG_IMAGE` to GITHUB_ENV. +# +# Inputs (env vars): +# DEVICE build artifact device name +# PLACE labgrid place name +# OPENWRT_RELEASE release dimension (separates 24.10.x vs 25.12.x) +# RUN_ID github.run_id (used in TFTP path to isolate runs) +# GITHUB_ENV path to GHA env file (set by the runner) + +set -euo pipefail + +DEVICE="${DEVICE:?DEVICE required}" +PLACE="${PLACE:?PLACE required}" +OPENWRT_RELEASE="${OPENWRT_RELEASE:?OPENWRT_RELEASE required}" +RUN_ID="${RUN_ID:?RUN_ID required}" + +echo "$(date -u +'%Y-%m-%dT%H:%M:%SZ') staging firmware for $DEVICE@$OPENWRT_RELEASE on place $PLACE" + +BASE="/srv/tftp/firmwares/ci" +STAGE="$BASE/$RUN_ID/$PLACE/$OPENWRT_RELEASE" +if ! mkdir -p "$STAGE" 2>/tmp/mkdir.err; then + echo "::error::Cannot create $STAGE: $(cat /tmp/mkdir.err)" + echo "Fix on the lab host (one-time):" + echo " sudo install -d -o \$RUNNER_USER -g \$RUNNER_USER -m 0775 $BASE" + ls -ld /srv/tftp /srv/tftp/firmwares "$BASE" 2>/dev/null || true + exit 1 +fi + +shopt -s nullglob +files=(fw/firmware-"$DEVICE".*) +if [[ ${#files[@]} -eq 0 ]]; then + echo "::error::No firmware-$DEVICE.* under fw/" + ls -la fw/ || true + exit 1 +fi + +src_prefix="firmware-$DEVICE" +dst_prefix="firmware-$PLACE" +for f in "${files[@]}"; do + base="$(basename "$f")" + if [[ "$base" != "$src_prefix"* ]]; then + echo "::warning::Unexpected artifact name $base (does not start with $src_prefix); copying as-is" + cp -a "$f" "$STAGE/" + continue + fi + suffix="${base#$src_prefix}" + cp -a "$f" "$STAGE/${dst_prefix}${suffix}" +done + +# Dual-TFTP mode: build_image.sh emits firmware-.bin (kernel) and +# firmware-.uimage (ramdisk-wrapped rootfs CPIO) for devices that +# need two TFTP loads + `bootm `. +KERNEL_FILE="$STAGE/firmware-$PLACE.bin" +RAMDISK_FILE="$STAGE/firmware-$PLACE.uimage" + +if [[ -f "$KERNEL_FILE" && -f "$RAMDISK_FILE" ]]; then + echo "=== dual-tftp mode: kernel + rootfs ramdisk uImage ===" + echo "LG_IMAGE=$KERNEL_FILE" >> "${GITHUB_ENV:-/dev/null}" + echo "LG_IMAGE_INITRD=$RAMDISK_FILE" >> "${GITHUB_ENV:-/dev/null}" + echo "Staged LG_IMAGE=$KERNEL_FILE" + echo "Staged LG_IMAGE_INITRD=$RAMDISK_FILE" + echo "=== firmware sanity ===" + file "$KERNEL_FILE" || true + ls -la "$KERNEL_FILE" + sha256sum "$KERNEL_FILE" + file "$RAMDISK_FILE" || true + ls -la "$RAMDISK_FILE" + sha256sum "$RAMDISK_FILE" +else + # Single-image mode (FIT, multi-uimage, x86-combined, sysupgrade). + image_candidates=("$STAGE"/firmware-"$PLACE".*) + LG_IMAGE="" + for f in "${image_candidates[@]}"; do + case "$f" in + *.manifest|*.sha256|*.txt|*.log) ;; + *) LG_IMAGE="$f"; break ;; + esac + done + if [[ -z "$LG_IMAGE" ]]; then + echo "::error::No bootable firmware artifact under $STAGE (only sidecars?)" + ls -la "$STAGE" || true + exit 1 + fi + + echo "LG_IMAGE=$LG_IMAGE" >> "${GITHUB_ENV:-/dev/null}" + echo "Staged LG_IMAGE=$LG_IMAGE" + echo "=== firmware sanity ===" + file "$LG_IMAGE" || true + ls -la "$LG_IMAGE" + sha256sum "$LG_IMAGE" +fi + +# Print the LibreMesh subset of the manifest for at-a-glance verification. +MANIFEST="$STAGE/firmware-$PLACE.manifest" +if [[ -f "$MANIFEST" ]]; then + echo "=== manifest sidecar ($(wc -l < "$MANIFEST") packages): LibreMesh entries ===" + grep -E '^(lime-|shared-state-|batctl|babeld|firewall4)' "$MANIFEST" \ + || echo "(no LibreMesh entries; image is NOT LibreMesh)" +else + echo "::warning::No manifest sidecar at $MANIFEST" +fi diff --git a/tools/ci/lab_stage_mesh.sh b/tools/ci/lab_stage_mesh.sh new file mode 100755 index 000000000..7032aafa8 --- /dev/null +++ b/tools/ci/lab_stage_mesh.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# +# Stage firmware artifacts for a mesh test (multi-node) and run pytest. +# +# Inputs (env vars): +# MODE "full" -> N-node mesh from MESH_PLACES_JSON / MESH_DEVICES_JSON +# "pair" -> 2-node walking-chain pair from PLACE_A/DEVICE_A/PLACE_B/DEVICE_B +# OPENWRT_RELEASE release dimension +# RUN_ID github.run_id +# PAIR pair index (only when MODE=pair) +# MESH_PLACES_JSON JSON array of labgrid places (when MODE=full) +# MESH_DEVICES_JSON JSON array of artifact device names (when MODE=full) +# PLACE_A, DEVICE_A, PLACE_B, DEVICE_B (when MODE=pair) +# SRC_DIR source dir under workspace ("fw-mesh" or "fw-pair") +# LOGS_DIR relative log output dir ("mesh-logs" or "mesh-pairs-logs") +# GITHUB_WORKSPACE set by the runner + +set -euo pipefail + +MODE="${MODE:-full}" +OPENWRT_RELEASE="${OPENWRT_RELEASE:?OPENWRT_RELEASE required}" +RUN_ID="${RUN_ID:?RUN_ID required}" +SRC_DIR="${SRC_DIR:-fw-mesh}" +LOGS_DIR="${LOGS_DIR:-mesh-logs}" + +case "$MODE" in + full) + BASE="/srv/tftp/firmwares/ci/$RUN_ID/mesh/$OPENWRT_RELEASE" + mapfile -t PLACES < <(echo "${MESH_PLACES_JSON:?}" | jq -r '.[]') + mapfile -t DEVICES < <(echo "${MESH_DEVICES_JSON:?}" | jq -r '.[]') + if [[ ${#PLACES[@]} -ne ${#DEVICES[@]} ]]; then + echo "::error::places/devices length mismatch (${#PLACES[@]} vs ${#DEVICES[@]})" >&2 + exit 1 + fi + ;; + pair) + BASE="/srv/tftp/firmwares/ci/$RUN_ID/mesh-pairs/${PAIR:?}/$OPENWRT_RELEASE" + PLACES=("${PLACE_A:?}" "${PLACE_B:?}") + DEVICES=("${DEVICE_A:?}" "${DEVICE_B:?}") + ;; + *) + echo "::error::Unknown MODE=$MODE" >&2 + exit 1 + ;; +esac + +for place in "${PLACES[@]}"; do + if ! mkdir -p "$BASE/$place" 2>/tmp/mkdir.err; then + echo "::error::Cannot create $BASE/$place: $(cat /tmp/mkdir.err)" + echo "Fix on the lab host (one-time):" + echo " sudo install -d -o \$RUNNER_USER -g \$RUNNER_USER -m 0775 /srv/tftp/firmwares/ci" + ls -ld /srv/tftp /srv/tftp/firmwares /srv/tftp/firmwares/ci 2>/dev/null || true + exit 1 + fi +done + +pick_image() { + local dir="$1" base_prefix="$2" picked="" + for f in "$dir"/${base_prefix}.*; do + case "$f" in + *.manifest|*.sha256|*.txt|*.log) ;; + *) picked="$f"; break ;; + esac + done + if [[ -z "$picked" ]]; then + echo "::error::No bootable firmware under $dir (only sidecars?)" >&2 + ls -la "$dir" >&2 || true + return 1 + fi + echo "$picked" +} + +mesh_places=() +mesh_image_map_entries=() +for i in "${!PLACES[@]}"; do + place="${PLACES[$i]}" + device="${DEVICES[$i]}" + src_dir="$GITHUB_WORKSPACE/$SRC_DIR/$device" + if [[ ! -d "$src_dir" ]]; then + echo "::error::Missing artifact dir $src_dir for place=$place device=$device" >&2 + exit 1 + fi + src_prefix="firmware-${device}" + dst_prefix="firmware-${place}" + for f in "$src_dir"/${src_prefix}.*; do + base="$(basename "$f")" + suffix="${base#$src_prefix}" + cp -a "$f" "$BASE/$place/${dst_prefix}${suffix}" + done + img=$(pick_image "$BASE/$place" "${dst_prefix}") + echo "=== mesh firmware sanity (place=$place / device=$device) ===" + file "$img" || true + ls -la "$img" + sha256sum "$img" + manifest="${img%.*}.manifest" + if [[ -f "$manifest" ]]; then + echo "=== manifest for $(basename "$img"): LibreMesh entries ===" + grep -E '^(lime-|shared-state-|batctl|babeld|firewall4)' "$manifest" \ + || echo "(no LibreMesh entries; image is NOT LibreMesh)" + else + echo "::warning::No manifest sidecar at $manifest" + fi + mesh_places+=("labgrid-fcefyn-$place") + mesh_image_map_entries+=("labgrid-fcefyn-$place=$img") +done + +IFS=',' LG_MESH_PLACES_VALUE="${mesh_places[*]}" +IFS=',' LG_IMAGE_MAP_VALUE="${mesh_image_map_entries[*]}" +unset IFS +export LG_PROXY=labgrid-fcefyn +export LG_MESH_PLACES="$LG_MESH_PLACES_VALUE" +export LG_IMAGE_MAP="$LG_IMAGE_MAP_VALUE" +mkdir -p "$GITHUB_WORKSPACE/$LOGS_DIR" +echo "LG_MESH_PLACES=$LG_MESH_PLACES" +echo "LG_IMAGE_MAP=$LG_IMAGE_MAP" +cd libremesh-tests +uv run pytest tests/test_mesh.py \ + --lg-log "$GITHUB_WORKSPACE/$LOGS_DIR/" \ + --junitxml="$GITHUB_WORKSPACE/$LOGS_DIR/report.xml" \ + --log-cli-level=INFO -v +rm -rf "$BASE" diff --git a/tools/ci/patch_dtb_local_mac.py b/tools/ci/patch_dtb_local_mac.py new file mode 100755 index 000000000..ea0f0828f --- /dev/null +++ b/tools/ci/patch_dtb_local_mac.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 +"""Inject `local-mac-address` properties into the GMAC and DSA-WAN nodes of a +flattened DTS so the kernel never queries the (UBI-backed) factory NVMEM for +the device MAC. + +Background +========== +On targets whose `factory` partition lives inside a UBI volume - e.g. the +Belkin RT3200 / Linksys E8450 (`mt7622-linksys-e8450-ubi.dts`) - `gmac0` +and the WAN switch port reference NVMEM cells provided by the UBI factory +volume: + + &gmac0 { + nvmem-cells = <&macaddr_factory_7fff4>; + nvmem-cell-names = "mac-address"; + }; + +The kernel calls `of_get_mac_address()` very early during `mtk_eth_soc.probe`, +which calls into `nvmem_cell_get()` for the `mac-address` cell. UBI itself is +not yet attached at that point, so `nvmem_cell_get()` returns `-EPROBE_DEFER`. +Per OpenWrt issue [openwrt/openwrt#22858] (NVMEM core perpetual `-EPROBE_DEFER` +blocks fallback mechanisms), the deferral is never converted to `-ENODEV` +even after the underlying flash partition fails to materialise, so the +ethernet driver stays stuck forever: + + [ X.YYY] platform 1b100000.ethernet: deferred probe pending: (reason unknown) + +…and the MT7915E PCIe WiFi card never finishes its DMA-side probe either +(it shares the same factory volume for its EEPROM cell), leaving LibreMesh +without LAN, WAN nor WiFi - exactly the failure mode CI run 25004392669 hit +on `belkin_rt3200_2`. + +The kernel resolves MAC addresses via `of_get_mac_address()`, which checks +DT properties **before** falling back to NVMEM: + + 1. mac-address (DT property) <- short-circuits NVMEM + 2. local-mac-address (DT property) <- short-circuits NVMEM + 3. nvmem-cells "mac-address" <- suffers from the bug above + +So injecting a deterministic `local-mac-address` into each affected node +makes the `nvmem-cells` reference moot for MAC purposes and unblocks the +deferred probe entirely. + +What this patches +================= +Two node classes are patched in-place: + +* `mac@` whose `compatible = "mediatek,eth-mac"` AND whose existing + body declares `nvmem-cell-names = "mac-address"`. Matches `gmac0` (and + `gmac1` if present) - the SoC GMAC nodes that otherwise stall + `mtk_eth_soc.probe`. +* `port@` (DSA switch ports) whose body declares + `nvmem-cell-names = "mac-address"`. Matches the WAN port (`port@4` on + the MT7531 switch in linksys_e8450-ubi). + +Nodes that already carry `local-mac-address` or `mac-address` are skipped, +so re-running the patch is idempotent. + +MAC generation +============== +Each patched node receives a deterministic locally-administered unicast MAC +derived from `-` via SHA-256. `role` is `gmac`, +`wan-port@`, etc., so two patched nodes never collide. + +* The first byte is forced to `0x02` (locally-administered, unicast) per + IEEE 802. This guarantees the MAC will never collide with an OEM-assigned + address space and makes lab traffic easy to filter (`02:*:*:*:*:*` is + obviously synthetic). +* The remaining five bytes come from `sha256(seed)[2:12]` so different + CI rebuilds of the same board profile produce the same MAC - important + for the testbed's DHCP / DNS records that pin per-DUT addresses. + +The MAC is written into the DTS as a `[xx xx xx xx xx xx]` byte array, +which is the canonical DTS encoding for `local-mac-address` (matches what +upstream board DTSes write when they hard-code a MAC). + +CLI +=== + patch_dtb_local_mac.py [--in ] [--out ] + +If `--in`/`--out` are omitted the script reads stdin / writes stdout, which +is the form `tools/ci/build_image.sh` uses inside the ImageBuilder container +(`dtc -I dtb -O dts | python3 patch_dtb_local_mac.py | dtc +-I dts -O dtb -o `). Diagnostics go to stderr so they are visible in +the CI job log without contaminating the patched DTS on stdout. + +Exit codes +========== +0 - patch attempted; output written. `--require-patch` upgrades + "no node matched" to a hard error (exit 2). Useful in CI to fail the + build loudly if a future DTS rename silently drops the patch instead + of shipping a still-broken firmware. +2 - required patch could not be applied (only with `--require-patch`). +1 - argv / I/O error. + +[openwrt/openwrt#22858]: https://github.com/openwrt/openwrt/issues/22858 +""" + +from __future__ import annotations + +import argparse +import hashlib +import re +import sys + + +def gen_mac(seed: str) -> str: + """Return a deterministic locally-administered unicast MAC encoded as + six space-separated lowercase hex bytes (DTS `[xx xx xx xx xx xx]`).""" + digest = hashlib.sha256(seed.encode("utf-8")).digest() + octets = bytearray(digest[:6]) + # IEEE 802: bit 1 of the first octet is U/L (1 = locally administered), + # bit 0 is I/G (0 = unicast). Force `0b000000_10` = 0x02 in the low + # nibble while preserving the SHA-derived high nibble for entropy. + octets[0] = (octets[0] & 0xFC) | 0x02 + return " ".join(f"{b:02x}" for b in octets) + + +def _find_block_end(text: str, open_brace_idx: int) -> int: + """Given the index of an opening `{`, return the index just past the + matching `}`. Counts nested braces. Returns -1 if no match.""" + depth = 1 + i = open_brace_idx + 1 + while i < len(text) and depth > 0: + c = text[i] + if c == "{": + depth += 1 + elif c == "}": + depth -= 1 + i += 1 + if depth != 0: + return -1 + return i # past the closing `}` + + +# Anchor we use both as the "patch me" marker (in classify) and as the +# insertion point inside the matched block. DTS grammar (and `dtc -I dts`) +# enforce "properties must precede subnodes" within a node, so anchoring +# on an existing property guarantees our injected `local-mac-address` +# also lands in the property zone - even when the node carries +# subnodes after its property block (e.g. `port@4` on Belkin RT3200's +# MT7531 DSA switch wraps a `fixed-link {...}` after its +# `nvmem-cell-names`, which is precisely what made the previous +# "before closing `};`" placement explode with +# `dtc: Properties must precede subnodes` (CI run 25011299098). +# +# The capturing group (`([ \t]*)`) reuses the anchor's own indentation +# for the new property, so the patched DTS keeps the source's spacing +# convention (tabs vs spaces) intact. +NVMEM_MAC_ANCHOR_RE = re.compile( + r'^([ \t]*)nvmem-cell-names\s*=\s*"mac-address"\s*;[ \t]*\n', + re.MULTILINE, +) + + +def _inject_local_mac(text: str, node_re: re.Pattern, classify, + label: str, *, multi: bool = True) -> tuple[str, int]: + """Walk every node header matched by `node_re`, scope each body to its + matching brace pair, and inject a `local-mac-address` property + immediately after the `nvmem-cell-names = "mac-address";` line in the + body whenever `classify(block_text, regex_match)` returns a non-empty + seed string. + + `classify` is responsible for both filtering (return `None` to skip) + and deciding the MAC seed; it is only called for nodes that do **not** + already carry a `local-mac-address` or `mac-address` property, so it + cannot leak state between siblings even on already-patched DTSes. + + Iteration runs back-to-front so insertions never shift offsets of + earlier matches still queued for processing. + + Returns `(patched_text, count)` where `count` is the number of nodes + that received the new property. When `multi=False` only the first + match is patched (used for tests that need exactly-one semantics).""" + out = text + count = 0 + matches = list(node_re.finditer(out)) + for m in reversed(matches): + header_start = m.start() + # m.end() points just past the `{` we anchored on (regex includes it). + open_brace_idx = m.end() - 1 + if open_brace_idx < 0 or out[open_brace_idx] != "{": + continue + end = _find_block_end(out, open_brace_idx) + if end < 0: + continue + block = out[header_start:end] + # Skip nodes that already pin a MAC via DT properties - both + # `local-mac-address` and `mac-address` short-circuit + # `of_get_mac_address()` ahead of any NVMEM lookup, so injecting + # again would be redundant and (for `mac-address`) would silently + # override an OEM-pinned address. The negative lookbehind + # forbids `nvmem-cell-names = "mac-address"` from matching, since + # that string carries `mac-address` as the cell *name*, not as + # an actual MAC property. + if re.search(r"(? tuple[str, int]: + """Apply both GMAC and DSA-WAN injections to the DTS source. + + Returns `(patched_dts, total_count)`.""" + total = 0 + + # 1) `mac@` with `compatible = "mediatek,eth-mac"` AND + # `nvmem-cell-names = "mac-address"` - the SoC GMAC nodes + # `mtk_eth_soc` binds against. The unit address is the GMAC index + # (0 or 1), folded into the seed so two GMACs on the same board + # never collide. + mac_re = re.compile(r"\bmac@([0-9a-f]+)\s*\{", re.IGNORECASE) + + def _classify_eth_mac(block: str, m: re.Match) -> str | None: + if 'compatible = "mediatek,eth-mac"' not in block: + return None + if 'nvmem-cell-names = "mac-address"' not in block: + return None + return f"{profile}-gmac{m.group(1)}" + + dts, c = _inject_local_mac(dts, mac_re, _classify_eth_mac, + label="mediatek,eth-mac") + total += c + + # 2) DSA switch ports (`port@`) with + # `nvmem-cell-names = "mac-address"` - the WAN port on Belkin + # RT3200's MT7531 (port@4) is the canonical case; other vendors may + # ship MAC NVMEM cells on additional ports. + port_re = re.compile(r"\bport@([0-9a-f]+)\s*\{", re.IGNORECASE) + + def _classify_mac_port(block: str, m: re.Match) -> str | None: + if 'nvmem-cell-names = "mac-address"' not in block: + return None + return f"{profile}-port{m.group(1)}" + + dts, c = _inject_local_mac(dts, port_re, _classify_mac_port, + label="dsa-port") + total += c + + return dts, total + + +def main(argv: list[str]) -> int: + p = argparse.ArgumentParser( + description="Inject local-mac-address into MAC-bearing DTS nodes " + "so the kernel skips the broken UBI-NVMEM lookup.", + ) + p.add_argument("profile", help="OpenWrt profile name; used as MAC seed") + p.add_argument("--in", dest="in_path", default=None, + help="DTS input path (default: stdin)") + p.add_argument("--out", dest="out_path", default=None, + help="DTS output path (default: stdout)") + p.add_argument("--require-patch", action="store_true", + help="Exit 2 if no node was patched") + args = p.parse_args(argv) + + try: + if args.in_path: + with open(args.in_path, "r", encoding="utf-8") as f: + src = f.read() + else: + src = sys.stdin.read() + except OSError as exc: + print(f"[patch_dtb_local_mac] read failed: {exc}", file=sys.stderr) + return 1 + + patched, count = patch_dts(src, args.profile) + print(f"[patch_dtb_local_mac] total nodes patched: {count}", + file=sys.stderr) + + if count == 0 and args.require_patch: + print("[patch_dtb_local_mac] --require-patch: no nodes matched, " + "this means the DTS no longer contains the GMAC / WAN " + "patterns we know how to patch. Update this script.", + file=sys.stderr) + return 2 + + try: + if args.out_path: + with open(args.out_path, "w", encoding="utf-8") as f: + f.write(patched) + else: + sys.stdout.write(patched) + except OSError as exc: + print(f"[patch_dtb_local_mac] write failed: {exc}", file=sys.stderr) + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/tools/ci/patch_dtb_partitions.py b/tools/ci/patch_dtb_partitions.py new file mode 100644 index 000000000..b9bd689a8 --- /dev/null +++ b/tools/ci/patch_dtb_partitions.py @@ -0,0 +1,508 @@ +#!/usr/bin/env python3 +"""Rewrite the linksys_e8450-ubi DTB partitioning to use the legacy 23.05 +layout (separate `bl2`, `fip`, `factory`, `ubi` MTD partitions) instead +of the OpenWrt 24.10 all-UBI layout (only `bl2` + `ubi`, with `fip` and +`factory` living as static volumes inside UBI). + +Background +========== + +In February 2024 OpenWrt mainline merged "mediatek: mt7622: modernize +Linksys E8450 / Belkin RT3200 UBI build" (DEVICE_COMPAT_VERSION 1.0 → +2.0). The new mt7622-linksys-e8450-ubi.dts shipped in 24.10 declares +exactly two MTD partitions: + + bl2 0x000000 size 0x80000 (read-only) + ubi 0x080000 size 0x7f80000 (compatible = "linux,ubi"; + declares fip / factory / ubootenv + / fit volumes inline) + +That layout is correct on a device that has been migrated by +`owrt-ubi-installer v1.1.3+`, which physically reflashes BL2 + FIP and +re-creates the UBI from scratch with `fip` and `factory` as static +volumes. + +It is catastrophically wrong on a device that is still on the legacy +23.05 layout, where: + + bl2 0x000000 size 0x80000 (read-only) + fip 0x080000 size 0x140000 (read-only) + factory 0x1c0000 size 0x100000 (read-only) + ubi 0x300000 size 0x7d00000 + +When the kernel-side UBI driver attaches the 24.10 `ubi` MTD +(0x080000-0x8000000), its scan walks every PEB-sized block. The bytes +that on a layout-1.0 device hold BL31+u-boot (0x080000-0x1c0000) and +the WiFi/MAC factory data (0x1c0000-0x2c0000) carry no valid UBI EC +header, so UBI marks those blocks as candidate empty PEBs and over +time writes its volume table on top of them. The next power cycle then +KODs because BL2 cannot read BL31 from the now-overwritten `fip` +region (`ERROR: BL2: Failed to load image id 3 (-2)`). + +The lab observed exactly this: every Belkin RT3200 unit passed the +first CI run after recovery and entered KOD on the next power-cycle, +and `hexdump /dev/mtd2` (factory) on the device showed `UBI#` magic +where calibration data should sit. + +Why we patch the FIT-shipped DTB instead of migrating the hardware +================================================================= + +The proper fix is `owrt-ubi-installer v1.1.4` (Linksys E8450 / Belkin +RT3200, OpenWrt 24.10.0): it reflashes BL2, rebuilds the UBI as +layout 2.0, and migrates the factory volume into UBI. We tried to run +it on the lab Belkins (see chat transcript 2026-04-27) and it aborts +with: + + INSTALLER: cannot find Wi-Fi EEPROM data + sysrq: Trigger a crash + +because earlier 24.10 CI runs already wrote UBI metadata over the +on-flash `factory` MTD partition: there is no calibration data left to +back up, so the installer refuses to migrate. Recovering it would +require a backup taken before the corruption (which we do not have) +plus serial-console reflashing. Patching the FIT-shipped DTB avoids +the recovery entirely: we ship a DTB that tells the kernel the device +is on layout 1.0, the kernel attaches UBI strictly to 0x300000+, and +the on-flash BL31/FIP that we just put back with `mtk_uartboot` stays +untouched forever. CI runs are RAM-booted initramfs anyway, so the +device never sysupgrades and never sees DEVICE_COMPAT_VERSION +enforcement. + +The patch is one-sided: it only matters for the kernel that runs +during a CI test run. The installed-on-flash bootchain (BL2 + FIP + +recovery) stays at 23.05.5 (what `mtk_uartboot` and the +`openwrt-mediatek-mt7622-linksys_e8450-ubi-bl31-uboot.fip` recovery +images write). + +What this patches +================= + +Round-tripping the FIT-shipped DTB through dtc -I dtb -O dts gives us +a free-standing DTS where: + +* the `&snand { partitions { ... } }` block lists exactly two + partitions (`bl2`, `ubi`) with the 24.10 offsets, and the `ubi` + child node carries `compatible = "linux,ubi"` plus a `volumes {}` + subtree declaring `ubi-volume-fit`, `ubi-volume-factory`, + `ubi-volume-ubootenv` etc.; + +* the `wmac`, `wmac1`, `gmac0`, and `wan` nodes reference the + factory cells through their `nvmem-cells` properties, either as + symbolic labels (`<&eeprom_factory_0>` etc.) when the DTB carries + `__symbols__` (built with `dtc -@`), OR as numeric phandles + (`<0x44>` etc.) when it does not. OpenWrt 24.10 builds normal + kernel DTBs WITHOUT `-@`, so the references in the FIT-shipped + DTB this patcher receives are the numeric form. Empirical + evidence from CI run 25059904061: `dtc -I dtb -O dts` on + `image-mt7622-linksys-e8450-ubi.dtb` produced a DTS where every + `eeprom_factory_*` / `macaddr_factory_*` label was missing + while the corresponding `eeprom@` / `macaddr@` nodes + carried `phandle = <0xNN>;` properties. + +We replace the entire `partitions { ... }` block with the layout 1.0 +shape: + + bl2 partition@0 + fip partition@80000 (read-only) + factory partition@1c0000 (read-only) - also the parent node + for the eeprom_factory_* and + macaddr_factory_* nvmem-cells + ubi partition@300000 + +The `factory` MTD partition declares the four nvmem-cells with +their original node names (`eeprom@0`, `eeprom@5000`, +`macaddr@7fff4`, `macaddr@7fffa`) and copies each cell's +`phandle = <0xNN>;` value verbatim from the original +`ubi-volume-factory > nvmem-layout` children. Re-using the same +phandle integers means the numeric references in +`wmac`/`wmac1`/`gmac0`/`wan` keep pointing at the right node after +recompile, no symbol table needed. We also keep the upstream +labels (`eeprom_factory_0:` etc.) for human readability and so the +patch is also correct against a DTB that DOES carry symbols (for a +future OpenWrt rev that flips on `-@` for kernel DTBs). + +Idempotency and safety +====================== + +* The patcher refuses to run if the partitioning block it expects to + rewrite (a `partitions { ... }` block whose body contains + `compatible = "linux,ubi"`) is not found exactly once. That guards + against future kernel revs that move the UBI declaration somewhere + else, or against accidentally running this on an already-patched + DTB (which has no `linux,ubi` block left). + +* For each of the four expected factory cells the patcher reads the + cell's phandle from the original block (if a `phandle` property + exists; missing-phandle cells just emit the new node without one, + trusting that nothing references them). If the input DTS has + zero matches for a given cell name (e.g. `eeprom@0` vanished + entirely), the patcher hard-fails - that would mean the upstream + DTS shape changed in an incompatible way and the script needs + updating. + +CLI +=== + + patch_dtb_partitions.py [--in ] [--out ] + +stdin/stdout when `--in`/`--out` are omitted; matches the +`patch_dtb_local_mac.py` convention used elsewhere in +tools/ci/build_image.sh. +""" + +from __future__ import annotations + +import argparse +import re +import sys + + +# Layout 1.0 partitioning, transcribed verbatim from +# https://github.com/openwrt/openwrt/blob/v23.05.5/target/linux/mediatek/dts/mt7622-linksys-e8450-ubi.dts +# (the dtsi `&snand { partitions { ... } }` block) and merged with the +# `&factory` nvmem cell declarations from the same file. +# +# Why these exact reg ranges: +# bl2 0x000000-0x080000 - fixed by mt7622 boot ROM (the BootROM +# pulls BL2 from offset 0). +# fip 0x080000-0x1c0000 - BL2 v2.4 looks for the FIP at 0x80000 +# and reads up to 1.25 MiB; matches the +# 23.05.5 OpenWrt FIP size. +# factory 0x1c0000-0x2c0000 - vendor-supplied calibration EEPROM +# + dual MAC; OEM stores it here and the +# 23.05 DTS expects it here. +# ubi 0x300000-0x8000000 - leaves a 0x40000 gap (256 KiB) before +# ubi for the U-Boot environment, which +# 23.05 stores in flash but does NOT +# expose as an MTD partition (legacy +# layout matches OpenWrt mainline). +# +# Placeholders `__PHANDLE____` are replaced at rewrite +# time with the literal phandle property line copied from the +# original DTS. Each placeholder is at the END of the cell's reg +# line so a missing phandle (cell had no upstream reference) +# resolves to an empty string with no spurious whitespace artefact. +LAYOUT_1_0_BLOCK = """\ +\t\tpartitions { +\t\t\tcompatible = "fixed-partitions"; +\t\t\t#address-cells = <0x01>; +\t\t\t#size-cells = <0x01>; + +\t\t\tpartition@0 { +\t\t\t\tlabel = "bl2"; +\t\t\t\treg = <0x00 0x80000>; +\t\t\t\tread-only; +\t\t\t}; + +\t\t\tpartition@80000 { +\t\t\t\tlabel = "fip"; +\t\t\t\treg = <0x80000 0x140000>; +\t\t\t\tread-only; +\t\t\t}; + +\t\t\tfactory: partition@1c0000 { +\t\t\t\tlabel = "factory"; +\t\t\t\treg = <0x1c0000 0x100000>; +\t\t\t\tread-only; +\t\t\t\tcompatible = "nvmem-cells"; +\t\t\t\t#address-cells = <0x01>; +\t\t\t\t#size-cells = <0x01>; + +\t\t\t\teeprom_factory_0: eeprom@0 { +\t\t\t\t\treg = <0x00 0x4da8>;__PHANDLE_eeprom_0__ +\t\t\t\t}; + +\t\t\t\teeprom_factory_5000: eeprom@5000 { +\t\t\t\t\treg = <0x5000 0xe00>;__PHANDLE_eeprom_5000__ +\t\t\t\t}; + +\t\t\t\tmacaddr_factory_7fff4: macaddr@7fff4 { +\t\t\t\t\treg = <0x7fff4 0x06>;__PHANDLE_macaddr_7fff4__ +\t\t\t\t}; + +\t\t\t\tmacaddr_factory_7fffa: macaddr@7fffa { +\t\t\t\t\treg = <0x7fffa 0x06>;__PHANDLE_macaddr_7fffa__ +\t\t\t\t}; +\t\t\t}; + +\t\t\tpartition@300000 { +\t\t\t\tlabel = "ubi"; +\t\t\t\treg = <0x300000 0x7d00000>; +\t\t\t}; +\t\t}; +""" + + +# `partitions {` line, top-level (not a member like `volumes {`). The +# leading indent is captured so the replacement keeps the same column +# level as the original block, which is required for some readers that +# rely on dtc's whitespace cues (and keeps CI diffs readable). +PARTITIONS_HEADER_RE = re.compile( + r"^([ \t]*)partitions\s*\{", + re.MULTILINE, +) + + +# Marker we use to identify "this `partitions {` block is the one +# attached to the SPI-NAND". We can't anchor on the parent node name +# because dtc decompiles `&snand` to a path-based node ID we cannot +# predict (depends on SoC dtsi); but the all-UBI partitioning is the +# only place in any mediatek/mt7622 DTS where a partition declares +# `compatible = "linux,ubi"`, so that string is a reliable selector. +LINUX_UBI_MARKER = 'compatible = "linux,ubi"' + + +# Cells we must find inside the original `ubi-volume-factory > +# nvmem-layout` block, identified by their (node_name, addr_hex) +# tuple - these are deterministic across OpenWrt revs because they +# come straight from the upstream DTS source. We extract each cell's +# numeric phandle (if any) so the new MTD-backed partition can +# replicate it; numeric references downstream (`<0xNN>`) then +# resolve to the new node without depending on a `__symbols__` +# section that 24.10 does not emit. +FACTORY_CELLS: tuple[tuple[str, str], ...] = ( + ("eeprom", "0"), + ("eeprom", "5000"), + ("macaddr", "7fff4"), + ("macaddr", "7fffa"), +) + + +def _find_block_end(text: str, open_brace_idx: int) -> int: + """Return the index just past the `}` matching the `{` at + `open_brace_idx`. -1 on unbalanced input.""" + depth = 1 + i = open_brace_idx + 1 + while i < len(text) and depth > 0: + c = text[i] + if c == "{": + depth += 1 + elif c == "}": + depth -= 1 + i += 1 + if depth != 0: + return -1 + return i + + +def _find_snand_partitions(dts: str) -> tuple[int, int, str] | None: + """Locate the start..end byte range of the `partitions { ... };` + block whose body declares `compatible = "linux,ubi";`. + + Returns `(start, end, indent)` where `indent` is the whitespace + preceding the `partitions {` keyword (used to reapply the original + indentation to the replacement block). Returns None when no block + matches. Raises `RuntimeError` when more than one block matches - + that would mean a future DTS rev has multiple UBI-on-flash + partitionings and we no longer know which one is the SPI-NAND. + """ + matches: list[tuple[int, int, str]] = [] + for hdr in PARTITIONS_HEADER_RE.finditer(dts): + open_brace_idx = hdr.end() - 1 + if dts[open_brace_idx] != "{": + continue + end = _find_block_end(dts, open_brace_idx) + if end < 0: + continue + # Walk forward to the trailing `;` so we replace the full + # statement (`partitions { ... };`), not just the braces. dtc + # always emits the `;` immediately after `}` (possibly with + # intervening whitespace). + tail_end = end + while tail_end < len(dts) and dts[tail_end] in " \t": + tail_end += 1 + if tail_end < len(dts) and dts[tail_end] == ";": + tail_end += 1 + # Also consume the trailing newline if present so the splice + # leaves the surrounding DTS clean. + if tail_end < len(dts) and dts[tail_end] == "\n": + tail_end += 1 + block = dts[hdr.start():tail_end] + if LINUX_UBI_MARKER in block: + matches.append((hdr.start(), tail_end, hdr.group(1))) + if not matches: + return None + if len(matches) > 1: + raise RuntimeError( + f"Found {len(matches)} `partitions {{}}` blocks containing " + f"{LINUX_UBI_MARKER!r}; expected exactly one. Refusing to " + "guess which one is the SPI-NAND. Update this script." + ) + return matches[0] + + +def _extract_factory_phandles( + block: str, +) -> dict[tuple[str, str], int | None]: + """Walk `block` (typically the SPI-NAND `partitions { ... }` + block we are about to replace) and return a mapping from + `(node_name, addr)` to the phandle integer for each entry of + FACTORY_CELLS. + + A `None` value means "node was found but had no `phandle` + property"; this is legal - it means nothing references that cell + and we can emit the new node without an explicit phandle. + + Raises `RuntimeError` when a cell node is missing entirely, + which would mean the upstream DTS shape changed and we need to + update FACTORY_CELLS. + """ + out: dict[tuple[str, str], int | None] = {} + for name, addr in FACTORY_CELLS: + # Match `\b@ {` so addresses like `7fff4` cannot + # accidentally be matched against `1c0000` etc. dtc may emit + # the address as the bare hex digits or with a `0x` prefix in + # rare cases - anchor to the literal upstream form, which + # never carries the prefix on node units. + pattern = rf"(?", + body, + ) + if pm is None: + out[(name, addr)] = None + continue + raw = pm.group(1) + out[(name, addr)] = int(raw, 16) if raw.startswith("0x") else int(raw) + return out + + +def _format_phandle_line(value: int | None) -> str: + """Render the phandle property line that follows a cell's `reg` + declaration. Empty string when there is no phandle to copy - + the placeholder slot in LAYOUT_1_0_BLOCK is on the same line as + `reg = ...;` so this collapses cleanly. + """ + if value is None: + return "" + # Hex form mirrors what dtc emits in -I dtb -O dts output, which + # makes intermediate `.dts` files easier to diff against the + # original. The exact textual form is irrelevant once dtc -I dts + # -O dtb consumes it. + return f"\n\t\t\t\t\tphandle = <0x{value:x}>;" + + +def patch_dts(dts: str) -> tuple[str, str]: + """Apply the layout 1.0 rewrite. Returns `(patched_dts, summary)`. + + The summary string is suitable for stderr emission and lists the + range that was rewritten and which phandles were preserved. + Raises `RuntimeError` on any condition that would result in a + half-patched / silently broken DTB.""" + found = _find_snand_partitions(dts) + if found is None: + raise RuntimeError( + "No `partitions { ... }` block containing " + f"{LINUX_UBI_MARKER!r} found in the input DTS. The DTB " + "either is already on layout 1.0 (so this patch is a " + "no-op and should not have been requested), or the kernel " + "DTS shape changed and this script needs updating." + ) + start, end, _indent = found + original_block = dts[start:end] + # Extract phandles from the soon-to-be-deleted block BEFORE we + # replace it. Numeric references downstream (in wmac/gmac0/wan) + # would otherwise dangle. + phandles = _extract_factory_phandles(original_block) + template = LAYOUT_1_0_BLOCK + for (name, addr), value in phandles.items(): + placeholder = f"__PHANDLE_{name}_{addr}__" + if placeholder not in template: + raise RuntimeError( + f"Internal: template is missing placeholder " + f"{placeholder} - fix LAYOUT_1_0_BLOCK to match " + "FACTORY_CELLS." + ) + template = template.replace(placeholder, _format_phandle_line(value)) + # Defensive: any leftover placeholder means the FACTORY_CELLS + # table and the template diverged. + if "__PHANDLE_" in template: + leftover = re.findall(r"__PHANDLE_[\w]+__", template) + raise RuntimeError( + "Internal: unsubstituted placeholders left in template: " + f"{leftover}" + ) + # The original block we replace ends with `;` (statement + # terminator), so the template must do the same. Sanity-check + # ourselves so a future template edit cannot ship a broken DTS. + if not template.rstrip("\n").endswith("};"): + raise RuntimeError( + "Internal: layout 1.0 template does not end with `};` - " + "this should be unreachable, fix the template literal." + ) + # We do NOT try to re-indent the template to match the original + # block's column. dtc is whitespace-agnostic; the recompiled + # .dtb is identical regardless of indentation depth. + out = dts[:start] + template + dts[end:] + phandle_summary = ", ".join( + f"{name}@{addr}->{('skip' if v is None else f'0x{v:x}')}" + for (name, addr), v in phandles.items() + ) + summary = ( + f"rewrote partitions block at bytes {start}..{end} " + f"({end - start} bytes -> {len(template)} bytes); " + f"factory phandles: {phandle_summary}" + ) + return out, summary + + +def main(argv: list[str]) -> int: + p = argparse.ArgumentParser( + description="Rewrite linksys_e8450-ubi DTB partitioning to layout 1.0", + ) + p.add_argument("--in", dest="in_path", default=None, + help="DTS input path (default: stdin)") + p.add_argument("--out", dest="out_path", default=None, + help="DTS output path (default: stdout)") + args = p.parse_args(argv) + + try: + if args.in_path: + with open(args.in_path, "r", encoding="utf-8") as f: + src = f.read() + else: + src = sys.stdin.read() + except OSError as exc: + print(f"[patch_dtb_partitions] read failed: {exc}", file=sys.stderr) + return 1 + + try: + patched, summary = patch_dts(src) + except RuntimeError as exc: + print(f"[patch_dtb_partitions] {exc}", file=sys.stderr) + return 2 + + print(f"[patch_dtb_partitions] {summary}", file=sys.stderr) + + try: + if args.out_path: + with open(args.out_path, "w", encoding="utf-8") as f: + f.write(patched) + else: + sys.stdout.write(patched) + except OSError as exc: + print(f"[patch_dtb_partitions] write failed: {exc}", file=sys.stderr) + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/tools/ci/prepare_matrix.sh b/tools/ci/prepare_matrix.sh new file mode 100755 index 000000000..12fd562bf --- /dev/null +++ b/tools/ci/prepare_matrix.sh @@ -0,0 +1,302 @@ +#!/usr/bin/env bash +# +# Compute the GitHub Actions matrix outputs from .github/ci/targets.yml. +# +# Inputs (env vars): +# TARGETS_INPUT comma-separated devices or "all" +# RELEASES_OVERRIDE comma-separated OpenWrt releases ("" -> targets.yml default) +# PHYSICAL_RELEASES_OVERRIDE comma-separated lab releases ("" -> targets.yml default) +# MESH_COUNT_INPUT "0", "2" or "3"; ignored on pull_request (forced to 3) +# EVENT_NAME github.event_name (e.g. pull_request) +# GITHUB_OUTPUT path to GHA output file +# +# Outputs (written to GITHUB_OUTPUT): +# targets_matrix, test_targets_matrix, mesh_test_matrix, mesh_pairs_matrix, +# qemu_single_matrix, qemu_mesh_matrix, archs_matrix, lime_packages_list, +# feed_hash + +set -euo pipefail + +TARGETS_INPUT="${TARGETS_INPUT:-all}" +RELEASES_OVERRIDE="${RELEASES_OVERRIDE:-}" +PHYSICAL_RELEASES_OVERRIDE="${PHYSICAL_RELEASES_OVERRIDE:-}" +MESH_COUNT_INPUT="${MESH_COUNT_INPUT:-0}" +EVENT_NAME="${EVENT_NAME:-}" + +if grep -RIn '^PKG_MIRROR_HASH:=skip' packages/; then + echo "PKG_MIRROR_HASH:=skip is deprecated on OpenWrt 24.10; pin PKG_SOURCE_VERSION and set a real sha256." >&2 + exit 1 +fi + +all_targets_json="$(yq -r '.targets | tojson' .github/ci/targets.yml)" +default_releases_json="$(yq -r '.openwrt_releases | tojson' .github/ci/targets.yml)" +feed_branches_json="$(yq -r '.feed_branches | tojson' .github/ci/targets.yml)" +default_physical_releases_json="$(yq -r '.default_physical_releases | tojson' .github/ci/targets.yml)" +packages="$(yq -r '.packages' .github/ci/targets.yml)" + +if [[ -n "$RELEASES_OVERRIDE" ]]; then + releases_json="$( + jq -cn --arg list "$RELEASES_OVERRIDE" ' + $list | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0)) + ' + )" +else + releases_json="$default_releases_json" +fi +if [[ "$(jq 'length' <<< "$releases_json")" -eq 0 ]]; then + echo "No OpenWrt releases selected (override='$RELEASES_OVERRIDE')" >&2 + exit 1 +fi + +# Every release must have a feed_branches entry, otherwise build_image.sh +# would route the local feed against the wrong upstream branch. +missing="$( + jq -cn --argjson rel "$releases_json" --argjson fb "$feed_branches_json" ' + $rel - ($fb | keys) + ' +)" +if [[ "$(jq 'length' <<< "$missing")" -gt 0 ]]; then + echo "openwrt_releases entries missing from feed_branches map: $missing" >&2 + exit 1 +fi + +if [[ -n "$PHYSICAL_RELEASES_OVERRIDE" ]]; then + physical_releases_json="$( + jq -cn --arg list "$PHYSICAL_RELEASES_OVERRIDE" ' + $list | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0)) + ' + )" +else + physical_releases_json="$default_physical_releases_json" +fi + +if [[ "$TARGETS_INPUT" == "all" ]]; then + selected_targets="$all_targets_json" +else + selected_targets="$( + jq -c --arg list "$TARGETS_INPUT" ' + ($list | split(",") | map(gsub("^\\s+|\\s+$"; ""))) as $wanted + | map(select((.device as $d | any($wanted[]; . == $d)))) + ' <<< "$all_targets_json" + )" +fi + +if [[ "$(jq 'length' <<< "$selected_targets")" -eq 0 ]]; then + echo "No targets selected after filtering input: $TARGETS_INPUT" >&2 + exit 1 +fi + +# `targets_matrix`: cross-product of selected targets x release. Per-target +# `packages:` may include the `{{ packages_default }}` placeholder which is +# replaced by the top-level default at this step. +targets_matrix="$( + jq -c --argjson releases "$releases_json" --argjson feeds "$feed_branches_json" --arg pkg "$packages" ' + {include: ( + . as $tgts + | $releases + | map(. as $rel + | $tgts + | map(. + { + openwrt_release: $rel, + feed_branch: ($feeds[$rel] // ""), + packages: ((.packages // $pkg) | gsub("\\{\\{\\s*packages_default\\s*\\}\\}"; $pkg)), + build_initramfs: (if (.build_initramfs == true) then "1" else "0" end), + image_format: (.image_format // "fit"), + fit_arch: (.fit_arch // ""), + fit_kernel_loadaddr: (.fit_kernel_loadaddr // ""), + fit_dts: (.fit_dts // ""), + fit_config: (.fit_config // "config-1"), + fit_bootargs: (.fit_bootargs // ""), + dtb_patch_nvmem_mac: (if (.dtb_patch_nvmem_mac == true) then "1" else "0" end), + dtb_force_legacy_partitions: (if (.dtb_force_legacy_partitions == true) then "1" else "0" end), + test_firmware: (if (.test_firmware == false) then "0" else "1" end), + test_qemu: (if (.test_qemu == true) then "1" else "0" end), + test_places: (.test_places // [.device]), + uboot_interrupt_spam_sec: (.uboot_interrupt_spam_sec // "" | tostring) + }) + ) | add + )} + ' <<< "$selected_targets" +)" + +# `test_targets_matrix`: physical single-node entries, expanded by labgrid +# place. Filtered by `physical_releases_json` so build-only smoke releases +# (e.g. 25.12.2 today) do not enqueue any lab job. +test_targets_matrix="$( + jq -c --argjson phys "$physical_releases_json" ' + {include: ( + .include + | map(select(.test_firmware == "1")) + | map(select([.openwrt_release] | inside($phys))) + | map(. as $t | $t.test_places | map($t + {place: .})) + | add // [] + )} + ' <<< "$targets_matrix" +)" + +# mesh_test_matrix: physical mesh shape from MESH_COUNT_INPUT. +# pull_request cannot pass workflow inputs, so it forces N=3. +if [[ "$EVENT_NAME" == "pull_request" ]]; then + MESH_COUNT_INPUT="3" +fi +case "$MESH_COUNT_INPUT" in + "2") mesh_places_json='["openwrt_one","bananapi_bpi-r4"]' ;; + "3") mesh_places_json='["openwrt_one","bananapi_bpi-r4","belkin_rt3200_2"]' ;; + *) mesh_places_json='[]' ;; +esac +mesh_device_map_json='{"openwrt_one":"openwrt_one","bananapi_bpi-r4":"bananapi_bpi-r4","belkin_rt3200_2":"linksys_e8450","belkin_rt3200_3":"linksys_e8450"}' +if [[ "$mesh_places_json" == "[]" ]]; then + mesh_test_matrix='{"include":[]}' +else + mesh_test_matrix="$( + jq -cn --argjson phys "$physical_releases_json" --argjson places "$mesh_places_json" --argjson devmap "$mesh_device_map_json" ' + {include: ($phys | map({ + openwrt_release: ., + places: $places, + devices: ($places | map($devmap[.])) + }))} + ' + )" +fi + +# `mesh_pairs_matrix`: walking-chain matrix used by the daily cron. +# Three 2-node pairs that share devices on purpose so each unit gets two +# end-to-end mesh validations per day. Excludes belkin_rt3200_1 (in repair). +# Note: pair #3 pairs bananapi_bpi-r4 (wired-only) with belkin_rt3200_3 on +# VLAN 200; two identical-model belkins are NOT paired because LibreMesh's +# primary_mac() derives identity from eth0 and same-model devices in +# initramfs mode share that MAC, producing a mesh identity collision. +if [[ "$(jq 'length' <<< "$physical_releases_json")" -eq 0 ]]; then + mesh_pairs_matrix='{"include":[]}' +else + primary_release="$(jq -r '.[0]' <<< "$physical_releases_json")" + mesh_pairs_matrix="$( + jq -cn --arg rel "$primary_release" ' + {include: [ + {pair: 1, place_a: "belkin_rt3200_2", device_a: "linksys_e8450", + place_b: "openwrt_one", device_b: "openwrt_one", + openwrt_release: $rel}, + {pair: 2, place_a: "openwrt_one", device_a: "openwrt_one", + place_b: "bananapi_bpi-r4", device_b: "bananapi_bpi-r4", + openwrt_release: $rel}, + {pair: 3, place_a: "bananapi_bpi-r4", device_a: "bananapi_bpi-r4", + place_b: "belkin_rt3200_3", device_b: "linksys_e8450", + openwrt_release: $rel} + ]} + ' + )" +fi + +# QEMU matrices. Crossed against the FULL release list (not the physical +# filter) so QEMU validation runs on every supported branch. +qemu_single_matrix="$( + jq -c '{include: (.include | map(select(.test_qemu == "1")))}' <<< "$targets_matrix" +)" +qemu_mesh_matrix="$qemu_single_matrix" + +# `archs_matrix` drives build-feed. Keyed on (arch, release) because the SDK +# toolchain differs across releases and the resulting IPKs are not +# interchangeable. extra_feeds / extra_packages aggregate per (arch, release). +archs_matrix="$( + jq -c --argjson releases "$releases_json" ' + ([.[] | { + arch, + sdk_arch, + index_imagebuilder, + extra_feeds: (.extra_feeds // []), + extra_packages: (.extra_packages // []) + }]) as $base + | { + include: ( + $releases + | map(. as $rel + | $base + | map(. + {openwrt_release: $rel, + sdk_arch: ( + .sdk_arch | sub("-openwrt-[0-9]+\\.[0-9]+(\\.[0-9]+)?$"; + "-openwrt-" + ($rel | split(".") | .[:2] | join("."))) + )}) + ) + | add + | group_by([.arch, .openwrt_release]) + | map({ + arch: .[0].arch, + sdk_arch: .[0].sdk_arch, + index_imagebuilder: .[0].index_imagebuilder, + openwrt_release: .[0].openwrt_release, + extra_feeds: ([.[] | .extra_feeds[]] | unique | join(" ")), + extra_packages: ([.[] | .extra_packages[]] | unique | join(" ")) + }) + ) + } + ' <<< "$selected_targets" +)" + +# extra_hash: 12-char sha256 prefix over (extra_feeds, extra_packages) so the +# build-feed cache key busts whenever the upstream commit pin or package +# selection of an extra src-git feed changes. +archs_matrix="$( + entries="$(jq -c '.include[]' <<<"$archs_matrix")" + updated="[]" + while IFS= read -r entry; do + ef="$(jq -r '.extra_feeds' <<<"$entry")" + ep="$(jq -r '.extra_packages' <<<"$entry")" + eh="$(printf '%s|%s' "$ef" "$ep" | sha256sum | cut -c1-12)" + updated="$(jq -c --argjson e "$entry" --arg eh "$eh" '. + [$e + {extra_hash: $eh}]' <<<"$updated")" + done <<<"$entries" + jq -cn --argjson inc "$updated" '{include: $inc}' +)" + +# `lime_packages_list`: union of every package name listed in targets.yml, +# filtered to names that exist as `packages//Makefile`. Restricting to +# requested packages keeps gh-action-sdk's per-package compile loop bounded. +requested_pkgs="$( + yq -r '[.packages, (.targets[].packages // empty)] + | map(select(. != null)) + | join(" ")' .github/ci/targets.yml \ + | tr ' \t' '\n' \ + | sed -n 's/^[A-Za-z0-9_+-][A-Za-z0-9_+-]*$/&/p' \ + | grep -v '^-' \ + | grep -vw 'packages_default' \ + | sort -u +)" +available_lime="$( + find packages -mindepth 2 -maxdepth 2 -name Makefile -printf '%h\n' \ + | xargs -n1 basename | sort -u +)" +lime_packages_list="$( + comm -12 <(printf '%s\n' "$requested_pkgs") <(printf '%s\n' "$available_lime") \ + | tr '\n' ' ' | sed 's/[[:space:]]*$//' +)" +echo "Computed package list ($(echo "$lime_packages_list" | wc -w) packages):" +echo "$lime_packages_list" | tr ' ' '\n' + +# `feed_hash`: sha256 over package sources, build_feed.sh and the resolved +# package list. Other targets.yml or workflow edits do NOT bust the cache. +feed_hash="$( + { + find packages -type f \( -name 'Makefile' -o -path 'packages/*/files/*' -o -path 'packages/*/patches/*' -o -path 'packages/*/src/*' \) -print0 \ + | LC_ALL=C sort -z \ + | xargs -0 sha256sum + sha256sum tools/ci/build_feed.sh + printf 'lime_packages_list=%s\n' "$lime_packages_list" + } | sha256sum | cut -d' ' -f1 +)" +echo "Computed feed_hash=$feed_hash" + +if [[ -z "${GITHUB_OUTPUT:-}" ]]; then + echo "GITHUB_OUTPUT not set; printing values to stdout instead." >&2 + GITHUB_OUTPUT="/dev/stdout" +fi + +{ + echo "targets_matrix=$targets_matrix" + echo "test_targets_matrix=$test_targets_matrix" + echo "mesh_test_matrix=$mesh_test_matrix" + echo "mesh_pairs_matrix=$mesh_pairs_matrix" + echo "qemu_single_matrix=$qemu_single_matrix" + echo "qemu_mesh_matrix=$qemu_mesh_matrix" + echo "archs_matrix=$archs_matrix" + echo "lime_packages_list=$lime_packages_list" + echo "feed_hash=$feed_hash" +} >> "$GITHUB_OUTPUT"