feat: initial upload

Initial public release
This commit is contained in:
Chris Blake 2024-05-19 14:04:59 -05:00
commit 85a5bd66e0
30 changed files with 943 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
# Unifi firmware
unifi-firmware/*.bin
BuildEnv
output

49
Dockerfile Normal file
View file

@ -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

26
Makefile Normal file
View file

@ -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

99
README.md Normal file
View file

@ -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.

14
genimage_final.cfg Normal file
View file

@ -0,0 +1,14 @@
image emmc.img {
hdimage {
partition-table-type = "gpt"
}
partition boot {
bootable = "true"
image = "boot.ext4"
}
partition rootfs {
image = "rootfs.ext4"
}
}

16
genimage_initial.cfg Normal file
View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -0,0 +1,2 @@
/dev/boot1 /boot ext4 defaults 0 1
/dev/boot2 / ext4 defaults 0 1

View file

@ -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

View file

@ -0,0 +1 @@
en_US.UTF-8 UTF-8

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

37
scripts/00_prereq_check.sh Executable file
View file

@ -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"

28
scripts/01_pre_docker.sh Executable file
View file

@ -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"

64
scripts/03_docker.sh Executable file
View file

@ -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"

25
scripts/04_post_docker.sh Executable file
View file

@ -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"

View file

@ -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 <<EOF >> /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

View file

@ -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

View file

@ -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

View file

@ -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"

27
scripts/docker/setup_mkimage.sh Executable file
View file

@ -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

92
scripts/ubnt-fw-parse.py Executable file
View file

@ -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)

36
scripts/vars.sh Executable file
View file

@ -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"
}

6
unifi-firmware/README.md Normal file
View file

@ -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