#!/bin/bash
# Proxmox 9.1.x: bypass IPv6 "type" gating in GUI/API (accept any syntactically valid IPv6)
# Self-healing + rerunnable ensure-state patcher for:
# /usr/share/perl5/PVE/API2/Network.pm
#
# Behaviour:
# - If Network.pm is broken, restore it to a compilable baseline first.
# - Remove any previous "bypass type gating (Proxmox...)" lines (including bad older patches).
# - Insert exactly one early return in check_ipv6_settings after ip_is_ipv6() syntax check.
# - Always restart pveproxy + pvedaemon.
#
# Last Update: 2026-01-08
set -euo pipefail
# Avoid perl locale warnings on nodes without generated locales
export LC_ALL=C
export LANG=C
TARGET="/usr/share/perl5/PVE/API2/Network.pm"
MARKER="bypass type gating (Proxmox)"
TS="$(date -u +%Y%m%dT%H%M%SZ)"
BACKUP="${TARGET}.bak-ipv6.${TS}"
TMPFILE=""
cleanup() {
if [ -n "${TMPFILE:-}" ] && [ -f "$TMPFILE" ]; then
rm -f "$TMPFILE"
fi
}
trap cleanup EXIT INT TERM
[ -f "$TARGET" ] || { echo "ERR: $TARGET not found"; exit 1; }
owning_pkg() {
dpkg -S "$TARGET" 2>/dev/null | head -n1 | cut -d: -f1 || true
}
restore_from_cached_deb() {
local pkg="$1"
local deb=""
deb="$(ls -1t "/var/cache/apt/archives/${pkg}"_*".deb" 2>/dev/null | head -n1 || true)"
[ -n "$deb" ] || return 1
echo "Attempting restore from cached deb: $deb"
TMPFILE="${TARGET}.restore.$$"
dpkg-deb --fsys-tarfile "$deb" | tar -xOf - "./usr/share/perl5/PVE/API2/Network.pm" > "$TMPFILE"
mv "$TMPFILE" "$TARGET"
TMPFILE=""
return 0
}
restore_from_reinstall() {
local pkg="$1"
echo "Attempting restore via reinstall: apt-get install --reinstall -y $pkg"
apt-get update
apt-get install --reinstall -y "$pkg"
}
restore_from_compilable_backup() {
local cand=""
for cand in $(ls -1t "${TARGET}".bak-ipv6.* 2>/dev/null || true); do
[ -f "$cand" ] || continue
if perl -c "$cand" >/dev/null 2>&1; then
echo "Restoring from compilable backup: $cand"
cp -a "$cand" "$TARGET"
return 0
fi
done
for cand in "${TARGET}.bak" "${TARGET}.orig" "${TARGET}.dpkg-dist"; do
[ -f "$cand" ] || continue
if perl -c "$cand" >/dev/null 2>&1; then
echo "Restoring from compilable backup: $cand"
cp -a "$cand" "$TARGET"
return 0
fi
done
return 1
}
ensure_target_compiles_or_restore() {
if perl -c "$TARGET" >/dev/null 2>&1; then
return 0
fi
echo "WARN: $TARGET does not compile; attempting self-heal restore."
if restore_from_compilable_backup; then
perl -c "$TARGET" >/dev/null 2>&1
return 0
fi
local pkg
pkg="$(owning_pkg)"
[ -n "$pkg" ] || { echo "ERR: cannot determine owning dpkg package for $TARGET"; exit 1; }
if restore_from_cached_deb "$pkg"; then
if perl -c "$TARGET" >/dev/null 2>&1; then
echo "Restore from cached deb succeeded."
return 0
fi
echo "WARN: cached-deb restore did not compile; continuing."
fi
restore_from_reinstall "$pkg"
perl -c "$TARGET" >/dev/null 2>&1 || {
echo "ERR: restore failed; $TARGET still does not compile after reinstall."
exit 1
}
echo "Restore via reinstall succeeded."
}
apply_patch_ensure_state() {
cp -a "$TARGET" "$BACKUP"
echo "Backup: $BACKUP"
TMPFILE="${TARGET}.tmp.$$"
export PVE_IPV6_BYPASS_MARKER="$MARKER"
perl - "$TARGET" > "$TMPFILE" <<'PERL'
use strict;
use warnings;
my $file = shift @ARGV or die "ERR: missing target file arg\n";
open(my $fh, "<", $file) or die "ERR: cannot read $file: $!\n";
my @lines = <$fh>;
close($fh);
my $marker = $ENV{PVE_IPV6_BYPASS_MARKER} // 'bypass type gating (Proxmox)';
my $marker_re = qr/bypass type gating \(Proxmox/;
# Find the check_ipv6_settings sub start
my $start = -1;
for (my $i = 0; $i <= $#lines; $i++) {
if ($lines[$i] =~ /my\s+\$check_ipv6_settings\s*=\s*sub\s*\{\s*$/) {
$start = $i;
last;
}
}
die "ERR: check_ipv6_settings sub start not found\n" if $start < 0;
# Find the sub end by brace depth
my $depth = 0;
my $end = -1;
for (my $i = $start; $i <= $#lines; $i++) {
my $l = $lines[$i];
my $opens = ($l =~ tr/{/{/);
my $closes = ($l =~ tr/}/}/);
$depth += ($opens - $closes);
if ($depth == 0) { $end = $i; last; }
}
die "ERR: check_ipv6_settings sub end not found\n" if $end < 0;
# Extract block and remove any prior bypass marker lines (self-heal)
my @blk = @lines[$start..$end];
@blk = grep { $_ !~ $marker_re } @blk;
my $inserted = 0;
# Primary insertion point: right after "if !Net::IP::ip_is_ipv6($address);"
for (my $i = 0; $i <= $#blk; $i++) {
if ($blk[$i] =~ /^(\s*)if\s*!Net::IP::ip_is_ipv6\(\$address\);\s*$/) {
my $indent = $1;
my $line = $indent . "return; # $marker\n";
splice(@blk, $i+1, 0, $line);
$inserted = 1;
last;
}
}
# Fallback: insert before first "my $binip = ipv6_tobin($address);"
if (!$inserted) {
for (my $i = 0; $i <= $#blk; $i++) {
if ($blk[$i] =~ /^(\s*)my\s+\$binip\s*=\s*ipv6_tobin\(\$address\)\s*;\s*$/) {
my $indent = $1;
my $line = $indent . "return; # $marker\n";
splice(@blk, $i, 0, $line);
$inserted = 1;
last;
}
}
}
die "ERR: insertion point not found in check_ipv6_settings\n" if !$inserted;
# Reassemble full file
splice(@lines, $start, ($end - $start + 1), @blk);
# Ensure exactly one marker exists
my $count = 0;
for my $l (@lines) { $count++ if $l =~ $marker_re; }
die "ERR: unexpected marker count after patch: $count\n" if $count != 1;
print @lines;
PERL
mv "$TMPFILE" "$TARGET"
TMPFILE=""
# Verify Perl syntax; restore from this run's backup if broken
if ! perl -c "$TARGET" >/dev/null 2>&1; then
echo "ERR: Perl syntax failed after patch; restoring from: $BACKUP"
cp -a "$BACKUP" "$TARGET"
perl -c "$TARGET" >/dev/null 2>&1 || { echo "ERR: restored backup also fails to compile"; exit 1; }
fi
}
restart_services() {
systemctl restart pveproxy
systemctl restart pvedaemon
}
# 1) Self-heal to a compilable baseline if needed
ensure_target_compiles_or_restore
# 2) Ensure-state patch (rerunnable, self-heals prior marker edits)
apply_patch_ensure_state
# 3) Final verify + restart
perl -c "$TARGET" >/dev/null 2>&1
restart_services
echo "OK: IPv6 type gating bypass ensured (syntactic IPv6 accepted)."
echo "Marker line:"
grep -n "bypass type gating (Proxmox" "$TARGET" || true