commit 85a5bd66e0a40328f5926138cb9ed8dcf472a3bd Author: Chris Blake Date: Sun May 19 14:04:59 2024 -0500 feat: initial upload Initial public release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca6ca15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# Unifi firmware +unifi-firmware/*.bin +BuildEnv +output diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c9593c8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +# Our AIO builder docker file +FROM debian:12 + +RUN mkdir /repo +COPY ./scripts/vars.sh /vars.sh +COPY ./scripts/docker/setup_mkimage.sh /setup_mkimage.sh + +RUN apt-get update && apt-get install -yq \ + autoconf \ + bc \ + binfmt-support \ + bison \ + bsdextrautils \ + build-essential \ + cpio \ + debootstrap \ + debhelper \ + device-tree-compiler \ + dosfstools \ + dwarves \ + fakeroot \ + flex \ + genext2fs \ + git \ + kmod \ + kpartx \ + libconfuse-common \ + libconfuse-dev \ + libelf-dev \ + libncurses-dev \ + libssl-dev \ + lvm2 \ + mtools \ + parted \ + pkg-config \ + python3-dev \ + python3-pyelftools \ + python3-setuptools \ + qemu-utils \ + qemu-user-static \ + rsync \ + swig \ + unzip \ + uuid-runtime \ + wget \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && /setup_mkimage.sh \ + && rm /setup_mkimage.sh \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..da5ad17 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +.DEFAULT_GOAL := build +CONTAINER_NAME = unvr-nas:builder + +setup: + sudo modprobe loop; \ + sudo modprobe binfmt_misc + +build: setup + @set -e; \ + for file in `ls ./scripts/[0-99]*.sh`; \ + do \ + bash $${file}; \ + done \ + +clean: mountclean + sudo rm -rf $(CURDIR)/BuildEnv; \ + docker ps -a | awk '{ print $$1,$$2 }' | grep $(CONTAINER_NAME) | awk '{print $$1 }' | xargs -I {} docker rm {}; + +distclean: clean + docker rmi $(CONTAINER_NAME) -f; \ + rm -rf $(CURDIR)/downloads $(CURDIR)/output + +mountclean: + sudo umount $(CURDIR)/BuildEnv/rootfs/boot; \ + sudo umount $(CURDIR)/BuildEnv/rootfs; \ + sudo losetup -D diff --git a/README.md b/README.md new file mode 100644 index 0000000..15cc949 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# UNVR-NAS + +Firmware builder to convert your Unifi NVR Pro into an OpenMediaVault NAS appliance. + +**This repo is still under heavy development and should be considered early alpha!** + +## Supported Devices + +* UNVR Pro + +Note that the 1U UNVR is not currently supported! + +## Usage + +1. Download the required UNVRPro firmware, and place it in the unifi-firmware directory. Please see the README.md in that directory for more information. +2. Make sure your system has the required packages installed for this repo, which are: + + `docker-ce losetup wget sudo make qemu-user-static squashfs-tools` + +3. Run the tool, and sit back and wait for it to do it's thing. Depending on your computer, this may take around an hour or so. + + `make` + +4. Once done, you will have a built disk image in ./output + +## Installation + +Note that currently the install process requires UART to modify the u-boot env for booting. + +1. Build the firmware image (follow the Usage section), and then throw it on an HDD/SSD formatted to ext4. Put said HDD in the UNVR Pro as the only hard drive. +2. Hook up UART to the UNVR Pro (4 pin header on the PCB near the DC Power Backup port) +3. Boot the UNVR Pro, and press esc twice when prompted to get to the u-boot shell +4. Run the following commands to update the kernel cmdline and save the changes: + + ``` + setenv rootfs /dev/boot2 + setenv bootargsextra boot=local rw + saveenv + ``` + +5. Boot into recovery (can use the below command) + + `run bootcmdrecovery` + +6. Once recovery boots up, login with `ubnt:ubnt` or `root:ubnt`. You can also use telnet for this instead of UART if you prefer. +7. Mount your HDD with the firmware image, backup the Unifi firmware, and then flash our custom firmware to the EMMC. (below command example assumes your ext4 disk partition is at /dev/sda1) + + ``` + mount /dev/sda1 /mnt + cd /mnt + dd if=/dev/boot of=./unvrpro-emmc-backup.bin bs=4M + gunzip debian-UNVRPRO.img.gz + dd if=./debian-UNVRPRO.img of=/dev/boot bs=4M + sync; reboot + ``` + +8. At this point you can remove the HDD/SSD you used, and enjoy Debian 12 with OpenMediaVault on your UNVR Pro! + +## Removal + +To restore back to the factory UNVR-Pro firmware, you can do the following steps: + +1. Hold the "reset" button on the front while powering on to boot into recovery +2. Once the display shows it's in recovery, telnet to the IP address. At the login prompt, login with `ubnt:ubnt` or `root:ubnt`. +3. Erase the uboot env, to remove our custom boot commands. This SHOULD be mtd1/mtd2, but **PLEASE VERIFY** first with `cat /proc/mtd` to prevent bricking your device! +4. Once the uboot env's are identified, erase them: + + ``` + dd if=/dev/zero of=/dev/mtd1 + dd if=/dev/zero of=/dev/mtd2 + ``` + +5. Next, erase the EMMC so all partitions are wiped: + + ``` + /sbin/parted -s -- /dev/boot mklabel gpt + ``` + +6. Now you can use the Unifi Recovery WebUI to upload the firmware file, and restore your device. + +## Known Issues + +* Installation is Hard + * Need to simplify the install process, this should be much easier once I can get latest GPL kernel source (no more uboot env stuff) +* Touchscreen + * disks do not populate atm, cuz no grpc service mocking/replacing ustated +* OpenMediaVault + * BTRFS does not work, period + * No kernel module in UBNT kernel, need new kernel source and we can make so many things better... +* Networking + * SFP+ port works, but cloud-init doesn't configure it, so it needs manual setup +* Fans + * No service monitoring temps to adjust fan speed, so fans just stay at low spin from u-boot. Fans are on an i2c adt7475 controller +* Reset Button + * Does literally nothing ATM, not sure if it's worth having it do something or not + +## Disclaimer + +Note that since prebuild Ubiquiti software is required for this tool to work, this repo will never have prebuilt images available. This is to prevent redistribution of Ubiquiti's IP, so please do not ask! Also, by using this repo you accept all risk associated with it including but not limited to voiding your warranty and releasing all parties from any liability associated with your device and this software. diff --git a/genimage_final.cfg b/genimage_final.cfg new file mode 100644 index 0000000..eea66f3 --- /dev/null +++ b/genimage_final.cfg @@ -0,0 +1,14 @@ +image emmc.img { + hdimage { + partition-table-type = "gpt" + } + + partition boot { + bootable = "true" + image = "boot.ext4" + } + + partition rootfs { + image = "rootfs.ext4" + } +} diff --git a/genimage_initial.cfg b/genimage_initial.cfg new file mode 100644 index 0000000..a3833d2 --- /dev/null +++ b/genimage_initial.cfg @@ -0,0 +1,16 @@ +image boot.ext4 { + ext4 { + label = "boot" + use-mke2fs = true + } + size = 255M +} + +image rootfs.ext4 { + name = "debian" + ext4 { + label = "root" + use-mke2fs = true # Needed to prevent resize issues... + } + size = 3G +} diff --git a/overlay/filesystem/etc/apt/sources.list b/overlay/filesystem/etc/apt/sources.list new file mode 100644 index 0000000..b20bd1c --- /dev/null +++ b/overlay/filesystem/etc/apt/sources.list @@ -0,0 +1,6 @@ +deb http://ftp.us.debian.org/debian bookworm main contrib non-free-firmware +deb-src http://ftp.us.debian.org/debian bookworm main contrib non-free-firmware +deb http://ftp.us.debian.org/debian bookworm-updates main contrib non-free-firmware +deb-src http://ftp.us.debian.org/debian bookworm-updates main contrib non-free-firmware +deb http://security.debian.org/debian-security bookworm-security main +deb-src http://security.debian.org/debian-security bookworm-security main diff --git a/overlay/filesystem/etc/cloud/cloud.cfg b/overlay/filesystem/etc/cloud/cloud.cfg new file mode 100644 index 0000000..fe1a03f --- /dev/null +++ b/overlay/filesystem/etc/cloud/cloud.cfg @@ -0,0 +1,113 @@ +# The top level settings are used as module +# and system configuration. +# A set of users which may be applied and/or used by various modules +# when a 'default' entry is found it will reference the 'default_user' +# from the distro configuration specified below +users: + - default + +# If this is set, 'root' will not be able to ssh in and they +# will get a message to login instead as the default $user +disable_root: true + +# This will cause the set+update hostname module to not operate (if true) +preserve_hostname: false + +apt: + # This prevents cloud-init from rewriting apt's sources.list file, + # which has been a source of surprise. + preserve_sources_list: true + +# If you use datasource_list array, keep array items in a single line. +# If you use multi line array, ds-identify script won't read array items. +# Example datasource config +# datasource: +# Ec2: +# metadata_urls: [ 'blah.com' ] +# timeout: 5 # (defaults to 50 seconds) +# max_wait: 10 # (defaults to 120 seconds) + +# The modules that run in the 'init' stage +cloud_init_modules: + - migrator + - seed_random + - bootcmd + - write-files + - growpart + - resizefs + - disk_setup + - mounts + - set_hostname + - update_hostname + - update_etc_hosts + - ca-certs + - rsyslog + - users-groups + - ssh + +# The modules that run in the 'config' stage +cloud_config_modules: + - snap + - ssh-import-id + - keyboard + - locale + - set-passwords + - grub-dpkg + - apt-pipelining + - apt-configure + - ntp + - timezone + - disable-ec2-metadata + - runcmd + - byobu + +# The modules that run in the 'final' stage +cloud_final_modules: + - package-update-upgrade-install + - fan + - landscape + - lxd + - write-files-deferred + - puppet + - chef + - mcollective + - salt-minion + - reset_rmc + - refresh_rmc_and_interface + - rightscale_userdata + - scripts-vendor + - scripts-per-once + - scripts-per-boot + - scripts-per-instance + - scripts-user + - ssh-authkey-fingerprints + - keys-to-console + - install-hotplug + - phone-home + - final-message + - power-state-change + +# System and/or distro specific settings +# (not accessible to handlers/transforms) +system_info: + # This will affect which distro class gets used + distro: debian + # Default user name + that default users groups (if added/used) + default_user: + name: debian + plain_text_passwd: 'debian' + lock_passwd: False + gecos: Debian + groups: [adm, audio, cdrom, dialout, dip, floppy, netdev, plugdev, sudo, video] + sudo: ["ALL=(ALL) NOPASSWD:ALL"] + shell: /bin/bash + # Other config here will be given to the distro class and/or path classes + paths: + cloud_dir: /var/lib/cloud/ + templates_dir: /etc/cloud/templates/ + package_mirrors: + - arches: [default] + failsafe: + primary: https://deb.debian.org/debian + security: https://deb.debian.org/debian-security + ssh_svcname: ssh diff --git a/overlay/filesystem/etc/cloud/cloud.cfg.d/99-none-omv-setup.cfg b/overlay/filesystem/etc/cloud/cloud.cfg.d/99-none-omv-setup.cfg new file mode 100644 index 0000000..2484078 --- /dev/null +++ b/overlay/filesystem/etc/cloud/cloud.cfg.d/99-none-omv-setup.cfg @@ -0,0 +1,15 @@ +# configure cloud-init for None +datasource_list: [ None ] +datasource: + None: + metadata: + local-hostname: "unvr-nas" + userdata_raw: | + #cloud-config + hostname: unvr-nas + + # Setup cmds for open media vault + runcmd: + - DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true apt-get install -f -y + - omv-confdbadm populate + - omv-salt deploy run hosts diff --git a/overlay/filesystem/etc/default/chrony b/overlay/filesystem/etc/default/chrony new file mode 100644 index 0000000..c47aacb --- /dev/null +++ b/overlay/filesystem/etc/default/chrony @@ -0,0 +1,6 @@ +# This is a configuration file for /etc/init.d/chrony and +# /lib/systemd/system/chrony.service; it allows you to pass various options to +# the chrony daemon without editing the init script or service file. + +# Options to pass to chrony. +DAEMON_OPTS="-F 0" diff --git a/overlay/filesystem/etc/fstab b/overlay/filesystem/etc/fstab new file mode 100644 index 0000000..d0bd541 --- /dev/null +++ b/overlay/filesystem/etc/fstab @@ -0,0 +1,2 @@ +/dev/boot1 /boot ext4 defaults 0 1 +/dev/boot2 / ext4 defaults 0 1 diff --git a/overlay/filesystem/etc/initramfs-tools/update-initramfs.conf b/overlay/filesystem/etc/initramfs-tools/update-initramfs.conf new file mode 100644 index 0000000..a60269c --- /dev/null +++ b/overlay/filesystem/etc/initramfs-tools/update-initramfs.conf @@ -0,0 +1,20 @@ +# +# Configuration file for update-initramfs(8) +# + +# +# update_initramfs [ yes | all | no ] +# +# Default is yes +# If set to all update-initramfs will update all initramfs +# If set to no disables any update to initramfs beside kernel upgrade + +update_initramfs=no + +# +# backup_initramfs [ yes | no ] +# +# Default is no +# If set to no leaves no .bak backup files. + +backup_initramfs=no diff --git a/overlay/filesystem/etc/locale.gen b/overlay/filesystem/etc/locale.gen new file mode 100644 index 0000000..a1b8ba1 --- /dev/null +++ b/overlay/filesystem/etc/locale.gen @@ -0,0 +1 @@ +en_US.UTF-8 UTF-8 \ No newline at end of file diff --git a/overlay/filesystem/etc/modules b/overlay/filesystem/etc/modules new file mode 100644 index 0000000..a6c9853 --- /dev/null +++ b/overlay/filesystem/etc/modules @@ -0,0 +1,7 @@ +# /etc/modules: kernel modules to load at boot time. +# +# This file contains the names of kernel modules that should be loaded +# at boot time, one per line. Lines beginning with "#" are ignored. +# Parameters can be specified after the module name. + +ubnthal # used by ulcmd diff --git a/overlay/filesystem/etc/systemd/resolved.conf b/overlay/filesystem/etc/systemd/resolved.conf new file mode 100644 index 0000000..3b5829e --- /dev/null +++ b/overlay/filesystem/etc/systemd/resolved.conf @@ -0,0 +1,34 @@ +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Entries in this file show the compile time defaults. Local configuration +# should be created by either modifying this file, or by creating "drop-ins" in +# the resolved.conf.d/ subdirectory. The latter is generally recommended. +# Defaults can be restored by simply deleting this file and all drop-ins. +# +# Use 'systemd-analyze cat-config systemd/resolved.conf' to display the full config. +# +# See resolved.conf(5) for details. + +[Resolve] +# Some examples of DNS servers which may be used for DNS= and FallbackDNS=: +# Cloudflare: 1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com 2606:4700:4700::1111#cloudflare-dns.com 2606:4700:4700::1001#cloudflare-dns.com +# Google: 8.8.8.8#dns.google 8.8.4.4#dns.google 2001:4860:4860::8888#dns.google 2001:4860:4860::8844#dns.google +# Quad9: 9.9.9.9#dns.quad9.net 149.112.112.112#dns.quad9.net 2620:fe::fe#dns.quad9.net 2620:fe::9#dns.quad9.net +DNS=1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com 2606:4700:4700::1111#cloudflare-dns.com 2606:4700:4700::1001#cloudflare-dns.com +FallbackDNS=8.8.8.8#dns.google 8.8.4.4#dns.google 2001:4860:4860::8888#dns.google 2001:4860:4860::8844#dns.google +#Domains= +#DNSSEC=no +#DNSOverTLS=no +#MulticastDNS=yes +#LLMNR=yes +#Cache=yes +#CacheFromLocalhost=no +#DNSStubListener=yes +#DNSStubListenerExtra= +#ReadEtcHosts=yes +#ResolveUnicastSingleLabel=no diff --git a/overlay/filesystem/etc/systemd/system/mock-ubnt-api.service b/overlay/filesystem/etc/systemd/system/mock-ubnt-api.service new file mode 100644 index 0000000..caa589f --- /dev/null +++ b/overlay/filesystem/etc/systemd/system/mock-ubnt-api.service @@ -0,0 +1,13 @@ +[Unit] +Description=Mock Unifi API to make ulcmd happy + +[Service] +Type=simple +ExecStart=/usr/bin/mock-ubnt-api +KillMode=process +Restart=on-failure +RestartSec=2s +TimeoutStopSec=2s + +[Install] +WantedBy=multi-user.target diff --git a/overlay/filesystem/usr/bin/mock-ubnt-api b/overlay/filesystem/usr/bin/mock-ubnt-api new file mode 100755 index 0000000..c3eece6 --- /dev/null +++ b/overlay/filesystem/usr/bin/mock-ubnt-api @@ -0,0 +1,23 @@ +#!/usr/bin/python3 +from flask import Flask, jsonify +import socket + +app = Flask(__name__) + +@app.route('/api/info') +def api_info(): + print(socket.gethostname()) + payload = { + "isSetup": True, + "hostname": socket.gethostname(), + } + return jsonify(payload) + +# No controllers for you +@app.route('/api/controllers') +def api_controllers(): + payload = {} + return jsonify(payload) + +if __name__ == '__main__': + app.run(host="0.0.0.0", port=11081) diff --git a/overlay/filesystem/usr/bin/ubnt-systool b/overlay/filesystem/usr/bin/ubnt-systool new file mode 100755 index 0000000..da6332a --- /dev/null +++ b/overlay/filesystem/usr/bin/ubnt-systool @@ -0,0 +1,9 @@ +#!/bin/bash + +if [ "$1" == "poweroff" ]; then + poweroff +elif [ "$1" == "reboot" ]; then + reboot +else + echo "Unknown ubnt-systool cmd: $@" >> /tmp/ubnt-systool-unknown.log +fi diff --git a/scripts/00_prereq_check.sh b/scripts/00_prereq_check.sh new file mode 100755 index 0000000..b5daf1c --- /dev/null +++ b/scripts/00_prereq_check.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -e + +# Source our common vars +scripts_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +. ${scripts_path}/vars.sh + +debug_msg "Starting 00_prereq_check.sh" + +# Check for required utils +for bin in losetup docker wget sudo unsquashfs; do + if ! which ${bin} > /dev/null; then + error_msg "${bin} is missing! Exiting..." + exit 1 + fi +done + +# Make sure loop module is loaded +if [ ! -d /sys/module/loop ]; then + error_msg "Loop module isn't loaded into the kernel! This is REQUIRED! Exiting..." + exit 1 +fi + +# Validate FW is downloaded +if ! [ -f "${root_path}/unifi-firmware/${firmware_filename}" ]; then + echo "Error: File ${firmware_filename} does not exist in ./unifi-firmware! Exiting..." + exit 1 +fi + +# Does the checksum match? +file_md5=$(md5sum ${root_path}/unifi-firmware/${firmware_filename} | awk '{print $1}') +if [ "$file_md5" != "${firmware_md5}" ]; then + echo "Error: File ${firmware_filename} does not have the expected checksum! This is either the wrong file, or it's corrupted. Exiting..." + exit 1 +fi + +debug_msg "Finished 00_prereq_check.sh" diff --git a/scripts/01_pre_docker.sh b/scripts/01_pre_docker.sh new file mode 100755 index 0000000..7804815 --- /dev/null +++ b/scripts/01_pre_docker.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -e + +# Source our common vars +scripts_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +. ${scripts_path}/vars.sh + +debug_msg "Starting 01_pre_docker.sh" + +# Make sure our BuildEnv dir exists +if [ -d ${build_path} ]; then + error_msg "BuildEnv already exists, this isn't a clean build! Things might fail, but we're going to try!" +else + mkdir ${build_path} +fi + +# Extract the goodies +${scripts_path}/ubnt-fw-parse.py "${root_path}/unifi-firmware/${firmware_filename}" "${build_path}/fw-extract" + +# Extract the squashfs rootfs +echo "Extracting unifi rootfs as root..." +sudo unsquashfs -f -d "${build_path}/fw-extract/rootfs" "${build_path}/fw-extract/rootfs.bin" + +# Always build to pickup changes/updates/improvements +debug_msg "Building ${docker_tag}" +docker build -t ${docker_tag} ${root_path} + +debug_msg "Finished 01_pre_docker.sh" \ No newline at end of file diff --git a/scripts/03_docker.sh b/scripts/03_docker.sh new file mode 100755 index 0000000..23d22b4 --- /dev/null +++ b/scripts/03_docker.sh @@ -0,0 +1,64 @@ +#!/bin/bash +set -e + +# Source our common vars +scripts_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +. ${scripts_path}/vars.sh + +debug_msg "Starting 03_docker.sh" + +debug_msg "Doing safety checks... please enter your password for sudo if prompted..." +# Before we do anything, make our dirs, and validate they are not mounted atm. If they are, exit! +if mountpoint -q ${build_path}/rootfs/boot; then + error_msg "ERROR: ${build_path}/rootfs/boot is mounted before it should be! Cleaning up..." + sudo umount ${build_path}/rootfs/boot +fi +if mountpoint -q ${build_path}/rootfs; then + error_msg "ERROR: ${build_path}/rootfs is mounted before it should be! Cleaning up..." + sudo umount ${build_path}/rootfs +fi + +# Validate we don't have image files yet, because if we do, they may be mounted, and +# that would be REAL BAD if we overwrite em +if [ -f ${build_path}/boot.ext4 ]; then + error_msg "ERROR: ${build_path}/boot.ext4 exists already! Cleaning up..." + rm -f ${build_path}/boot.ext4 +fi +if [ -f ${build_path}/rootfs.ext4 ]; then + error_msg "ERROR: ${build_path}/rootfs.ext4 exists already! Cleaning up..." + rm -f ${build_path}/rootfs.ext4 +fi + +debug_msg "Docker: Generating rootfs and boot partitions..." +docker run --ulimit nofile=1024 --rm -v "${root_path}:/repo:Z" -it ${docker_tag} /repo/scripts/docker/run_mkimage_initial.sh + +debug_msg "Note: You might be asked for your password for losetup and mounting of said loopback devices since sudo is used..." + +debug_msg "Mounting generated block files for use with docker..." +# Mount our loopbacks +boot_loop_dev=$(sudo losetup -f --show ${build_path}/boot.ext4) +rootfs_loop_dev=$(sudo losetup -f --show ${build_path}/rootfs.ext4) + +# And now mount them to the dirs :) +mkdir -p ${build_path}/rootfs +sudo mount -t ext4 ${rootfs_loop_dev} ${build_path}/rootfs +sudo mkdir -p ${build_path}/rootfs/boot +sudo mount -t ext4 ${boot_loop_dev} ${build_path}/rootfs/boot + +# Remove stupid placeholder files -_- +sudo rm -f ${build_path}/rootfs/placeholder ${build_path}/rootfs/boot/placeholder + +# SAFETY NET - trap it, even tho we have makefile with set -e +debug_msg "Docker: debootstraping..." +trap "sudo umount ${build_path}/rootfs/boot; sudo umount ${build_path}/rootfs; sudo losetup -d ${boot_loop_dev}; sudo losetup -d ${rootfs_loop_dev}" SIGINT SIGTERM +docker run --ulimit nofile=1024 --rm --privileged --cap-add=ALL -v /dev:/dev -v "${root_path}:/repo:Z" -it ${docker_tag} /repo/scripts/docker/run_debootstrap.sh + +debug_msg "Note: You might be asked for your password for losetup and umount since we are cleaning up mounts..." +debug_msg "Cleaning up..." +sudo umount ${build_path}/rootfs/boot +sudo umount ${build_path}/rootfs +sudo losetup -d ${boot_loop_dev} +sudo losetup -d ${rootfs_loop_dev} +rm -rf ${build_path}/rootfs + +debug_msg "Finished 03_docker.sh" diff --git a/scripts/04_post_docker.sh b/scripts/04_post_docker.sh new file mode 100755 index 0000000..a27f315 --- /dev/null +++ b/scripts/04_post_docker.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +# Source our common vars +scripts_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +. ${scripts_path}/vars.sh + +debug_msg "Starting 04_post_docker.sh" + +if [ -d ${build_path}/final ]; then + debug_msg "WARNING: final builddir already exists! Cleaning up..." + rm -rf ${build_path}/final +fi +mkdir -p ${build_path}/final + +# Kick off the docker to do the magics for us, since we need genimage +docker run --rm -v "${root_path}:/repo:Z" -it ${docker_tag} /repo/scripts/docker/run_mkimage_final.sh + +# Just create our final dir and move bits over +TIMESTAMP=`date +%Y%m%d-%H%M` +mkdir -p ${root_path}/output/${TIMESTAMP} +mv ${build_path}/final/debian*.img.gz ${root_path}/output/${TIMESTAMP}/ +sudo rm -rf ${build_path} # Be gone, we done buildin! :) + +debug_msg "Finished 04_post_docker.sh" \ No newline at end of file diff --git a/scripts/docker/bootstrap/001-bootstrap b/scripts/docker/bootstrap/001-bootstrap new file mode 100755 index 0000000..7c3884b --- /dev/null +++ b/scripts/docker/bootstrap/001-bootstrap @@ -0,0 +1,62 @@ +#!/bin/bash +set -e + +echo "Starting 001-bootstrap within chroot!" + +export DEBIAN_FRONTEND=noninteractive +export APT_LISTCHANGES_FRONTEND=none + +# Conf debconf +debconf-set-selections /debconf.set +rm -f /debconf.set + +# Initial package install +apt-get clean +apt-get update +apt-mark hold linux-image-* # We do not want these, as we run our own kernel! + +# Setup ulcmd +systemctl enable ulcmd +systemctl enable mock-ubnt-api + +# Now that we have our wanted kernel in place, do the rest of our installs +apt-get -o Dpkg::Options::="--force-confold" -y --allow-downgrades \ + --allow-remove-essential --allow-change-held-packages install cloud-init \ + bsdextrautils git binutils ca-certificates e2fsprogs haveged parted curl \ + locales console-common openssh-server less vim net-tools wireguard-tools \ + ntpsec u-boot-tools wget u-boot-menu initramfs-tools python3-flask gnupg + +# Locale gen +locale-gen + +# Setup OMV repo +wget --quiet --output-document=- https://packages.openmediavault.org/public/archive.key | gpg --dearmor --yes --output "/usr/share/keyrings/openmediavault-archive-keyring.gpg" +cat <> /etc/apt/sources.list.d/openmediavault.list +deb [signed-by=/usr/share/keyrings/openmediavault-archive-keyring.gpg] https://packages.openmediavault.org/public sandworm main +# deb [signed-by=/usr/share/keyrings/openmediavault-archive-keyring.gpg] https://downloads.sourceforge.net/project/openmediavault/packages sandworm main +## This software is not part of OpenMediaVault, but is offered by third-party +## developers as a service to OpenMediaVault users. +deb [signed-by=/usr/share/keyrings/openmediavault-archive-keyring.gpg] https://packages.openmediavault.org/public sandworm partner +# deb [signed-by=/usr/share/keyrings/openmediavault-archive-keyring.gpg] https://downloads.sourceforge.net/project/openmediavault/packages sandworm partner +EOF + +# Install OMV +apt-get update +apt-get --yes --auto-remove --show-upgraded \ + --allow-downgrades --allow-change-held-packages \ + --no-install-recommends \ + --option DPkg::Options::="--force-confdef" \ + --option DPkg::Options::="--force-confold" \ + install openmediavault openmediavault-md || true # We "fail" all apt cmds from here on til we boot on HW + +# Disable systemd-networkd +systemctl disable systemd-networkd +systemctl disable systemd-networkd-wait-online +systemctl mask systemd-networkd +systemctl mask systemd-networkd-wait-online + +# Cleanup stuff we don't want floating around +apt-get autoclean || true +apt-get --purge -y autoremove || true +rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /etc/resolv.conf +rm -rf /var/lib/dbus/machine-id /etc/machine-id # Nuke machine IDs diff --git a/scripts/docker/run_debootstrap.sh b/scripts/docker/run_debootstrap.sh new file mode 100755 index 0000000..7e46036 --- /dev/null +++ b/scripts/docker/run_debootstrap.sh @@ -0,0 +1,61 @@ +#!/bin/bash +set -e + +docker_scripts_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +scripts_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" +. ${scripts_path}/vars.sh + +# Exports +export PATH=${build_path}/toolchain/${toolchain_bin_path}:${PATH} +export GCC_COLORS=auto +export CROSS_COMPILE=${toolchain_cross_compile} +export ARCH=arm64 +export DEBIAN_FRONTEND=noninteractive +export DEBCONF_NONINTERACTIVE_SEEN=true + +# CD into our rootfs mount, and starts the fun! +cd ${build_path}/rootfs +debootstrap --no-check-gpg --foreign --arch=${deb_arch} --include=apt-transport-https ${deb_release} ${build_path}/rootfs ${deb_mirror} +cp /usr/bin/qemu-aarch64-static usr/bin/ +chroot ${build_path}/rootfs /debootstrap/debootstrap --second-stage + +# Copy over our kernel modules and kernel +mv -f "${build_path}/fw-extract/rootfs/lib/modules" ${build_path}/rootfs/lib +cp "${build_path}/fw-extract/kernel.bin" "${build_path}/rootfs/boot/uImage" + +# Copy over our overlay if we have one +if [[ -d ${root_path}/overlay/${fs_overlay_dir}/ ]]; then + echo "Applying ${fs_overlay_dir} overlay" + cp -R ${root_path}/overlay/${fs_overlay_dir}/* ./ +fi + +# Apply our part UUIDs to fstab +sed -i "s|BOOTUUIDPLACEHOLDER|$(blkid -o value -s UUID ${build_path}/boot.ext4)|g" ${build_path}/rootfs/etc/fstab +sed -i "s|ROOTUUIDPLACEHOLDER|$(blkid -o value -s UUID ${build_path}/rootfs.ext4)|g" ${build_path}/rootfs/etc/fstab + +# Hostname +echo "${distrib_name}" > ${build_path}/rootfs/etc/hostname +echo "127.0.1.1 ${distrib_name}" >> ${build_path}/rootfs/etc/hosts + +# Console settings +echo "console-common console-data/keymap/policy select Select keymap from full list +console-common console-data/keymap/full select us +" > ${build_path}/rootfs/debconf.set + +# Copy over stuff for ulcmd, this is hacky, but that's this ENTIRE repo for you +mv "${build_path}/fw-extract/rootfs/lib/systemd/system/ulcmd.service" "${build_path}/rootfs/lib/systemd/system/ulcmd.service" +mv "${build_path}/fw-extract/rootfs/usr/bin/ulcmd" "${build_path}/rootfs/usr/bin/ulcmd" +mkdir -p "${build_path}/rootfs/usr/lib/ubnt-fw/" +for file in libgrpc++.so.1 libgrpc.so.10 libprotobuf.so.23 \ + libssl.so.1.1 libcrypto.so.1.1 libabsl*.so.20200923 libatomic.so.1; do + mv ${build_path}/fw-extract/rootfs/usr/lib/aarch64-linux-gnu/${file} "${build_path}/rootfs/usr/lib/ubnt-fw/" +done +sed -i 's|Type=simple|Type=simple\nEnvironment="LD_LIBRARY_PATH=/usr/lib/ubnt-fw"|g' "${build_path}/rootfs/lib/systemd/system/ulcmd.service" + +# Kick off bash setup script within chroot +cp ${docker_scripts_path}/bootstrap/001-bootstrap ${build_path}/rootfs/bootstrap +chroot ${build_path}/rootfs /bootstrap +rm ${build_path}/rootfs/bootstrap + +# Final cleanup +rm ${build_path}/rootfs/usr/bin/qemu-aarch64-static \ No newline at end of file diff --git a/scripts/docker/run_mkimage_final.sh b/scripts/docker/run_mkimage_final.sh new file mode 100755 index 0000000..269fafc --- /dev/null +++ b/scripts/docker/run_mkimage_final.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -e + +scripts_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" +. ${scripts_path}/vars.sh + +# Tempdir for root +GENIMAGE_ROOT=$(mktemp -d) + +# setup and move bits +mkdir -p ${build_path}/final +cp ${build_path}/boot.ext4 ${build_path}/final/ +cp ${build_path}/rootfs.ext4 ${build_path}/final/ +cp ${root_path}/genimage_final.cfg ${build_path}/genimage.cfg + +# We get to gen an image per board, YAY! +echo "Generating disk image" +genimage \ + --rootpath "${GENIMAGE_ROOT}" \ + --tmppath "/tmp/genimage-initial-tmppath" \ + --inputpath "${build_path}/final" \ + --outputpath "${build_path}/final" \ + --config "${build_path}/genimage.cfg" +mv ${build_path}/final/emmc.img ${build_path}/final/debian-UNVRPRO.img +gzip ${build_path}/final/debian-UNVRPRO.img +rm -rf /tmp/genimage-initial-tmppath # Cleanup + +# Cleanup +rm ${build_path}/final/boot.ext4 ${build_path}/final/rootfs.ext4 ${build_path}/genimage.cfg \ No newline at end of file diff --git a/scripts/docker/run_mkimage_initial.sh b/scripts/docker/run_mkimage_initial.sh new file mode 100755 index 0000000..4314f4a --- /dev/null +++ b/scripts/docker/run_mkimage_initial.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e + +scripts_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" +. ${scripts_path}/vars.sh + +# Tempdir for root +GENIMAGE_ROOT=$(mktemp -d) + +# "fake" file so generation is happy +touch ${GENIMAGE_ROOT}/placeholder + +# Generate our boot and rootfs disk images +genimage \ + --rootpath "${GENIMAGE_ROOT}" \ + --tmppath "/tmp/genimage-initial-tmppath" \ + --inputpath "${build_path}" \ + --outputpath "${build_path}" \ + --config "${root_path}/genimage_initial.cfg" diff --git a/scripts/docker/setup_mkimage.sh b/scripts/docker/setup_mkimage.sh new file mode 100755 index 0000000..0a5bc45 --- /dev/null +++ b/scripts/docker/setup_mkimage.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +# setup_mkimage is special, as it's copied to the dockerfile and ran +# same with vars.sh which we put in place +. /vars.sh + +cd /usr/src +echo "Downloading genimage..." +wget ${genimage_src} -O /usr/src/${genimage_filename} + +echo "Extracting genimage..." +tar -xJf /usr/src/${genimage_filename} + +echo "Building genimage..." +cd ./${genimage_repopath} +./configure +make + +echo "Installing genimage..." +make install + +echo "Cleaning up..." +rm -rf /vars.sh # We wanna use it n burn it +rm -rf /usr/src/${genimage_repopath}* + +exit 0 diff --git a/scripts/ubnt-fw-parse.py b/scripts/ubnt-fw-parse.py new file mode 100755 index 0000000..e0995c4 --- /dev/null +++ b/scripts/ubnt-fw-parse.py @@ -0,0 +1,92 @@ +#!/usr/bin/python3 +import argparse +import binascii +import mmap +import sys +import zlib + +from io import BytesIO + +from pathlib import Path + +def main(fwfile: str, savedir: str): + # Does the file exist? + if not Path(fwfile).is_file(): + print(f"Error: {fwfile} is not a file! Exiting...") + sys.exit(1) + + # Does our save dir exist? + if not Path(savedir).is_dir(): + print(f"Warning: {savedir} is not a directory, attempting to create it...") + Path(savedir).mkdir(parents=True) + + # Start parsing the OTA file + with open(fwfile, 'rb+') as f: + # Read from disk, not memory + mm = mmap.mmap(f.fileno(), 0) + + ubntheader = mm.read(0x104) # Read header in + ubntheadercrc = int.from_bytes(mm.read(0x4)) # Read header CRC, convert to int + + # Do we have the UBNT header? + if ubntheader[0:4].decode("utf-8") != "UBNT": + print(f"Error: {fwfile} is missing the UBNT header! Is this the right firmware file?") + sys.exit(1) + + # Is the header CRC valid? + if zlib.crc32(ubntheader) != ubntheadercrc: + print(f"Error: {fwfile} has in incorrect CRC for it's header! Please re-download the file!") + sys.exit(1) + + # If we are here, that's a great sign :) + ubnt_fw_ver_string = ubntheader[4:].decode("utf-8") + print(f"Loaded in firmware file {ubnt_fw_ver_string}") + + # Start parsing out all of the files in the OTA + #files = [] + fcount = 1 + while True: + file_header_offset = mm.find(b'\x46\x49\x4C\x45') # FILE in hex bye string + # Are we done scanning the file? + if file_header_offset == -1: + break + + # We found one, seek to it + mm.seek(file_header_offset) + file_header = mm.read(0x38) # Entire header + file_position = file_header[39] # Increments with files read, can be used to validate this is a file for us + file_location = file_header_offset+0x38 # header location - header = data :) + + # Is this a VALID file?! + if fcount == file_position: + file_name = file_header[4:33].decode("utf-8").rstrip('\x00') # Name/type of FILE + file_length = int(file_header[48:52].hex(), 16) + print(f"{file_name} is at offset {"0x%0.2X" % file_location}, {file_length} bytes") + # print(int(file_header[52:56].hex(), 16)) # Maybe reserved memory or partition size? We don't use this tho + #files.extend([(file_position, file_name, file_location, file_length)]) + fcount = fcount+1 # Increment on find! + + fcontents = mm.read(file_length) # Read into memory + file_footer_crc32 = mm.read(0x8)[0:4] # Read in tailing 8 bytes (crc32) footer, but we only want the first 4 + + # Does our calculated crc32 match the unifi footer in the img? + if hex(zlib.crc32(file_header + fcontents)).lstrip('0x') != file_footer_crc32.hex().lstrip('0'): + print(f"Error: Contents of {file_name} does not match the Unifi CRC! Please re-download the file!") + sys.exit(1) + + # Write file out since our mmap position is now AT the data, and we parsed the length + with open(f"{savedir}/{file_name}.bin", "wb") as wf: + wf.write(fcontents) # Write out file + print(f"{file_name} has been written to {savedir}/{file_name}.bin") + + del fcontents # Cleanup memory for next run + + print(f"Finished extracting the contents of {fwfile}, enjoy!") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("ubnt-fw-parse for Dream Machines") + parser.add_argument("file", help="The Ubiquiti firmware file to parse", type=str) + parser.add_argument("savedir", help="The directory to save the parsed files to", type=str) + args = parser.parse_args() + main(args.file, args.savedir) diff --git a/scripts/vars.sh b/scripts/vars.sh new file mode 100755 index 0000000..93cfc10 --- /dev/null +++ b/scripts/vars.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +root_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" +build_path="${root_path}/BuildEnv" + +# Docker image name +docker_tag=unvr-nas:builder + +# Expected UNVR Firmware and hash +firmware_filename="f449-UNVRPRO-4.0.3-fdec2c4f-1855-4eb6-8711-e22f8f904922.bin" +firmware_md5="5dcdc03bdec1524767007fcd12e81777" + +# Genimage +genimage_src="https://github.com/pengutronix/genimage/releases/download/v16/genimage-16.tar.xz" +genimage_filename="$(basename ${genimage_src})" +genimage_repopath="${genimage_filename%.tar.xz}" + +# Distro +distrib_name="debian" +#deb_mirror="http://ftp.us.debian.org/debian" +deb_mirror="http://debian.uchicago.edu/debian" +deb_release="bookworm" +deb_arch="arm64" +fs_overlay_dir="filesystem" + +debug_msg () { + BLU='\033[0;32m' + NC='\033[0m' + printf "${BLU}${@}${NC}\n" +} + +error_msg () { + BLU='\033[0;31m' + NC='\033[0m' + printf "${BLU}${@}${NC}\n" +} diff --git a/unifi-firmware/README.md b/unifi-firmware/README.md new file mode 100644 index 0000000..75064e0 --- /dev/null +++ b/unifi-firmware/README.md @@ -0,0 +1,6 @@ +# unifi-firmware + +Please place the UNVR-Pro firmware in this directory. + +Name: f449-UNVRPRO-4.0.3-fdec2c4f-1855-4eb6-8711-e22f8f904922.bin +MD5Sum: 5dcdc03bdec1524767007fcd12e81777 \ No newline at end of file