Update-Opnsense.ps1:
[CmdletBinding()]
param(
[string]$OpnsenseHost = "",
[string]$SshUser = "root",
[string]$ProxyUrl = "http://10.254.253.181:8080",
[ValidateSet("", "status", "update", "upgrade", "log", "diag",
"configure", "series", "pkgs", "pkgboot", "bootstrap",
"reboot", "clean")]
[string]$Action = "",
[string]$TargetVersion = "",
# Reboot handling. Interactive runs ask on the firewall itself, in the same
# SSH session, so there is no second password prompt. Unattended runs never
# reboot unless -Reboot is given.
[switch]$Reboot,
[switch]$NoReboot,
# Version-dependent opnsense-update syntax. Verify against the option
# parsing that -Action diag prints, and adjust here if it ever changes.
[string]$UpdateCmd = "opnsense-update -bkp",
[string]$UpgradeCmd = "opnsense-update -bkpr %VERSION%",
[string]$BootstrapCmd = "opnsense-bootstrap -f -r %VERSION%"
)
$ErrorActionPreference = "Stop"
function Fail([string]$Message) {
Write-Host ""
Write-Host "ERROR: $Message" -ForegroundColor Red
exit 1
}
# ======================================================== embedded shell ====
# Single-quoted here-string: nothing is interpolated by PowerShell.
# __PROXY__, __UPDATE_CMD__, __UPGRADE_CMD__ and __BOOTSTRAP_CMD__ are
# substituted below.
$Shell = @'
#!/bin/sh
PROXY="__PROXY__"
NOPROXY="localhost,127.0.0.1"
UPDATE_CMD="__UPDATE_CMD__"
UPGRADE_CMD="__UPGRADE_CMD__"
BOOTSTRAP_CMD="__BOOTSTRAP_CMD__"
PKG_CONF="/usr/local/etc/pkg.conf"
CONFIGD_DIR="/usr/local/opnsense/service/conf/configd.conf.d"
CONFIGD_FILE="$CONFIGD_DIR/zz-update-proxy.conf"
KMOD_OFF="/usr/local/etc/pkg/repos/zz-disable-kmods.conf"
OPN_REPO="/usr/local/etc/pkg/repos/OPNsense.conf"
VERDIR="/usr/local/opnsense/version"
MARK="# managed by Update-Opnsense.ps1"
ACTION="${1:-configure}"
ARGVERSION="$2"
REBOOT_MODE="${3:-no}" # ask | yes | no
die() { echo ""; echo "ERROR: $*" >&2; exit 1; }
say() { echo "==> $*"; }
rule() { echo "----- $* -----"; }
[ "$(id -u)" = "0" ] || die "must run as root"
HTTP_PROXY="$PROXY"; HTTPS_PROXY="$PROXY"
http_proxy="$PROXY"; https_proxy="$PROXY"
NO_PROXY="$NOPROXY"; no_proxy="$NOPROXY"
export HTTP_PROXY HTTPS_PROXY http_proxy https_proxy NO_PROXY no_proxy
ABI=$(pkg config abi 2>/dev/null)
read_state() {
S_BASE=$(cat "$VERDIR/base" 2>/dev/null)
S_KERNEL=$(cat "$VERDIR/kernel" 2>/dev/null)
S_CORE=$(opnsense-version -v 2>/dev/null)
S_ABI=$(sed -n 's/.*"product_abi"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$VERDIR/core" 2>/dev/null)
S_SETS=$(echo "$S_BASE" | cut -d. -f1,2)
S_OS=$(freebsd-version -u 2>/dev/null)
S_RUN=$(uname -r)
}
print_state() {
read_state
echo " core version : $S_CORE (product_abi: $S_ABI)"
echo " base set : $S_BASE"
echo " kernel set : $S_KERNEL"
echo " userland/os : $S_OS running kernel: $S_RUN"
if [ "$S_SETS" != "$S_ABI" ]; then
echo " STATE : MISMATCH. Sets on $S_SETS, core on $S_ABI."
echo " A major upgrade was started and never finished."
else
echo " STATE : consistent on $S_ABI"
fi
}
# OPNsense ships in January and July, so the series after 25.7 is 26.1.
next_series() {
_y=$(echo "$1" | cut -d. -f1)
_m=$(echo "$1" | cut -d. -f2)
case "$_m" in
1) echo "$_y.7" ;;
*) echo "$((_y + 1)).1" ;;
esac
}
# Returns 0 = the release exists, 1 = it does not, 2 = could not tell.
# Never use -q here: it hides the difference between a 404 (the release really
# is not published) and a timeout or proxy failure (we simply could not look).
# Reporting the second as the first is how "26.7 does not exist yet" got said
# about a release that had been out for a month.
SERIES_ERR=""
series_exists() {
_url="https://pkg.opnsense.org/$ABI/$1/latest/meta.conf"
SERIES_ERR=$(fetch -T 20 -o /dev/null "$_url" 2>&1)
_rc=$?
[ "$_rc" -eq 0 ] && return 0
case "$SERIES_ERR" in
*"Not Found"*|*"404"*) return 1 ;;
*) return 2 ;;
esac
}
# CORE_NEXT is only the NAME of the coming release, never a statement that it
# is available, so it is used as a hint and always verified against the mirror.
core_next() {
sed -n 's/.*"CORE_NEXT"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
/usr/local/opnsense/version/core 2>/dev/null | head -1
}
repo_series() {
sed -n 's|.*/\([0-9][0-9.]*\)/latest.*|\1|p' "$OPN_REPO" 2>/dev/null | head -1
}
# Two hard signals that a reboot is pending, plus the exit code the wrapper
# watches for. Exit 64 means "reboot started", so the wrapper knows to wait for
# the box instead of treating the dropped session as a failure.
reboot_reason() {
_k=$(freebsd-version -k 2>/dev/null)
_r=$(uname -r)
if [ -n "$_k" ] && [ "$_k" != "$_r" ]; then
echo "installed kernel $_k differs from the running kernel $_r"
return 0
fi
if [ -d /var/cache/opnsense-update/.sets.pending ] && \
[ -n "$(ls -A /var/cache/opnsense-update/.sets.pending 2>/dev/null)" ]; then
echo "offline sets are staged and install on the next boot"
return 0
fi
return 1
}
do_reboot() {
say "Rebooting now. The session will drop."
# The reboot must outlive this SSH session. sshd tears down the whole
# process group of a pty session, and nohup only blocks SIGHUP, so a
# backgrounded sleep gets killed before it fires. daemon(8) calls setsid(),
# putting the process in its own session where the teardown cannot reach it.
if command -v daemon >/dev/null 2>&1; then
echo " issuing: daemon -f /bin/sh -c 'sleep 5; /sbin/shutdown -r now'"
daemon -f /bin/sh -c 'sleep 5; /sbin/shutdown -r now'
_drc=$?
else
echo " daemon(8) not found, falling back to nohup"
nohup /bin/sh -c 'sleep 5; /sbin/shutdown -r now' </dev/null >/dev/null 2>&1 &
_drc=$?
fi
if [ "$_drc" -ne 0 ]; then
say "Could not detach the reboot (exit $_drc). Rebooting in the foreground."
/sbin/shutdown -r now
exit 64
fi
# Confirm something is actually queued before claiming success.
sleep 1
if pgrep -f 'sleep 5' >/dev/null 2>&1 || pgrep shutdown >/dev/null 2>&1; then
echo " reboot is queued, going down in about 5 seconds"
else
say "Nothing queued. Rebooting in the foreground instead."
/sbin/shutdown -r now
fi
exit 64
}
maybe_reboot() {
_level="$1"
_why="$2"
echo ""
if [ "$_level" = "required" ]; then
say "REBOOT REQUIRED: $_why"
else
say "Reboot recommended: $_why"
fi
case "$REBOOT_MODE" in
yes) do_reboot ;;
no) echo " Not rebooting. Run later: /sbin/shutdown -r now"; return 0 ;;
esac
printf " Reboot now? [y/N] "
read _ans
case "$_ans" in
y|Y|yes|YES|j|J|ja) do_reboot ;;
*) echo " Skipped. Run later: /sbin/shutdown -r now" ;;
esac
}
# Drop any pkg_env block plus our marker line, print the rest.
strip_pkg_env() {
awk '
$0 ~ /managed by Update-Opnsense\.ps1/ { next }
skip == 1 {
o = gsub(/\{/, "{"); c = gsub(/\}/, "}")
depth += o - c
if (depth <= 0) skip = 0
next
}
$0 ~ /^[[:space:]]*pkg_env[[:space:]]*:/ {
skip = 1; depth = 0
o = gsub(/\{/, "{"); c = gsub(/\}/, "}")
depth += o - c
if (depth <= 0) skip = 0
next
}
{ print }
' "$1"
}
disable_kmods() {
cat > "$KMOD_OFF" <<'KEOF'
FreeBSD-kmods: {
enabled: no
}
KEOF
say "Disabled the FreeBSD-kmods repository via $KMOD_OFF"
}
# Run a firmware command with live output, capture its real exit code, and
# retry once without the FreeBSD-kmods repo if that is what broke it. That
# repo's URL is built from the running FreeBSD minor version and 404s on the
# mirror for some versions; it is not needed for an OPNsense upgrade.
run_firmware() {
_cmd="$1"
_out="/tmp/opn-fw.out"
{ eval "$_cmd"; echo "__RC=$?" > /tmp/opn-fw.rc; } 2>&1 | tee "$_out"
_rc=$(sed -n 's/^__RC=//p' /tmp/opn-fw.rc 2>/dev/null)
[ -n "$_rc" ] || _rc=1
if [ "$_rc" -ne 0 ] && grep -q 'repository FreeBSD-kmods' "$_out"; then
echo ""
say "Failed on the FreeBSD-kmods repository. Disabling it and retrying once."
if [ -f "$KMOD_OFF" ]; then
die "kmods is already disabled and it still failed. Read the output above."
fi
disable_kmods
pkg update -f >/dev/null 2>&1
echo ""
say "Retrying: $_cmd"
{ eval "$_cmd"; echo "__RC=$?" > /tmp/opn-fw.rc; } 2>&1 | tee "$_out"
_rc=$(sed -n 's/^__RC=//p' /tmp/opn-fw.rc 2>/dev/null)
[ -n "$_rc" ] || _rc=1
fi
return "$_rc"
}
# ------------------------------------------------------------- clean -------
if [ "$ACTION" = "clean" ]; then
[ -f "$CONFIGD_FILE" ] && rm -f "$CONFIGD_FILE" && say "removed $CONFIGD_FILE"
[ -f "$KMOD_OFF" ] && rm -f "$KMOD_OFF" && say "removed $KMOD_OFF (kmods repo re-enabled)"
if [ -f "$PKG_CONF" ] && grep -q 'pkg_env' "$PKG_CONF"; then
strip_pkg_env "$PKG_CONF" > "$PKG_CONF.new" && mv "$PKG_CONF.new" "$PKG_CONF"
say "removed pkg_env from $PKG_CONF"
fi
service configd restart >/dev/null 2>&1
say "proxy configuration removed"
exit 0
fi
# ------------------------------------------------------------ reboot -------
if [ "$ACTION" = "reboot" ]; then
if _why=$(reboot_reason); then
say "A reboot is pending: $_why"
else
say "No pending reboot was detected, but you asked for one."
fi
if [ "$REBOOT_MODE" = "no" ]; then
REBOOT_MODE="ask"
fi
maybe_reboot required "requested"
exit 0
fi
# --------------------------------------------------------------- log -------
if [ "$ACTION" = "log" ]; then
rule "state"
print_state
rule "/var/cache/opnsense-update/.upgrade.log"
cat /var/cache/opnsense-update/.upgrade.log 2>/dev/null || echo "(not present)"
rule "everything in /usr/local/opnsense/version/"
ls -la /usr/local/opnsense/version/ 2>/dev/null
for f in /usr/local/opnsense/version/*; do
case "$f" in *.mtree) continue ;; esac
[ -f "$f" ] || continue
echo "--- $f"
head -c 2000 "$f"
echo
done
rule "installed opnsense packages"
pkg info -x '^opnsense|^os-' 2>&1
rule "does the next series exist on the mirror?"
for _s in $(next_series "$(repo_series)") ; do
for _p in "latest" "aux" ""; do
_u="https://pkg.opnsense.org/$ABI/$_s${_p:+/$_p}/meta.conf"
printf '%s -> ' "$_u"
if fetch -q -T 15 -o /dev/null "$_u" 2>/dev/null; then echo "OK"; else echo "not found"; fi
done
printf 'sets: https://pkg.opnsense.org/%s/%s/sets/ -> ' "$ABI" "$_s"
if fetch -q -T 15 -o /dev/null "https://pkg.opnsense.org/$ABI/$_s/sets/" 2>/dev/null; then echo "OK"; else echo "not found"; fi
done
rule "what the GUI reads: cached firmware status"
ls -la /tmp/pkg_upgrade.progress /var/cache/opnsense-update/.upgrade.log 2>/dev/null
configctl firmware product 2>&1 | head -30
rule "pkg repo config files"
for f in /usr/local/etc/pkg/repos/*.conf; do
echo "--- $f"
cat "$f"
done
rule "all configured repos"
pkg -vv 2>/dev/null | sed -n '/^Repositories:/,$p'
rule "opnsense-update generated repo configs"
for d in /var/cache/opnsense-update/*/; do
[ -f "$d/OPNsense.conf" ] || continue
echo "--- $d"
cat "$d/OPNsense.conf"
done
rule "packages from FreeBSD-kmods"
pkg query -e '%R == "FreeBSD-kmods"' '%n-%v' 2>/dev/null || echo "(none)"
rule "end"
exit 0
fi
# -------------------------------------------------------------- diag -------
if [ "$ACTION" = "diag" ]; then
rule "state"
print_state
rule "pkg binaries and versions"
echo "pkg : $(command -v pkg) -> $(pkg -v 2>&1)"
echo "pkg-static : $(command -v pkg-static) -> $(pkg-static -v 2>&1)"
pkg info -x '^pkg' 2>&1
rule "kernel messages about signal 11 / core dumps"
dmesg 2>/dev/null | grep -i -E 'signal 11|core dumped' | tail -20
echo "(if empty, nothing was logged this boot)"
rule "core files"
sysctl kern.corefile 2>/dev/null
ls -la /*.core /var/crash/*.core 2>/dev/null | head -10
rule "opnsense tools present"
ls -la /usr/local/sbin/opnsense-* 2>/dev/null
rule "opnsense-update option parsing (from the script itself)"
grep -n 'while getopts' -A 60 /usr/local/sbin/opnsense-update 2>/dev/null | head -80
rule "how opnsense-update builds the repo config"
grep -n 'OPNsense.conf\|product_abi\|repos' /usr/local/sbin/opnsense-update 2>/dev/null | head -30
rule "update cache"
ls -laR /var/cache/opnsense-update/ 2>/dev/null | head -40
rule "end"
exit 0
fi
# ------------------------------------------------------------ status -------
if [ "$ACTION" = "status" ]; then
say "Current state"
print_state
echo ""
say "What is available"
CURSER=$(repo_series)
if pkg update -q >/dev/null 2>&1; then
UPOUT=$(pkg upgrade -n 2>&1)
if echo "$UPOUT" | grep -qi 'your packages are up to date'; then
echo " updates within $CURSER : none"
else
NUP=$(echo "$UPOUT" | sed -n 's/^Number of packages to be upgraded: *\([0-9]*\).*/\1/p')
NIN=$(echo "$UPOUT" | sed -n 's/^Number of packages to be installed: *\([0-9]*\).*/\1/p')
echo " updates within $CURSER : ${NUP:-0} to upgrade, ${NIN:-0} new -> use the update action"
fi
else
echo " updates within $CURSER : could not check (no repository access)"
fi
if _why=$(reboot_reason); then
echo " pending reboot : YES, $_why"
else
echo " pending reboot : none detected"
fi
NEXT=$(core_next)
[ -n "$NEXT" ] || NEXT=$(next_series "$CURSER")
series_exists "$NEXT"
case $? in
0) echo " next major release : $NEXT is available -> use the upgrade action" ;;
1) echo " next major release : $NEXT is not published yet" ;;
*) echo " next major release : $NEXT, could not check the mirror"
echo " $SERIES_ERR" ;;
esac
echo ""
rule "pkg repositories"
pkg -vv 2>/dev/null | grep -A6 '^ OPNsense'
rule "pkg config pkg_env"
pkg config pkg_env 2>&1
rule "configd proxy drop-in"
cat "$CONFIGD_FILE" 2>/dev/null || echo "(not present)"
rule "kmods repo override"
cat "$KMOD_OFF" 2>/dev/null || echo "(not present, repo enabled)"
rule "end"
exit 0
fi
# --------------------------------------------------------- pre-flight ------
say "Pre-flight checks"
netstat -rn -f inet | grep -q '^default' || \
die "no IPv4 default route. A proxy cannot fix a missing gateway."
_hp="${PROXY#http://}"; _hp="${_hp%%/*}"
PROXY_HOST="${_hp%%:*}"
PROXY_PORT="${_hp##*:}"
[ "$PROXY_PORT" = "$PROXY_HOST" ] && PROXY_PORT=80
case "$PROXY_HOST" in
*[!0-9.]*)
getent hosts "$PROXY_HOST" >/dev/null 2>&1 || \
die "cannot resolve $PROXY_HOST. Use the proxy IP instead: with an
HTTP proxy the firewall needs no working DNS of its own."
echo " DNS ok: $PROXY_HOST" ;;
*)
echo " DNS skipped: $PROXY_HOST is a literal IP" ;;
esac
nc -z -w 5 "$PROXY_HOST" "$PROXY_PORT" >/dev/null 2>&1 || \
die "cannot open TCP $PROXY_HOST:$PROXY_PORT"
echo " TCP ok: $PROXY_HOST:$PROXY_PORT"
read_state
[ -n "$ABI" ] && [ -n "$S_ABI" ] || die "could not determine ABI or version"
say "Testing an HTTPS fetch through the proxy"
TEST_URL="https://pkg.opnsense.org/$ABI/$(repo_series)/latest/meta.conf"
fetch -q -T 25 -o /dev/null "$TEST_URL" || \
die "proxy answers on TCP but the HTTPS fetch failed.
Likely TLS interception, or pkg.opnsense.org not allowed.
Test by hand: fetch -v -o - $TEST_URL"
echo " fetch ok: $TEST_URL"
# ----------------------------------------------------------- pkg.conf ------
# Fully managed: an existing pkg_env block is replaced, never appended to.
# Two blocks break UCL parsing, and a partial block (http_proxy only) fails
# silently on the https repository URL.
say "Configuring $PKG_CONF"
[ -f "$PKG_CONF" ] || : > "$PKG_CONF"
[ -f "$PKG_CONF.orig" ] || cp "$PKG_CONF" "$PKG_CONF.orig"
strip_pkg_env "$PKG_CONF" > "$PKG_CONF.new"
cat >> "$PKG_CONF.new" <<EOF
$MARK
pkg_env: {
http_proxy: "$PROXY",
https_proxy: "$PROXY",
no_proxy: "$NOPROXY"
}
EOF
mv "$PKG_CONF.new" "$PKG_CONF"
echo " pkg_env written:"
pkg config pkg_env 2>&1 | sed 's/^/ /'
# ------------------------------------------------------------ configd ------
# Drop-in, not configd.conf itself: the base file is vendor shipped and is
# overwritten by every core update, which silently drops the proxy again.
say "Configuring $CONFIGD_FILE"
mkdir -p "$CONFIGD_DIR"
cat > "$CONFIGD_FILE" <<EOF
[environment]
HTTP_PROXY=$PROXY
HTTPS_PROXY=$PROXY
http_proxy=$PROXY
https_proxy=$PROXY
NO_PROXY=$NOPROXY
EOF
echo " written"
say "Restarting configd"
service configd restart || die "configd failed to restart"
say "Refreshing the package catalogue"
pkg update -f || die "pkg update failed even with the proxy set"
echo ""
say "Proxy is working. State:"
print_state
# ------------------------------------------------------------- actions -----
if [ "$ACTION" = "configure" ]; then
echo ""
say "Firmware untouched. Next: status, update or upgrade."
exit 0
fi
if [ "$ACTION" = "update" ]; then
if [ "$S_SETS" != "$S_ABI" ]; then
die "refusing to run an in-series update.
Base/kernel are on $S_SETS while core is on $S_ABI. An in-series update
would DOWNGRADE base and kernel back to $S_ABI. Use the upgrade action
to finish the stalled move to $S_SETS instead."
fi
echo ""
say "Running: $UPDATE_CMD"
run_firmware "$UPDATE_CMD"
RC=$?
echo ""
if [ "$RC" -eq 0 ] && grep -qi 'nothing to do' /tmp/opn-fw.out && \
! grep -qiE '^\[[0-9]+/[0-9]+\]' /tmp/opn-fw.out; then
say "Already up to date on $S_ABI. Nothing was changed."
exit 0
fi
say "Done."
if [ "$RC" -eq 0 ]; then
if _why=$(reboot_reason); then
maybe_reboot required "$_why"
elif grep -qiE '^\[[0-9]+/[0-9]+\] (Upgrading|Installing|Reinstalling)' /tmp/opn-fw.out; then
maybe_reboot recommended "packages were replaced under running services"
fi
fi
exit "$RC"
fi
if [ "$ACTION" = "series" ]; then
TARGET="$ARGVERSION"
[ -n "$TARGET" ] || TARGET="$S_SETS"
[ -n "$TARGET" ] || die "no target series given, e.g. 26.1"
CUR=$(repo_series)
[ -n "$CUR" ] || die "could not read the current series from $OPN_REPO"
if [ "$CUR" = "$TARGET" ]; then
say "Repository is already on $TARGET, nothing to change."
else
say "Repointing the package repository from $CUR to $TARGET"
[ -f "$OPN_REPO.orig" ] || cp "$OPN_REPO" "$OPN_REPO.orig"
sed -i '' "s|/$CUR/latest|/$TARGET/latest|g" "$OPN_REPO"
echo " now: $(repo_series) (backup at $OPN_REPO.orig)"
fi
say "Refreshing the catalogue against $TARGET"
pkg update -f || die "pkg update failed against the $TARGET repository"
echo ""
say "Upgrade candidates:"
pkg upgrade -n 2>&1 | tail -20
echo ""
say "If that list looks right, run the upgrade action next."
exit 0
fi
if [ "$ACTION" = "pkgs" ]; then
# opnsense-update reads VERSIONDIR/pkgs as INSTALLED_PKGS and only rewrites
# it after its own package phase completes. Repointing the repository by
# hand never reaches that line, so the file can be left claiming an old
# series while the packages themselves have long since moved on. The GUI
# believes the file, which is where phantom "packages 25.1 -> next" rows
# come from.
PKGSFILE="/usr/local/opnsense/version/pkgs"
RECORDED=$(cat "$PKGSFILE" 2>/dev/null)
REAL=$(repo_series)
say "Package bookkeeping"
echo " $PKGSFILE says : ${RECORDED:-(missing)}"
echo " repository is on : $REAL"
echo " core is on : $S_ABI"
if [ -f "$PKGSFILE.lock" ]; then
say "A stale $PKGSFILE.lock is present."
ls -la "$PKGSFILE.lock"
echo " opnsense-update takes that to mean a package phase is running."
rm -f "$PKGSFILE.lock" && echo " removed."
fi
# Only claim a series the installed packages actually agree with.
if [ "$REAL" != "$S_ABI" ]; then
die "repository ($REAL) and core ($S_ABI) disagree.
Not writing anything. Sort the repository series out first."
fi
if ! pkg upgrade -n 2>&1 | grep -qi 'your packages are up to date'; then
die "there are pending package upgrades against $REAL.
Not writing anything: run the update action first, so the recorded
series matches what is really installed."
fi
if [ "$RECORDED" = "$REAL" ]; then
say "Already correct, nothing to do."
exit 0
fi
say "Recording $REAL (packages verified up to date against that repository)"
[ -f "$PKGSFILE" ] && cp "$PKGSFILE" "$PKGSFILE.before"
echo "$REAL" > "$PKGSFILE"
echo " $PKGSFILE now says: $(cat "$PKGSFILE")"
say "Refreshing the firmware status the GUI reads"
configctl firmware check >/dev/null 2>&1 && echo " done"
echo ""
say "Reload System > Firmware in the GUI. The Updates tab should be empty."
exit 0
fi
if [ "$ACTION" = "pkgboot" ]; then
TARGET="$ARGVERSION"
[ -n "$TARGET" ] || TARGET="$S_SETS"
[ -n "$TARGET" ] || die "no target version given, e.g. 25.7"
REPO="https://pkg.opnsense.org/$ABI/$TARGET/latest"
WORK="/tmp/pkgboot.$$"
say "Bootstrapping pkg from $REPO"
echo " Current: $(pkg-static -v 2>/dev/null)"
mkdir -p "$WORK" || die "cannot create $WORK"
cd "$WORK" || die "cannot enter $WORK"
fetch -T 60 -o packagesite.pkg "$REPO/packagesite.pkg" || die "cannot fetch the package index"
tar -xf packagesite.pkg || die "cannot extract the package index"
[ -f packagesite.yaml ] || die "packagesite.yaml not found in the index"
LINE=$(grep '"name":"pkg",' packagesite.yaml | head -1)
[ -n "$LINE" ] || die "no pkg package found in the $TARGET index"
REPOPATH=$(echo "$LINE" | sed -n 's/.*"repopath":"\([^"]*\)".*/\1/p')
NEWVER=$(echo "$LINE" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
[ -n "$REPOPATH" ] || die "could not read repopath for pkg"
say "Target pkg version: $NEWVER ($REPOPATH)"
fetch -T 120 -o newpkg.pkg "$REPO/$REPOPATH" || die "cannot fetch $REPOPATH"
# A .pkg is a tarball with the metadata members prefixed by + or . and the
# payload under usr/local. Extracting it by hand is the standard way to
# replace a pkg that is too broken to upgrade itself.
say "Installing the new pkg binaries"
cp -p /usr/local/sbin/pkg-static "/usr/local/sbin/pkg-static.before-$TARGET" 2>/dev/null
tar -xf newpkg.pkg -C / --exclude '+*' --exclude '.pkg*' || die "extraction failed"
say "Verifying"
NOWVER=$(/usr/local/sbin/pkg-static -v 2>&1)
echo " pkg-static now reports: $NOWVER"
say "Registering it in the package database"
/usr/local/sbin/pkg-static add -f newpkg.pkg 2>&1 | sed 's/^/ /'
say "Smoke test: pkg-static update"
/usr/local/sbin/pkg-static update -f 2>&1 | tail -10
RC=$?
cd / && rm -rf "$WORK"
echo ""
if [ "$RC" -eq 0 ]; then
say "pkg looks healthy. Now run the upgrade action."
else
say "pkg still returns $RC. Check the output above before going further."
fi
exit "$RC"
fi
if [ "$ACTION" = "upgrade" ]; then
TARGET="$ARGVERSION"
CURSER=$(repo_series)
if [ -z "$TARGET" ]; then
if [ "$S_SETS" != "$S_ABI" ]; then
TARGET="$S_SETS"
say "Finishing the stalled upgrade to $TARGET."
else
TARGET=$(core_next)
[ -n "$TARGET" ] || TARGET=$(next_series "$CURSER")
say "Currently on $CURSER. The next release would be $TARGET."
series_exists "$TARGET"
case $? in
0) echo " $TARGET is available." ;;
1) say "$TARGET is not published yet, so there is nothing to upgrade to."
echo " Use the update action for updates within $CURSER."
exit 0 ;;
*) say "Could not reach the mirror to confirm $TARGET:"
echo " $SERIES_ERR"
echo " Continuing anyway. opnsense-update will fail cleanly if it"
echo " really is not there, and a failed check is not proof of absence." ;;
esac
fi
fi
[ -n "$TARGET" ] || die "no target version given, e.g. 26.1"
# opnsense-update does not repoint the repository: the series is hardcoded
# in OPNsense.conf and copied verbatim into its temporary config. Without
# this it would fetch nothing and report "Nothing to do".
if [ -n "$CURSER" ] && [ "$CURSER" != "$TARGET" ]; then
say "Repointing the repository from $CURSER to $TARGET"
[ -f "$OPN_REPO.orig" ] || cp "$OPN_REPO" "$OPN_REPO.orig"
sed -i '' "s|/$CURSER/latest|/$TARGET/latest|g" "$OPN_REPO"
pkg update -f >/dev/null 2>&1 || die "no repository access for $TARGET"
echo " repository now on $(repo_series)"
fi
echo ""
say "What this will change:"
pkg upgrade -n 2>&1 | tail -8 | sed 's/^/ /'
CMD=$(echo "$UPGRADE_CMD" | sed "s/%VERSION%/$TARGET/")
echo ""
say "Running: $CMD"
echo ""
run_firmware "$CMD"
RC=$?
echo ""
if [ "$RC" -eq 0 ]; then
if grep -qi 'nothing to do' /tmp/opn-fw.out && \
! grep -qiE '^\[[0-9]+/[0-9]+\] (Upgrading|Installing|Extracting)' /tmp/opn-fw.out; then
say "WARNING: the command succeeded but changed nothing."
echo " Do not reboot expecting a difference. Check the repository series."
else
say "New state:"
print_state
if _why=$(reboot_reason); then
maybe_reboot required "$_why"
else
maybe_reboot recommended "core packages were replaced under running services"
fi
fi
fi
exit "$RC"
fi
if [ "$ACTION" = "bootstrap" ]; then
TARGET="$ARGVERSION"
[ -n "$TARGET" ] || TARGET="$S_SETS"
[ -n "$TARGET" ] || die "no target version given, e.g. 25.7"
command -v opnsense-bootstrap >/dev/null 2>&1 || \
die "opnsense-bootstrap is not present on this system."
CMD=$(echo "$BOOTSTRAP_CMD" | sed "s/%VERSION%/$TARGET/")
echo ""
say "Running: $CMD"
echo " This reinstalls every OPNsense package from the $TARGET repository."
echo " config.xml is preserved. Everything else is replaced."
echo ""
run_firmware "$CMD"
exit $?
fi
die "unknown action: $ACTION"
'@
# ==================================================== end embedded shell ====
# --- host discovery --------------------------------------------------------
# In this deployment pattern OPNsense is always the DHCP server, so the DHCP
# server address of the active adapter IS the firewall. One candidate means no
# question and no probing. The default gateway is only consulted as a fallback
# for a machine with a static address, and is labelled as such.
function Get-AdapterAddresses {
param([ValidateSet("dhcp", "gateway")][string]$Kind)
$map = @{}
$order = @()
try {
$cfgs = Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration `
-Filter "IPEnabled=TRUE" -ErrorAction Stop
} catch { return @() }
foreach ($cfg in $cfgs) {
$addrs = if ($Kind -eq "dhcp") { @($cfg.DHCPServer) } else { @($cfg.DefaultIPGateway) }
foreach ($addr in $addrs) {
if (-not $addr) { continue }
if ($addr.Contains(":")) { continue }
if ($addr -eq "0.0.0.0" -or $addr -eq "255.255.255.255") { continue }
if ($addr.StartsWith("169.254.")) { continue }
if ($map.ContainsKey($addr)) { continue }
$map[$addr] = [pscustomobject]@{
Address = $addr
Adapter = $cfg.Description
Ssh = $false
}
$order += $addr
}
}
return @($order | ForEach-Object { $map[$_] })
}
function Test-SshPort {
param([string]$Address, [int]$TimeoutMs = 1200)
try {
$client = New-Object System.Net.Sockets.TcpClient
$async = $client.BeginConnect($Address, 22, $null, $null)
$opened = $async.AsyncWaitHandle.WaitOne($TimeoutMs, $false)
$result = $opened -and $client.Connected
$client.Close()
return $result
} catch { return $false }
}
function Select-FromCandidates {
param([array]$List, [string]$Label)
foreach ($c in $List) { $c.Ssh = Test-SshPort $c.Address }
$live = @($List | Where-Object { $_.Ssh })
if ($live.Count -eq 1) {
Write-Host (" Found {0} ({1}, SSH open)" -f $live[0].Address, $Label) -ForegroundColor Green
return $live[0].Address
}
$show = if ($live.Count -gt 1) { $live } else { $List }
Write-Host ""
for ($i = 0; $i -lt $show.Count; $i++) {
$c = $show[$i]
$ssh = if ($c.Ssh) { "SSH open" } else { "no SSH" }
Write-Host (" {0}) {1,-15} {2,-10} {3}" -f ($i + 1), $c.Address, $ssh, $c.Adapter)
}
Write-Host " m) enter an address manually"
Write-Host ""
$pick = (Read-Host " Which host? [1]").Trim()
if (-not $pick) { $pick = "1" }
if ($pick -eq "m") {
$manual = (Read-Host " OPNsense IP or hostname").Trim()
if (-not $manual) { Fail "no host given." }
return $manual
}
$n = 0
if ([int]::TryParse($pick, [ref]$n) -and $n -ge 1 -and $n -le $show.Count) {
return $show[$n - 1].Address
}
Fail "invalid choice: $pick"
}
function Resolve-OpnsenseHost {
$dhcp = Get-AdapterAddresses -Kind dhcp
if ($dhcp.Count -eq 1) {
Write-Host (" Found {0} (DHCP server)" -f $dhcp[0].Address) -ForegroundColor Green
return $dhcp[0].Address
}
if ($dhcp.Count -gt 1) {
Write-Host " Several adapters have a DHCP server." -ForegroundColor DarkGray
return (Select-FromCandidates -List $dhcp -Label "DHCP server")
}
Write-Host " No DHCP server on any adapter (static address?)." -ForegroundColor Yellow
$gw = Get-AdapterAddresses -Kind gateway
if ($gw.Count -eq 1) {
Write-Host (" Falling back to the default gateway: {0}" -f $gw[0].Address) -ForegroundColor Yellow
Write-Host " Verify this really is the firewall before running anything." -ForegroundColor Yellow
return $gw[0].Address
}
if ($gw.Count -gt 1) {
Write-Host " Falling back to the default gateways." -ForegroundColor Yellow
return (Select-FromCandidates -List $gw -Label "gateway")
}
$manual = (Read-Host " Nothing detected. OPNsense IP or hostname").Trim()
if (-not $manual) { Fail "no host given." }
return $manual
}
# --- prerequisites ---------------------------------------------------------
if (-not (Get-Command ssh -ErrorAction SilentlyContinue)) {
Fail "ssh not found. Install the OpenSSH Client via Settings > Apps > Optional Features."
}
Write-Host ""
Write-Host " OPNsense update via proxy" -ForegroundColor Cyan
Write-Host " -------------------------" -ForegroundColor Cyan
if (-not $OpnsenseHost) { $OpnsenseHost = Resolve-OpnsenseHost }
$Interactive = -not $Action
if (-not $Action) {
$retry = $true
while ($retry) {
$retry = $false
Write-Host ""
Write-Host (" Host: {0} Proxy: {1}" -f $OpnsenseHost, $ProxyUrl) -ForegroundColor DarkGray
Write-Host ""
Write-Host " Normal use" -ForegroundColor Cyan
Write-Host " 1) status which version it runs and what is available"
Write-Host " 2) update install updates within the current release"
Write-Host " 3) upgrade move up to the next major release"
Write-Host " r) reboot reboot the firewall"
Write-Host ""
Write-Host " Diagnostics (read-only)" -ForegroundColor DarkGray
Write-Host " 4) log upgrade log and repository definitions"
Write-Host " 5) diag pkg versions, crash messages, available tools"
Write-Host ""
Write-Host " Repair and maintenance" -ForegroundColor DarkGray
Write-Host " 6) configure (re)apply the proxy settings only"
Write-Host " 7) series repoint the repository at a specific release"
Write-Host " 8) pkgs fix the recorded package series the GUI reads"
Write-Host " 9) pkgboot replace a broken pkg with the target release build"
Write-Host " 0) clean remove the proxy configuration again"
Write-Host " B) bootstrap reinstall every package from a release"
Write-Host ""
Write-Host " h) host target a different firewall"
Write-Host " x) proxy use a different proxy URL"
Write-Host ""
$choice = (Read-Host " Choice [1]").Trim()
if (-not $choice) { $choice = "1" }
switch ($choice) {
"1" { $Action = "status" }
"2" { $Action = "update" }
"3" { $Action = "upgrade" }
"4" { $Action = "log" }
"5" { $Action = "diag" }
"6" { $Action = "configure" }
"7" { $Action = "series" }
"8" { $Action = "pkgs" }
"9" { $Action = "pkgboot" }
"B" { $Action = "bootstrap" }
"b" { $Action = "bootstrap" }
"0" { $Action = "clean" }
"r" { $Action = "reboot" }
"h" { $OpnsenseHost = Resolve-OpnsenseHost; $retry = $true }
"x" {
$np = (Read-Host " Proxy URL [$ProxyUrl]").Trim()
if ($np) { $ProxyUrl = $np }
$retry = $true
}
default { Fail "invalid choice: $choice" }
}
}
}
if ($Action -eq "series" -and -not $TargetVersion) {
$TargetVersion = (Read-Host " Target series (blank = use the installed sets version)").Trim()
}
if ($Action -eq "pkgboot" -and -not $TargetVersion) {
$TargetVersion = (Read-Host " Target version (blank = use the installed sets version)").Trim()
}
if ($Action -eq "bootstrap") {
if (-not $TargetVersion) {
$TargetVersion = (Read-Host " Target version (blank = use the installed sets version)").Trim()
}
Write-Host ""
Write-Host " BOOTSTRAP on $OpnsenseHost reinstalls every package. config.xml survives." -ForegroundColor Yellow
Write-Host " Do not run this without a snapshot and console access." -ForegroundColor Yellow
if ((Read-Host " Type BOOTSTRAP to continue") -cne "BOOTSTRAP") { Fail "aborted." }
}
if ($Action -eq "upgrade") {
Write-Host ""
Write-Host " MAJOR UPGRADE on $OpnsenseHost." -ForegroundColor Yellow
Write-Host " The next release is selected automatically and the package list is" -ForegroundColor Yellow
Write-Host " shown before anything is installed." -ForegroundColor Yellow
Write-Host " Take a config backup and a VM snapshot, and have console access." -ForegroundColor Yellow
if ((Read-Host " Type UPGRADE to continue") -cne "UPGRADE") { Fail "aborted." }
}
# --- reboot mode -----------------------------------------------------------
# ask : the shell prompts on the firewall, inside the same session
# yes : reboot without asking
# no : never reboot, just report
if ($NoReboot) { $RebootMode = "no" }
elseif ($Reboot) { $RebootMode = "yes" }
elseif ($Interactive) { $RebootMode = "ask" }
else { $RebootMode = "no" }
# --- build the payload -----------------------------------------------------
$shell = $Shell -replace "`r`n", "`n"
$shell = $shell.Replace("__PROXY__", $ProxyUrl).
Replace("__UPDATE_CMD__", $UpdateCmd).
Replace("__UPGRADE_CMD__", $UpgradeCmd).
Replace("__BOOTSTRAP_CMD__", $BootstrapCmd)
$bytes = [System.Text.Encoding]::UTF8.GetBytes($shell)
$ms = New-Object System.IO.MemoryStream
$gz = New-Object System.IO.Compression.GZipStream($ms, [System.IO.Compression.CompressionMode]::Compress)
$gz.Write($bytes, 0, $bytes.Length)
$gz.Dispose()
$b64 = [Convert]::ToBase64String($ms.ToArray())
# Base64 contains only A-Za-z0-9+/= so it needs no shell quoting.
# The arguments are quoted so an empty TargetVersion does not shift $3.
$remote = "printf %s $b64 | openssl base64 -d -A | gzip -d > /tmp/opn-update.sh && " +
"sh /tmp/opn-update.sh '$Action' '$TargetVersion' '$RebootMode'"
Write-Host ""
Write-Host (" Action: {0} Host: {1} Proxy: {2}" -f $Action, $OpnsenseHost, $ProxyUrl) -ForegroundColor Cyan
Write-Host (" " + ("-" * 68))
& ssh -t "$SshUser@$OpnsenseHost" $remote
$rc = $LASTEXITCODE
Write-Host (" " + ("-" * 68))
# --- result ----------------------------------------------------------------
if ($rc -eq 64) {
Write-Host " Reboot started. Waiting for the firewall to come back..." -ForegroundColor Yellow
Start-Sleep -Seconds 20
$deadline = (Get-Date).AddMinutes(6)
$back = $false
while ((Get-Date) -lt $deadline) {
if (Test-SshPort -Address $OpnsenseHost -TimeoutMs 2000) { $back = $true; break }
Write-Host "." -NoNewline
Start-Sleep -Seconds 5
}
Write-Host ""
if ($back) {
Write-Host " Back up and answering on SSH. Run the status action to verify." -ForegroundColor Green
exit 0
}
Write-Host " No SSH after six minutes. Check the console." -ForegroundColor Red
exit 1
}
if ($rc -eq 0) {
Write-Host " Completed successfully." -ForegroundColor Green
switch ($Action) {
"status" { Write-Host " Nothing was changed." -ForegroundColor Green }
"log" { Write-Host " Nothing was changed." -ForegroundColor Green }
"diag" { Write-Host " Nothing was changed." -ForegroundColor Green }
"configure" { Write-Host " Proxy is set. Re-run for status, update or upgrade." -ForegroundColor Green }
"update" { }
"upgrade" { }
"reboot" { Write-Host " No reboot was performed." -ForegroundColor Green }
"series" { Write-Host " Now run the upgrade action." -ForegroundColor Green }
"pkgboot" { Write-Host " Now run the upgrade action." -ForegroundColor Green }
"pkgs" { Write-Host " Reload System > Firmware in the GUI." -ForegroundColor Green }
"bootstrap" { Write-Host " Reboot and check with the status action." -ForegroundColor Green }
"clean" { Write-Host " Proxy configuration removed." -ForegroundColor Green }
}
exit 0
}
if ($rc -eq 255) {
Write-Host " SSH connection dropped (exit 255)." -ForegroundColor Yellow
Write-Host " Expected if the firewall rebooted. Reconnect and run the status action." -ForegroundColor Yellow
exit 255
}
Write-Host " Remote script exited with code $rc. The reason is in the output above." -ForegroundColor Red
exit $rc
Usage:
<#
.SYNOPSIS
Configures an outbound HTTP proxy on an OPNsense firewall and drives its updates and major upgrades, entirely over one SSH session.
.DESCRIPTION
Self-contained: the shell script is embedded below and shipped to the firewall gzip+base64 encoded as part of the SSH command, so there is no second file, no scp, and only one password prompt.
Run it with no parameters. It finds the firewall via the DHCP server address of the active adapter and goes straight to the menu.
Requires the Windows OpenSSH Client. For root login, OPNsense needs System > Settings > Administration > "Permit root user login" enabled.
.PARAMETER Action
status : which version it runs and what is available
update : install updates within the current release
upgrade : move to the next major release; picks and prepares it itself
log : upgrade log and repository definitions
diag : pkg versions, crash messages, available tools
configure : (re)apply the proxy settings only
series : repoint the repository at a specific release
pkgs : fix the recorded package series the GUI reads
pkgboot : replace a broken pkg with the target release build
bootstrap : reinstall every package from a release
reboot : reboot the firewall
clean : remove the proxy configuration again
.EXAMPLE
.\Update-Opnsense.ps1
.\Update-Opnsense.ps1 -OpnsenseHost 172.17.238.180 -Action status
.\Update-Opnsense.ps1 -OpnsenseHost 172.17.238.180 -Action upgrade
#>
Fix ISC DHCP:
mkdir -p /var/dhcpd/dev mount -t devfs devfs /var/dhcpd/dev configctl dhcpd restart
Fix Github config back-up:
pkg install -y socat echo 'Host github.com' > /root/.ssh/config echo ' Hostname ssh.github.com' >> /root/.ssh/config echo ' Port 443' >> /root/.ssh/config echo ' User git' >> /root/.ssh/config echo ' ProxyCommand socat - PROXY:proxy.ezorg.nl:%h:%p,proxyport=8080' >> /root/.ssh/config chmod 600 /root/.ssh/config