To keep TrueNAS Core running with security fixes, we need to be able to apply patches and rebuild some components from time to time. FreeBSD’s native build tools are already present on Core 13.3, but some userspace components, including ZFS’s, require build tools installed from packages. Public mirrors are dropping unsupported FreeBSD releases, so let’s host our own in the LAN.

This opus describes how to do precisely that. My 13.3 collection now contains 34,346 packages and takes about 120 GiB.

Saving the catalog

Set upstream to the collection directory’s full URL, including the protocol, hostname, and path. The URL below is the only one I found that still serves both the FreeBSD 13 release_4 collection and its bootstrap signature1. archive is the local directory.

On the TrueNAS host, run (replace tank with your pool name; we’ll create the package-mirror dataset there):

sudo sh
midclt call pool.dataset.create '{"name":"tank/package-mirror"}'
archive=/mnt/tank/package-mirror/13.3
upstream='https://mirror.yandex.ru/mirrors/freebsd-pkg/FreeBSD:13:amd64/release_4'
mkdir -p "$archive/control/repos" "$archive/control/db" "$archive/control/cache" "$archive/repository"

for file in meta.conf data.pkg packagesite.pkg; do
    fetch -o "$archive/repository/$file" "$upstream/$file"
done

Configure a separate database, cache, and repository for pkg, then import the catalog:

cat > "$archive/control/pkg.conf" <<EOF
PKG_DBDIR: "$archive/control/db"
PKG_CACHEDIR: "$archive/control/cache"
REPOS_DIR: ["$archive/control/repos"]
REPO_AUTOUPDATE: false
EOF

cat > "$archive/control/repos/Archive.conf" <<EOF
Archive: {
    url: "$upstream",
    mirror_type: "none",
    signature_type: "fingerprints",
    fingerprints: "/usr/share/keys/pkg"
}
EOF

pkg -C "$archive/control/pkg.conf" update

-C targets this configuration, keeping the host’s package database untouched.2 Automatic catalog updates are disabled so the package list stays fixed during the download.

Downloading the packages

Download the complete collection:

pkg -C "$archive/control/pkg.conf" fetch -a -o "$archive/repository"

Rerun after an interrupted download; valid files are reused.

Add the package manager’s bootstrap link and signature:

mkdir -p "$archive/repository/Latest"
ln -s ../All/pkg-1.21.3.pkg "$archive/repository/Latest/pkg.pkg"
fetch -o "$archive/repository/Latest/pkg.pkg.sig" "$upstream/Latest/pkg.pkg.sig"

Saving the release files

If 13.3 is not cached, fetch it from FreeBSD’s release archive:

iocage fetch -r 13.3-RELEASE -s https://archive.freebsd.org -d old-releases/amd64/amd64 --noupdate --noeol

That archive may disappear too. Save the release files alongside the packages, using FreeBSD’s directory layout:3

mkdir -p "$archive/repository/releases/amd64/amd64/13.3-RELEASE"
cd /mnt/tank/iocage/download/13.3-RELEASE
cp MANIFEST base.txz lib32.txz src.txz "$archive/repository/releases/amd64/amd64/13.3-RELEASE/"

After restoring the dataset, import the release:

iocage activate tank
iocage fetch -f -d /mnt/tank/package-mirror/13.3/repository/releases/amd64/amd64 -r 13.3-RELEASE --noupdate --noeol

Serving the mirror

Create the server jail, mount the repository read-only, and enter it:

iocage create -n package-mirror -r 13.3-RELEASE dhcp=1 vnet=1 bpf=1 boot=1
mkdir -p /mnt/tank/iocage/jails/package-mirror/root/mnt/repository
iocage fstab -a package-mirror "$archive/repository" /mnt/repository nullfs ro 0 0
iocage console -f package-mirror

Inside the jail, configure pkg to use the mounted repository and install Nginx:

exec sh
mkdir -p /usr/local/etc/pkg/repos
cat > /usr/local/etc/pkg/repos/FreeBSD.conf <<'EOF'
FreeBSD: {
    url: "file:///mnt/repository",
    mirror_type: "none",
    signature_type: "fingerprints",
    fingerprints: "/usr/share/keys/pkg"
}
EOF
pkg install nginx

Configure Nginx to serve /13.3/, start it, and exit the jail:

cat > /usr/local/etc/nginx/nginx.conf <<'EOF'
events {}
http {
    server {
        listen 80;
        location /13.3/ {
            alias /mnt/repository/;
        }
    }
}
EOF
sysrc nginx_enable=YES
service nginx start
exit

Back on the host, get the jail’s address:

iocage exec package-mirror ifconfig epair0b inet

Using the mirror

Create and enter client jail:

iocage create -n new-jail -r 13.3-RELEASE dhcp=1 vnet=1 bpf=1
iocage console -f new-jail

Inside new-jail, replace <mirror-ip> in the HTTP URL1 with the repository jail’s LAN IP obtained above:

exec sh
mkdir -p /usr/local/etc/pkg/repos
cat > /usr/local/etc/pkg/repos/FreeBSD.conf <<'EOF'
FreeBSD: {
    url: "http://<mirror-ip>/13.3",
    mirror_type: "none",
    signature_type: "fingerprints",
    fingerprints: "/usr/share/keys/pkg"
}
EOF

Now pkg can be used to install packages normally:

pkg install git vim gmake

Footnotes


  1. signature_type: "fingerprints" checks catalog and bootstrap signatures against the trusted fingerprints shipped with FreeBSD under /usr/share/keys/pkg. Packages are checked against hashes in the signed catalog. HTTPS isn’t required for this LAN mirror. ↩︎ ↩︎

  2. pkg update downloads the catalog again, imports it into its database, and discards the downloaded archives. The copies fetched above retain the signed catalog for clients of our mirror. ↩︎

  3. The two amd64 directories identify the platform and processor architecture. Both are amd64 here; see FreeBSD’s directory layout. ↩︎