v46.1-ui-mt76: Argon theme + hardened WG tunnel + Link Health dashboard
基于 v46 正式版(mt76)只恢复 UI 层: - luci-theme-argon 2.4.3: local-background-wins 登录页 + bg1.jpg fallback - luci-app-argon-config: 去 ui.changes.apply, ACL mutator 移 write - luci-app-wgtunnel: rpcd ucode 后端(get/status/prepare/apply/rollback/reconnect), JSONMap 前端, 60s 一次性 token, 快照回滚, 无全局 network ACL - luci-app-tr3000-status: 只读 rpcd ucode + 5s 轮询仪表盘 - tools/: audit_ui_packages.py + install_preview.sh + rollback_watchdog.sh - docs/: 开发经历与翻车记录 + 固件哈希记录 固件本体(含烤入 WG 私钥/PSK)不入 git, 仅 K 盘保存. kernel 成员与 v46 byte-identical; 尚未刷机.
This commit is contained in:
+213
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Static audit for the Link Health read-only plugin.
|
||||
|
||||
Rejects patterns that would turn the dashboard into an action surface, and
|
||||
insists the backend uses only the documented, fixed read commands. Operates
|
||||
fully offline against the package source tree.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PKG = ROOT.parent
|
||||
|
||||
BACKEND = PKG / 'root' / 'usr' / 'share' / 'rpcd' / 'ucode' / 'luci.tr3000_status'
|
||||
FRONTEND = PKG / 'htdocs' / 'luci-static' / 'resources' / 'view' / 'status' / 'tr3000.js'
|
||||
ACL = PKG / 'root' / 'usr' / 'share' / 'rpcd' / 'acl.d' / 'luci-app-tr3000-status.json'
|
||||
MENU = PKG / 'root' / 'usr' / 'share' / 'luci' / 'menu.d' / 'luci-app-tr3000-status.json'
|
||||
MAKEFILE = PKG / 'Makefile'
|
||||
|
||||
REQUIRED_ALLOWED_READ = ['get']
|
||||
PROHIBITED_DATA_FILES = [
|
||||
'/etc/config/network',
|
||||
'/etc/config/firewall',
|
||||
'/etc/config/dhcp',
|
||||
'/etc/config/wireless',
|
||||
'/etc/rc.local',
|
||||
'/etc/hotplug.d/',
|
||||
'/etc/init.d/',
|
||||
]
|
||||
|
||||
FORBIDDEN_BACKEND_PATTERNS = [
|
||||
re.compile(r"wg\s+show\s+wg0\s+dump"),
|
||||
re.compile(r"wg\s+show\s+all\s+dump"),
|
||||
re.compile(r"\bifup\b"),
|
||||
re.compile(r"\bifdown\b"),
|
||||
re.compile(r"\bip\s+link\s+set\b"),
|
||||
re.compile(r"\buci\s+set\b"),
|
||||
re.compile(r"\buci\s+commit\b"),
|
||||
re.compile(r"\bnetwork\s+restart\b"),
|
||||
re.compile(r"\bfirewall\s+restart\b"),
|
||||
re.compile(r"\bwifi\s+restart\b"),
|
||||
re.compile(r"\bwifi\s+reload\b"),
|
||||
re.compile(r"fs\.exec"),
|
||||
re.compile(r"fs\.write"),
|
||||
re.compile(r"fs\.remove"),
|
||||
re.compile(r"fs\.mkdir"),
|
||||
re.compile(r"writefile\("),
|
||||
re.compile(r"unlink\("),
|
||||
re.compile(r"rename\("),
|
||||
re.compile(r"handleSaveApply"),
|
||||
]
|
||||
|
||||
FORBIDDEN_FRONTEND_PATTERNS = [
|
||||
re.compile(r"\brequire\s+(?:uci|network|fs)\b"),
|
||||
re.compile(r"form\.Map\("),
|
||||
re.compile(r"handleSaveApply"),
|
||||
re.compile(r"handleSave\("),
|
||||
re.compile(r"fs\.exec"),
|
||||
re.compile(r"fs\.write"),
|
||||
re.compile(r"\buci\.set\b"),
|
||||
re.compile(r"\buci\.commit\b"),
|
||||
re.compile(r"\bifup\b"),
|
||||
re.compile(r"\bifdown\b"),
|
||||
re.compile(r"\bwifi\s+restart\b"),
|
||||
]
|
||||
|
||||
REQUIRED_COMMANDS = [
|
||||
'/usr/sbin/iw dev',
|
||||
'/usr/sbin/ip -j link show',
|
||||
'/usr/sbin/ip -d -j link show',
|
||||
'/usr/sbin/ip -4 -j route show',
|
||||
'/usr/bin/wg show wg0 latest-handshakes',
|
||||
'/usr/bin/wg show wg0 transfer',
|
||||
'/usr/sbin/nft -j list chain inet fw4 mangle_forward',
|
||||
'/usr/sbin/nft -j list table inet fw4',
|
||||
]
|
||||
|
||||
KEY_LIKE = re.compile(r"(?<![A-Za-z0-9+/])[A-Za-z0-9+/]{43}=(?![A-Za-z0-9+/=])")
|
||||
MAC_LIKE = re.compile(r"(?i:(?:[0-9a-f]{2}:){5}[0-9a-f]{2})")
|
||||
|
||||
|
||||
def fail(message: str, errors: list[str]) -> None:
|
||||
errors.append(message)
|
||||
|
||||
|
||||
def check_file(label: str, path: Path, patterns: list[re.Pattern[str]], errors: list[str]) -> None:
|
||||
if not path.exists():
|
||||
fail(f'{label}: missing {path.relative_to(PKG)}', errors)
|
||||
return
|
||||
text = path.read_text(encoding='utf-8')
|
||||
for pat in patterns:
|
||||
for match in pat.finditer(text):
|
||||
start = match.start()
|
||||
line_start = text.rfind('\n', 0, start) + 1
|
||||
line_end = text.find('\n', start)
|
||||
if line_end < 0:
|
||||
line_end = len(text)
|
||||
line = text[line_start:line_end].strip()
|
||||
# allow explicit null overrides like "handleSaveApply: null"
|
||||
if re.match(r'^\s*\w+\s*:\s*null\s*,?\s*$', line):
|
||||
continue
|
||||
line_no = text[:start].count('\n') + 1
|
||||
fail(f'{label}: forbidden pattern {pat.pattern!r} at {path.relative_to(PKG)}:{line_no}', errors)
|
||||
|
||||
|
||||
def check_required_commands(errors: list[str]) -> None:
|
||||
if not BACKEND.exists():
|
||||
fail('backend: missing', errors)
|
||||
return
|
||||
text = BACKEND.read_text(encoding='utf-8')
|
||||
for cmd in REQUIRED_COMMANDS:
|
||||
if cmd not in text:
|
||||
fail(f'backend: missing required command {cmd!r}', errors)
|
||||
|
||||
|
||||
def check_prohibited_data_files(errors: list[str]) -> None:
|
||||
if not BACKEND.exists():
|
||||
return
|
||||
text = BACKEND.read_text(encoding='utf-8')
|
||||
for entry in PROHIBITED_DATA_FILES:
|
||||
if entry in text:
|
||||
fail(f'backend: contains prohibited data path {entry!r}', errors)
|
||||
|
||||
|
||||
def check_key_leak(path: Path, label: str, errors: list[str]) -> None:
|
||||
if not path.exists():
|
||||
return
|
||||
text = path.read_text(encoding='utf-8')
|
||||
for match in KEY_LIKE.finditer(text):
|
||||
line = text[:match.start()].count('\n') + 1
|
||||
fail(f'{label}: key-like literal at {path.relative_to(PKG)}:{line}', errors)
|
||||
for match in MAC_LIKE.finditer(text):
|
||||
line = text[:match.start()].count('\n') + 1
|
||||
fail(f'{label}: MAC-like literal at {path.relative_to(PKG)}:{line}', errors)
|
||||
|
||||
|
||||
def check_acl(errors: list[str]) -> None:
|
||||
if not ACL.exists():
|
||||
fail('acl: missing', errors)
|
||||
return
|
||||
data = json.loads(ACL.read_text(encoding='utf-8'))
|
||||
section = data.get('luci-app-tr3000-status', {})
|
||||
read = section.get('read', {}).get('ubus', {}).get('luci.tr3000_status', [])
|
||||
write = section.get('write', {})
|
||||
if read != REQUIRED_ALLOWED_READ:
|
||||
fail(f'acl: read must be exactly {REQUIRED_ALLOWED_READ!r}, got {read!r}', errors)
|
||||
if write not in (None, {}, []):
|
||||
fail('acl: write must be empty/absent for a read-only dashboard', errors)
|
||||
if 'luci.wgtunnel' in json.dumps(data):
|
||||
fail('acl: must not grant access to luci.wgtunnel', errors)
|
||||
|
||||
|
||||
def check_menu(errors: list[str]) -> None:
|
||||
if not MENU.exists():
|
||||
fail('menu: missing', errors)
|
||||
return
|
||||
data = json.loads(MENU.read_text(encoding='utf-8'))
|
||||
admin = data.get('admin/status/tr3000', {})
|
||||
if admin.get('action', {}).get('path') != 'status/tr3000':
|
||||
fail('menu: action.path must be status/tr3000', errors)
|
||||
acl = admin.get('depends', {}).get('acl', [])
|
||||
if 'luci-app-tr3000-status' not in acl:
|
||||
fail('menu: depends.acl must reference luci-app-tr3000-status', errors)
|
||||
|
||||
|
||||
def check_makefile(errors: list[str]) -> None:
|
||||
if not MAKEFILE.exists():
|
||||
fail('makefile: missing', errors)
|
||||
return
|
||||
text = MAKEFILE.read_text(encoding='utf-8')
|
||||
for needed in ('rpcd-mod-ucode', 'ucode-mod-fs', 'ucode-mod-ubus', 'ucode-mod-uci', 'wireguard-tools', 'ip-full', 'nftables-json'):
|
||||
if needed not in text:
|
||||
fail(f'makefile: missing dependency {needed!r}', errors)
|
||||
for bad in ('kmod-mediatek_hnat', 'kmod-warp', 'kmod-mt_wifi', 'kmod-tcp-bbr'):
|
||||
if bad in text:
|
||||
fail(f'makefile: prohibited acceleration dependency {bad!r}', errors)
|
||||
|
||||
|
||||
def check_acceleration_states(errors: list[str]) -> None:
|
||||
if not BACKEND.exists():
|
||||
return
|
||||
text = BACKEND.read_text(encoding='utf-8')
|
||||
for bad in ('disabled_unverified', 'unsupported'):
|
||||
if bad in text:
|
||||
fail(f'backend: uses forbidden acceleration state {bad!r}', errors)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
check_file('backend', BACKEND, FORBIDDEN_BACKEND_PATTERNS, errors)
|
||||
check_file('frontend', FRONTEND, FORBIDDEN_FRONTEND_PATTERNS, errors)
|
||||
check_required_commands(errors)
|
||||
check_prohibited_data_files(errors)
|
||||
check_acl(errors)
|
||||
check_menu(errors)
|
||||
check_makefile(errors)
|
||||
check_acceleration_states(errors)
|
||||
check_key_leak(BACKEND, 'backend', errors)
|
||||
check_key_leak(FRONTEND, 'frontend', errors)
|
||||
if errors:
|
||||
for line in errors:
|
||||
print(f'FAIL: {line}', file=sys.stderr)
|
||||
return 1
|
||||
print('OK: luci-app-tr3000-status passes static audit', file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"scenario": "all_healthy",
|
||||
"expect_partial": false,
|
||||
"expect_errors": [],
|
||||
"expect_summary": { "ok": true, "status": "healthy" },
|
||||
"expect_wireguard_status": "healthy",
|
||||
"expect_route_present": true,
|
||||
"expect_vxlan_present": true,
|
||||
"expect_vxlan_all_invariants_ok": true,
|
||||
"expect_acceleration": {
|
||||
"mt76": { "status": "enabled" },
|
||||
"wed": { "status": "enabled" },
|
||||
"hnat": { "status": "disabled_by_design" },
|
||||
"warp": { "status": "disabled_by_design" },
|
||||
"flowtable": { "status": "disabled_by_design" },
|
||||
"fullcone": { "status": "disabled_by_design" },
|
||||
"masquerade": { "status": "disabled_by_design" }
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"scenario": "design_invariant_violation",
|
||||
"expect_partial": false,
|
||||
"expect_lan_to_wan_forwarding_absent": false,
|
||||
"expect_summary": { "ok": false, "status": "degraded", "invariant_ok": false }
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"scenario": "ip_unavailable",
|
||||
"expect_partial": true,
|
||||
"expect_errors_any": ["WIFI_LINK_STATUS_UNAVAILABLE", "WG_ROUTE_STATUS_UNAVAILABLE", "WG_LINK_STATUS_UNAVAILABLE", "VXLAN_STATUS_UNAVAILABLE"]
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"scenario": "malformed_json",
|
||||
"expect_partial": true,
|
||||
"expect_errors_any": ["WIFI_LINK_JSON_INVALID", "WG_ROUTE_JSON_INVALID", "VXLAN_JSON_INVALID", "MSS_RULE_JSON_INVALID", "FIREWALL_RUNTIME_JSON_INVALID"]
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"scenario": "mss_missing",
|
||||
"expect_partial": false,
|
||||
"expect_mss_clamp_1330": false,
|
||||
"expect_summary": { "ok": false, "status": "degraded", "invariant_ok": false }
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"scenario": "nft_unavailable",
|
||||
"expect_partial": true,
|
||||
"expect_errors_any": ["FIREWALL_RUNTIME_UNAVAILABLE", "MSS_RULE_STATUS_UNAVAILABLE"],
|
||||
"expect_acceleration_flowtable_status": "unavailable"
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"scenario": "no_hwmon",
|
||||
"expect_partial": true,
|
||||
"expect_errors": ["HWMON_TEMPERATURE_UNAVAILABLE"]
|
||||
}
|
||||
immortalwrt-patches/feeds/luci/applications/luci-app-tr3000-status/tests/fixtures/route_missing.json
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"scenario": "route_missing",
|
||||
"expect_partial": false,
|
||||
"expect_route_present": false,
|
||||
"expect_summary": { "ok": false, "status": "degraded", "route_ok": false }
|
||||
}
|
||||
immortalwrt-patches/feeds/luci/applications/luci-app-tr3000-status/tests/fixtures/vxlan_missing.json
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"scenario": "vxlan_missing",
|
||||
"expect_partial": false,
|
||||
"expect_vxlan_present": false,
|
||||
"expect_summary": { "ok": false, "status": "degraded", "vxlan_ok": false }
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"scenario": "vxlan_wrong_vni",
|
||||
"expect_partial": false,
|
||||
"expect_vxlan_present": true,
|
||||
"expect_vxlan_vni": 11,
|
||||
"expect_vxlan_all_invariants_ok": false,
|
||||
"expect_summary": { "ok": false, "status": "degraded", "vxlan_ok": false }
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"scenario": "wg_interface_down",
|
||||
"expect_partial": false,
|
||||
"expect_wireguard_status": "healthy",
|
||||
"expect_wireguard_interface_up": false,
|
||||
"expect_summary": { "ok": false, "status": "degraded", "link_ok": false }
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"scenario": "wg_stale_handshake",
|
||||
"expect_partial": false,
|
||||
"expect_wireguard_status": "stale",
|
||||
"expect_summary": { "ok": false, "status": "degraded", "wg_ok": false }
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Preview install for the Link Health read-only plugin only.
|
||||
# Strictly UI-only: this script never touches network/firewall/wireless,
|
||||
# never runs ifup/ifdown/wifi reload, never edits /etc/config/*.
|
||||
#
|
||||
# It MUST be run from the build server shell, with the new IPK already
|
||||
# present locally and the previous IPK backed up.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PKG_NAME="luci-app-tr3000-status"
|
||||
NEW_IPK="${1:?usage: install_preview.sh path/to/luci-app-tr3000-status_<ver>_all.ipk}"
|
||||
ROUTER="${ROUTER:-root@192.168.1.2}"
|
||||
BACKUP_DIR="/root/.v46.1-preview/luci-app-tr3000-status"
|
||||
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
|
||||
[ -f "$NEW_IPK" ] || { echo "new ipk not found: $NEW_IPK" >&2; exit 2; }
|
||||
|
||||
echo "[info] target router: $ROUTER"
|
||||
echo "[info] backup dir: $BACKUP_DIR/$TIMESTAMP"
|
||||
|
||||
ssh -o BatchMode=yes "$ROUTER" "set -e; mkdir -p '$BACKUP_DIR/$TIMESTAMP'; if opkg list-installed | grep -q '$PKG_NAME'; then opkg list-installed $PKG_NAME | awk '{print \$3}' > '$BACKUP_DIR/$TIMESTAMP/prev_version'; fi; tar -C / -czf '$BACKUP_DIR/$TIMESTAMP/pre_overlay.tar.gz' etc/config/network etc/config/firewall etc/config/dhcp etc/config/wireless etc/rc.local etc/hotplug.d || true; echo '[ok] backup taken'"
|
||||
|
||||
echo "[info] installing $NEW_IPK"
|
||||
scp "$NEW_IPK" "$ROUTER:/tmp/$PKG_NAME.ipk"
|
||||
ssh -o BatchMode=yes "$ROUTER" "opkg install --force-reinstall /tmp/$PKG_NAME.ipk && rm -f /tmp/$PKG_NAME.ipk && /etc/init.d/rpcd restart && /etc/init.d/uhttpd restart >/dev/null 2>&1 || true; echo '[ok] installed'"
|
||||
|
||||
echo "[info] smoke test: call the rpc and inspect schema_version"
|
||||
ssh -o BatchMode=yes "$ROUTER" "ubus call luci.tr3000_status get | head -c 400; echo"
|
||||
|
||||
echo "[done] backup retained at $BACKUP_DIR/$TIMESTAMP on the router"
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# Roll back the Link Health read-only plugin to the previously installed
|
||||
# version captured by install_preview.sh. Strictly UI-only.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PKG_NAME="luci-app-tr3000-status"
|
||||
ROUTER="${ROUTER:-root@192.168.1.2}"
|
||||
BACKUP_ROOT="/root/.v46.1-preview/luci-app-tr3000-status"
|
||||
BACKUP_DIR="${1:?usage: rollback_preview.sh <backup_timestamp_dir>}"
|
||||
|
||||
echo "[info] rolling back from $BACKUP_ROOT/$BACKUP_DIR"
|
||||
ssh -o BatchMode=yes "$ROUTER" "set -e
|
||||
if [ -f '$BACKUP_ROOT/$BACKUP_DIR/prev_version' ]; then
|
||||
PKG_VERSION=\"\$(cat '$BACKUP_ROOT/$BACKUP_DIR/prev_version')\"
|
||||
echo \"[info] previous package version: \$PKG_VERSION\"
|
||||
if [ -n \"\$PKG_VERSION\" ]; then
|
||||
opkg install --force-reinstall --downgrade '$PKG_NAME'=\"\$PKG_VERSION\" || true
|
||||
fi
|
||||
else
|
||||
echo '[warn] no prev_version file; will remove package only'
|
||||
opkg remove '$PKG_NAME' || true
|
||||
fi
|
||||
/etc/init.d/rpcd restart
|
||||
/etc/init.d/uhttpd restart >/dev/null 2>&1 || true
|
||||
echo '[ok] rolled back'
|
||||
"
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Drive the Link Health backend ucode against synthetic fixtures.
|
||||
|
||||
The test driver is intentionally lightweight and offline. It runs ucode if
|
||||
available, otherwise it skips with a non-zero exit and a clear message. The
|
||||
goal is to validate the backend's JSON shape and tri-state semantics, not to
|
||||
re-implement rpcd. We do NOT need a live router.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
BACKEND = ROOT.parent / 'root' / 'usr' / 'share' / 'rpcd' / 'ucode' / 'luci.tr3000_status'
|
||||
FIXTURES = ROOT / 'fixtures'
|
||||
|
||||
UCODE = shutil.which('ucode') or '/opt/immortalwrt-build/immortalwrt-mt76/staging_dir/hostpkg/bin/ucode'
|
||||
|
||||
|
||||
def syntax_check() -> int:
|
||||
if not Path(UCODE).exists():
|
||||
print(f'[skip] no ucode available at {UCODE}', file=sys.stderr)
|
||||
return 0
|
||||
proc = subprocess.run([UCODE, '-c', str(BACKEND)], capture_output=True, text=True, timeout=30)
|
||||
if proc.returncode != 0:
|
||||
print(f'[fail] ucode syntax check failed: {proc.stderr}', file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not BACKEND.exists():
|
||||
print(f'[fail] backend missing: {BACKEND}', file=sys.stderr)
|
||||
return 1
|
||||
rc = syntax_check()
|
||||
fixtures = sorted(p.name for p in FIXTURES.glob('*.json'))
|
||||
print(f'[info] fixtures available: {len(fixtures)} ({", ".join(fixtures)})', file=sys.stderr)
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Verify the immutable v46 network surface is unchanged after the plugin
|
||||
# preview install. Reads remote SHA-256 of the protected files and compares
|
||||
# against the frozen hashes recorded on disk. Strictly read-only.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROUTER="${ROUTER:-root@192.168.1.2}"
|
||||
HASH_FILE="${HASH_FILE:?usage: verify_immutable.sh /path/to/hashes.txt}"
|
||||
|
||||
echo "[info] reading remote hashes from $ROUTER"
|
||||
remote_hashes=$(ssh -o BatchMode=yes "$ROUTER" "sha256sum /etc/config/network /etc/config/firewall /etc/config/dhcp /etc/config/wireless /etc/rc.local /etc/hotplug.d/iface/20-vxlan /etc/hotplug.d/iface/30-mss-clamp /usr/share/nftables.d/chain-pre/mangle_forward/30-mss-clamp.nft 2>/dev/null" | sort)
|
||||
|
||||
echo "[info] comparing against $HASH_FILE"
|
||||
while read -r expected_hash rest; do
|
||||
[ -z "$expected_hash" ] && continue
|
||||
remote_line=$(echo "$remote_hashes" | awk -v want="$rest" '$2 == want {print}')
|
||||
if [ -z "$remote_line" ]; then
|
||||
echo "MISSING: $rest"
|
||||
exit 1
|
||||
fi
|
||||
if [ "${remote_line%% *}" != "$expected_hash" ]; then
|
||||
echo "DRIFT: $rest"
|
||||
echo " expected $expected_hash"
|
||||
echo " got ${remote_line%% *}"
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: $rest"
|
||||
done < "$HASH_FILE"
|
||||
|
||||
echo "[ok] all immutable files match frozen hashes"
|
||||
Reference in New Issue
Block a user