mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-29 15:55:13 +08:00
🎈 Sync 2026-09-09 01:06:03
This commit is contained in:
1 parent
b6b067e0ad
commit
302b45db1b
35 files changed
+5887
No files matched your search
@@ -0,0 +1,180 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
umask 077
|
||||
|
||||
LOCK_DIR=/var/lock/haproxy-manager.apply
|
||||
LOCK_BUSY_EXIT=75
|
||||
TMP_CFG="/tmp/haproxy-manager-apply.$$"
|
||||
BACKUP_ID=""
|
||||
APPLIED=0
|
||||
UHTTPD_CHANGED=0
|
||||
|
||||
if [ "${1:-}" = --backup ]; then
|
||||
BACKUP_ID="${2:-}"
|
||||
[ "$#" -eq 2 ] || { echo "Usage: apply [--backup YYYYMMDD-HHMMSS]" >&2; exit 1; }
|
||||
is_valid_backup_id "$BACKUP_ID" || { echo "Invalid backup identifier" >&2; exit 1; }
|
||||
elif [ "$#" -ne 0 ]; then
|
||||
echo "Usage: apply [--backup YYYYMMDD-HHMMSS]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
operation_lock_acquire "$LOCK_DIR" || {
|
||||
echo "Another HAProxy Manager operation is already running." >&2
|
||||
exit "$LOCK_BUSY_EXIT"
|
||||
}
|
||||
|
||||
cleanup_temp() {
|
||||
rm -f "$TMP_CFG"
|
||||
}
|
||||
|
||||
release_lock() {
|
||||
operation_lock_release "$LOCK_DIR"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
cleanup_temp
|
||||
release_lock
|
||||
}
|
||||
|
||||
restore_on_error() {
|
||||
status=$?
|
||||
trap - EXIT
|
||||
if [ "$APPLIED" != 1 ] && [ -n "$BACKUP_ID" ]; then
|
||||
if [ "$UHTTPD_CHANGED" = 1 ]; then
|
||||
cleanup_temp
|
||||
(
|
||||
sleep 2
|
||||
if /usr/libexec/haproxy-manager/rollback --sync "$BACKUP_ID" >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Apply failed; restored $BACKUP_ID"
|
||||
else
|
||||
logger -t haproxy-manager "Apply failed; could not restore $BACKUP_ID"
|
||||
fi
|
||||
release_lock
|
||||
) >/dev/null 2>&1 &
|
||||
operation_lock_set_pid "$LOCK_DIR" "$!"
|
||||
exit "$status"
|
||||
fi
|
||||
|
||||
if /usr/libexec/haproxy-manager/rollback --sync "$BACKUP_ID" >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Apply failed; restored $BACKUP_ID"
|
||||
else
|
||||
logger -t haproxy-manager "Apply failed; could not restore $BACKUP_ID"
|
||||
fi
|
||||
fi
|
||||
cleanup
|
||||
exit "$status"
|
||||
}
|
||||
trap restore_on_error EXIT
|
||||
|
||||
if [ -n "$BACKUP_ID" ]; then
|
||||
[ -d "$(backup_dir)/$BACKUP_ID" ] || { echo "Backup not found: $BACKUP_ID" >&2; exit 1; }
|
||||
else
|
||||
BACKUP_ID="$(basename "$(/usr/libexec/haproxy-manager/backup)")"
|
||||
fi
|
||||
|
||||
/usr/libexec/haproxy-manager/migrate >/dev/null
|
||||
|
||||
defer_disabled_reload() {
|
||||
APPLIED=1
|
||||
trap - EXIT
|
||||
cleanup_temp
|
||||
(
|
||||
sleep 2
|
||||
if /etc/init.d/uhttpd reload >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "LuCI listener bindings restored"
|
||||
else
|
||||
if /usr/libexec/haproxy-manager/rollback --sync "$BACKUP_ID" >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "LuCI reload failed; restored $BACKUP_ID"
|
||||
else
|
||||
logger -t haproxy-manager "LuCI reload failed; could not restore $BACKUP_ID"
|
||||
fi
|
||||
fi
|
||||
release_lock
|
||||
) >/dev/null 2>&1 &
|
||||
operation_lock_set_pid "$LOCK_DIR" "$!"
|
||||
}
|
||||
|
||||
if [ "$(uci_get main enabled 0)" != 1 ]; then
|
||||
MODE="$(active_mode)"
|
||||
restore_uhttpd_bindings
|
||||
[ "$UHTTPD_CHANGED" != 1 ] || uci commit uhttpd
|
||||
/usr/libexec/haproxy-manager/firewall-sync --disable >/dev/null
|
||||
case "$MODE" in
|
||||
generated|raw)
|
||||
/etc/init.d/haproxy stop >/dev/null 2>&1 || true
|
||||
/etc/init.d/haproxy disable >/dev/null 2>&1 || true
|
||||
uci set "$CONFIG.main.active_mode=none"
|
||||
uci commit "$CONFIG"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$UHTTPD_CHANGED" = 1 ]; then
|
||||
defer_disabled_reload
|
||||
echo "Managed HAProxy service disabled. LuCI bindings will be restored shortly. Backup: $BACKUP_ID"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
APPLIED=1
|
||||
trap - EXIT
|
||||
cleanup
|
||||
echo "Managed HAProxy service disabled. Backup: $BACKUP_ID"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
/usr/libexec/haproxy-manager/generate "$TMP_CFG" >/dev/null
|
||||
/usr/libexec/haproxy-manager/validate "$TMP_CFG"
|
||||
|
||||
if [ "$(uci_get main manage_firewall 0)" = 1 ]; then
|
||||
/usr/libexec/haproxy-manager/firewall-sync --check >/dev/null
|
||||
fi
|
||||
|
||||
MANAGE_UHTTPD="$(uci_get main manage_uhttpd_bind 0)"
|
||||
LAN_BIND_IP="$(lan_ip || true)"
|
||||
|
||||
if [ "$MANAGE_UHTTPD" = 1 ]; then
|
||||
[ -n "$LAN_BIND_IP" ] || { echo "Cannot detect LAN IPv4 address. Set the LuCI LAN address manually." >&2; exit 1; }
|
||||
is_valid_ip_or_host "$LAN_BIND_IP" || { echo "Invalid LuCI LAN address: $LAN_BIND_IP" >&2; exit 1; }
|
||||
capture_uhttpd_bindings
|
||||
set_uhttpd_list listen_http "$LAN_BIND_IP:80"
|
||||
set_uhttpd_list listen_https "$LAN_BIND_IP:443"
|
||||
else
|
||||
restore_uhttpd_bindings
|
||||
fi
|
||||
|
||||
[ "$UHTTPD_CHANGED" != 1 ] || uci commit uhttpd
|
||||
|
||||
/usr/libexec/haproxy-manager/firewall-sync >/dev/null
|
||||
atomic_install_file "$TMP_CFG" "$HAPROXY_CFG"
|
||||
uci set "$CONFIG.main.active_mode=generated"
|
||||
uci commit "$CONFIG"
|
||||
/etc/init.d/haproxy enable >/dev/null 2>&1 || true
|
||||
|
||||
if [ "$UHTTPD_CHANGED" = 1 ]; then
|
||||
APPLIED=1
|
||||
trap - EXIT
|
||||
cleanup_temp
|
||||
(
|
||||
sleep 2
|
||||
if /etc/init.d/uhttpd reload >/dev/null 2>&1 && /etc/init.d/haproxy restart >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "LuCI and HAProxy reloaded successfully"
|
||||
else
|
||||
if /usr/libexec/haproxy-manager/rollback --sync "$BACKUP_ID" >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Service reload failed; restored $BACKUP_ID"
|
||||
else
|
||||
logger -t haproxy-manager "Service reload failed; could not restore $BACKUP_ID"
|
||||
fi
|
||||
fi
|
||||
release_lock
|
||||
) >/dev/null 2>&1 &
|
||||
operation_lock_set_pid "$LOCK_DIR" "$!"
|
||||
echo "Applied successfully. LuCI and HAProxy will reload shortly. Backup: $BACKUP_ID"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
/etc/init.d/haproxy restart
|
||||
|
||||
APPLIED=1
|
||||
trap - EXIT
|
||||
cleanup
|
||||
echo "Applied successfully. Backup: $BACKUP_ID"
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
umask 077
|
||||
|
||||
SRC="${1:-}"
|
||||
is_safe_temp_path "$SRC" || { echo "Refusing unsafe raw config path: $SRC" >&2; exit 1; }
|
||||
|
||||
[ -f "$SRC" ] || { echo "Raw config file not found: $SRC" >&2; exit 1; }
|
||||
|
||||
LOCK_DIR=/var/lock/haproxy-manager.apply
|
||||
operation_lock_acquire "$LOCK_DIR" || { echo "Another HAProxy Manager operation is already running." >&2; exit 1; }
|
||||
TMP_RAW="/tmp/haproxy-manager-raw-apply.$$"
|
||||
BACKUP_ID=""
|
||||
APPLIED=0
|
||||
|
||||
restore_on_error() {
|
||||
status=$?
|
||||
trap - EXIT
|
||||
if [ "$APPLIED" != 1 ] && [ -n "$BACKUP_ID" ]; then
|
||||
/usr/libexec/haproxy-manager/rollback --sync "$BACKUP_ID" >/dev/null 2>&1 || true
|
||||
logger -t haproxy-manager "Raw apply failed; restored $BACKUP_ID"
|
||||
fi
|
||||
rm -f "$TMP_RAW" "$SRC"
|
||||
operation_lock_release "$LOCK_DIR"
|
||||
exit "$status"
|
||||
}
|
||||
trap restore_on_error EXIT
|
||||
|
||||
cp "$SRC" "$TMP_RAW"
|
||||
rm -f "$SRC"
|
||||
/usr/libexec/haproxy-manager/validate "$TMP_RAW"
|
||||
BACKUP_ID="$(basename "$(/usr/libexec/haproxy-manager/backup)")"
|
||||
|
||||
atomic_install_file "$TMP_RAW" "$HAPROXY_CFG"
|
||||
uci set "$CONFIG.main.active_mode=raw"
|
||||
uci commit "$CONFIG"
|
||||
/etc/init.d/haproxy enable >/dev/null 2>&1 || true
|
||||
/etc/init.d/haproxy restart
|
||||
|
||||
APPLIED=1
|
||||
trap - EXIT
|
||||
rm -f "$TMP_RAW" "$SRC"
|
||||
operation_lock_release "$LOCK_DIR"
|
||||
echo "Raw HAProxy config applied. Backup: $BACKUP_ID"
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
REQUESTED_BASE="$(backup_dir)"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
|
||||
umask 077
|
||||
mkdir -p "$REQUESTED_BASE"
|
||||
BASE="$(resolve_storage_dir "$REQUESTED_BASE")" || {
|
||||
echo "Recovery point directory resolves outside /root or /mnt" >&2
|
||||
exit 1
|
||||
}
|
||||
chmod 700 "$BASE" 2>/dev/null || true
|
||||
DIR="$BASE/$STAMP"
|
||||
attempt=1
|
||||
while ! mkdir "$DIR" 2>/dev/null; do
|
||||
[ "$attempt" -lt 5 ] || {
|
||||
echo "Cannot create recovery point in $BASE after $attempt attempts" >&2
|
||||
exit 1
|
||||
}
|
||||
sleep 1
|
||||
attempt=$((attempt + 1))
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
DIR="$BASE/$STAMP"
|
||||
done
|
||||
chmod 700 "$DIR" 2>/dev/null || true
|
||||
[ -f /etc/config/firewall ] && cp /etc/config/firewall "$DIR/firewall"
|
||||
[ -f /etc/config/uhttpd ] && cp /etc/config/uhttpd "$DIR/uhttpd"
|
||||
[ -f /etc/config/haproxy_manager ] && cp /etc/config/haproxy_manager "$DIR/haproxy_manager"
|
||||
[ -f "$HAPROXY_CFG" ] && cp "$HAPROXY_CFG" "$DIR/haproxy.cfg"
|
||||
{
|
||||
if /etc/init.d/haproxy enabled >/dev/null 2>&1; then
|
||||
printf 'enabled=1\n'
|
||||
else
|
||||
printf 'enabled=0\n'
|
||||
fi
|
||||
if /etc/init.d/haproxy status >/dev/null 2>&1; then
|
||||
printf 'running=1\n'
|
||||
else
|
||||
printf 'running=0\n'
|
||||
fi
|
||||
} > "$DIR/haproxy.state"
|
||||
|
||||
printf '%s\n' "$STAMP" > "$BASE/LAST"
|
||||
|
||||
count=0
|
||||
for old in $(find "$BASE" -maxdepth 1 -type d -name '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]' 2>/dev/null | sort -r); do
|
||||
count=$((count + 1))
|
||||
[ "$count" -le "$BACKUP_LIMIT" ] && continue
|
||||
case "$old" in
|
||||
"$BASE"/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9])
|
||||
rm -f "$old"/*
|
||||
rmdir "$old"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '%s\n' "$DIR"
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
BASE="$(backup_dir)"
|
||||
[ ! -d "$BASE" ] || BASE="$(resolve_storage_dir "$BASE")" || exit 1
|
||||
LAST=""
|
||||
[ ! -r "$BASE/LAST" ] || LAST="$(basename "$(cat "$BASE/LAST")")"
|
||||
|
||||
count=0
|
||||
for dir in $(find "$BASE" -maxdepth 1 -type d -name '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]' 2>/dev/null | sort -r); do
|
||||
[ -d "$dir" ] || continue
|
||||
id="$(basename "$dir")"
|
||||
current=0
|
||||
[ "$id" != "$LAST" ] || current=1
|
||||
files=0
|
||||
for file in firewall uhttpd haproxy_manager haproxy.cfg; do
|
||||
[ ! -f "$dir/$file" ] || files=$((files + 1))
|
||||
done
|
||||
printf 'backup\t%s\t%s\t%s\n' "$id" "$current" "$files"
|
||||
count=$((count + 1))
|
||||
[ "$count" -lt "$BACKUP_LIMIT" ] || break
|
||||
done
|
||||
@@ -0,0 +1,361 @@
|
||||
#!/bin/sh
|
||||
|
||||
CONFIG=haproxy_manager
|
||||
HAPROXY_CFG=/etc/haproxy.cfg
|
||||
TMP_CFG=/tmp/haproxy-manager-generated.cfg
|
||||
BACKUP_LIMIT=7
|
||||
INCIDENT_LIMIT=7
|
||||
FIREWALL_RULE_NAME='HAProxy Manager: WAN listeners'
|
||||
FIREWALL_DISABLED_PREFIX='HAProxy Manager disabled: '
|
||||
|
||||
uci_get() {
|
||||
local section="$1"
|
||||
local option="$2"
|
||||
local default="${3:-}"
|
||||
uci -q get "$CONFIG.$section.$option" 2>/dev/null || printf '%s\n' "$default"
|
||||
}
|
||||
|
||||
backup_dir() {
|
||||
local dir
|
||||
dir="$(uci_get main backup_dir /root/haproxy-manager-backups)"
|
||||
is_safe_storage_dir "$dir" || {
|
||||
echo "Recovery point directory must be a plain path under /root or /mnt: $dir" >&2
|
||||
return 1
|
||||
}
|
||||
printf '%s\n' "$dir"
|
||||
}
|
||||
|
||||
incident_dir() {
|
||||
local dir
|
||||
dir="$(uci_get main incident_dir /root/haproxy-manager-incidents)"
|
||||
is_safe_storage_dir "$dir" || {
|
||||
echo "Incident directory must be a plain path under /root or /mnt: $dir" >&2
|
||||
return 1
|
||||
}
|
||||
printf '%s\n' "$dir"
|
||||
}
|
||||
|
||||
is_safe_storage_dir() {
|
||||
case "$1" in
|
||||
/root/*|/mnt/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
case "/${1#/}/" in
|
||||
*/../*|*/./*|*[!A-Za-z0-9_./-]*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
resolve_storage_dir() {
|
||||
local dir="$1" resolved
|
||||
[ -d "$dir" ] || return 1
|
||||
resolved="$(cd "$dir" 2>/dev/null && pwd -P)" || return 1
|
||||
is_safe_storage_dir "$resolved" || return 1
|
||||
printf '%s\n' "$resolved"
|
||||
}
|
||||
|
||||
is_safe_temp_path() {
|
||||
local path="$1" name
|
||||
name="${path#/tmp/}"
|
||||
[ "$path" = "/tmp/$name" ] || return 1
|
||||
case "$name" in
|
||||
haproxy-manager-[A-Za-z0-9._-]*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
operation_lock_acquire() {
|
||||
local lock_dir="$1" lock_pid
|
||||
if mkdir "$lock_dir" 2>/dev/null; then
|
||||
printf '%s\n' "$$" > "$lock_dir/pid"
|
||||
return 0
|
||||
fi
|
||||
|
||||
lock_pid="$(cat "$lock_dir/pid" 2>/dev/null || true)"
|
||||
case "$lock_pid" in
|
||||
''|*[!0-9]*) lock_pid=0 ;;
|
||||
esac
|
||||
if [ "$lock_pid" -gt 1 ] 2>/dev/null && kill -0 "$lock_pid" 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
rm -f "$lock_dir/pid"
|
||||
rmdir "$lock_dir" 2>/dev/null || return 1
|
||||
mkdir "$lock_dir" 2>/dev/null || return 1
|
||||
printf '%s\n' "$$" > "$lock_dir/pid"
|
||||
}
|
||||
|
||||
operation_lock_set_pid() {
|
||||
printf '%s\n' "$2" > "$1/pid"
|
||||
}
|
||||
|
||||
operation_lock_release() {
|
||||
rm -f "$1/pid"
|
||||
rmdir "$1" 2>/dev/null || true
|
||||
}
|
||||
|
||||
atomic_install_file() {
|
||||
local source="$1" destination="$2" temporary mode
|
||||
[ -f "$source" ] || return 1
|
||||
temporary="$destination.haproxy-manager.$$"
|
||||
rm -f "$temporary"
|
||||
if ! cp "$source" "$temporary"; then
|
||||
rm -f "$temporary"
|
||||
return 1
|
||||
fi
|
||||
mode="$(stat -c '%a' "$destination" 2>/dev/null || stat -c '%a' "$source" 2>/dev/null || echo 600)"
|
||||
chmod "$mode" "$temporary" || { rm -f "$temporary"; return 1; }
|
||||
mv -f "$temporary" "$destination" || { rm -f "$temporary"; return 1; }
|
||||
}
|
||||
|
||||
active_mode() {
|
||||
local mode
|
||||
mode="$(uci_get main active_mode)"
|
||||
case "$mode" in
|
||||
generated|raw|none) printf '%s\n' "$mode" ;;
|
||||
*)
|
||||
if [ "$(uci_get main enabled 0)" = 1 ] &&
|
||||
grep -q '^# Generated by luci-app-haproxy-manager\.' "$HAPROXY_CFG" 2>/dev/null; then
|
||||
printf 'generated\n'
|
||||
else
|
||||
printf 'none\n'
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
wan_interface() {
|
||||
local interface
|
||||
interface="$(uci_get main wan_interface wan)"
|
||||
case "$interface" in
|
||||
''|*[!A-Za-z0-9_.-]*) return 1 ;;
|
||||
esac
|
||||
printf '%s\n' "$interface"
|
||||
}
|
||||
|
||||
wan_ip() {
|
||||
local configured interface
|
||||
configured="$(uci_get main wan_bind_ip auto)"
|
||||
if [ "$configured" != auto ] && [ -n "$configured" ]; then
|
||||
printf '%s\n' "$configured"
|
||||
return 0
|
||||
fi
|
||||
|
||||
interface="$(wan_interface)" || return 1
|
||||
ubus call "network.interface.$interface" status 2>/dev/null \
|
||||
| jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null \
|
||||
| grep -m1 .
|
||||
}
|
||||
|
||||
lan_ip() {
|
||||
local configured
|
||||
configured="$(uci_get main lan_bind_ip auto)"
|
||||
if [ "$configured" != auto ] && [ -n "$configured" ]; then
|
||||
printf '%s\n' "$configured"
|
||||
return 0
|
||||
fi
|
||||
|
||||
ubus call network.interface.lan status 2>/dev/null \
|
||||
| jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null \
|
||||
| grep -m1 .
|
||||
}
|
||||
|
||||
route_name() {
|
||||
printf '%s' "$1" | tr '.:-' '___' | tr -cd 'A-Za-z0-9_'
|
||||
}
|
||||
|
||||
is_valid_host() {
|
||||
case "$1" in
|
||||
''|[!A-Za-z0-9]*|*[!A-Za-z0-9]) return 1 ;;
|
||||
*[!A-Za-z0-9.-]*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
is_valid_ip_or_host() {
|
||||
case "$1" in
|
||||
''|*[!A-Za-z0-9_.:-]*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
is_valid_backup_id() {
|
||||
case "$1" in
|
||||
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
backend_address() {
|
||||
local host="$1"
|
||||
local port="$2"
|
||||
|
||||
case "$host" in
|
||||
*:*) printf '[%s]:%s\n' "$host" "$port" ;;
|
||||
*) printf '%s:%s\n' "$host" "$port" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_valid_port() {
|
||||
local port="$1"
|
||||
[ "$port" -ge 1 ] 2>/dev/null && [ "$port" -le 65535 ] 2>/dev/null
|
||||
}
|
||||
|
||||
route_sections() {
|
||||
uci -q show "$CONFIG" \
|
||||
| sed -n "s/^$CONFIG\\.\\(@route\\[[0-9][0-9]*\\]\\)=route/\\1/p"
|
||||
}
|
||||
|
||||
route_kind() {
|
||||
local section="$1"
|
||||
local kind
|
||||
|
||||
kind="$(uci_get "$section" kind)"
|
||||
[ -n "$kind" ] || kind="$(uci_get "$section" protocol http)"
|
||||
printf '%s\n' "$kind"
|
||||
}
|
||||
|
||||
route_port_maps() {
|
||||
local section="$1"
|
||||
local kind="$2"
|
||||
local public_port backend_port
|
||||
|
||||
case "$kind" in
|
||||
ssh)
|
||||
public_port="$(uci_get "$section" ssh_listen_port 22)"
|
||||
backend_port="$(uci_get "$section" ssh_backend_port 22)"
|
||||
printf '%s:%s\n' "$public_port" "$backend_port"
|
||||
;;
|
||||
rdp)
|
||||
public_port="$(uci_get "$section" rdp_listen_port 3389)"
|
||||
backend_port="$(uci_get "$section" rdp_backend_port 3389)"
|
||||
printf '%s:%s\n' "$public_port" "$backend_port"
|
||||
;;
|
||||
custom)
|
||||
for map in $(uci_get "$section" port_map); do
|
||||
printf '%s\n' "$map"
|
||||
done
|
||||
;;
|
||||
tcp)
|
||||
public_port="$(uci_get "$section" listen_port "$(uci_get "$section" backend_port)")"
|
||||
backend_port="$(uci_get "$section" backend_port)"
|
||||
printf '%s:%s\n' "$public_port" "$backend_port"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_valid_port_map() {
|
||||
local map="$1"
|
||||
local public_port backend_port
|
||||
|
||||
case "$map" in
|
||||
*:*:*) return 1 ;;
|
||||
*:*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
public_port="${map%%:*}"
|
||||
backend_port="${map#*:}"
|
||||
is_valid_port "$public_port" && is_valid_port "$backend_port"
|
||||
}
|
||||
|
||||
required_ports() {
|
||||
local section enabled kind http_enabled https_enabled map
|
||||
local http_port https_port
|
||||
|
||||
http_port="$(uci_get main http_port 80)"
|
||||
https_port="$(uci_get main https_port 443)"
|
||||
[ "$(uci_get main enabled 0)" = 1 ] || return 0
|
||||
|
||||
for section in $(route_sections); do
|
||||
enabled="$(uci_get "$section" enabled 1)"
|
||||
[ "$enabled" = 1 ] || continue
|
||||
kind="$(route_kind "$section")"
|
||||
|
||||
case "$kind" in
|
||||
web)
|
||||
http_enabled="$(uci_get "$section" web_http 1)"
|
||||
https_enabled="$(uci_get "$section" web_https 1)"
|
||||
[ "$http_enabled" != 1 ] || printf '%s\n' "$http_port"
|
||||
[ "$https_enabled" != 1 ] || printf '%s\n' "$https_port"
|
||||
;;
|
||||
http)
|
||||
printf '%s\n' "$http_port"
|
||||
;;
|
||||
https)
|
||||
printf '%s\n' "$https_port"
|
||||
;;
|
||||
both)
|
||||
printf '%s\n%s\n' "$http_port" "$https_port"
|
||||
;;
|
||||
ssh|rdp|custom|tcp)
|
||||
for map in $(route_port_maps "$section" "$kind"); do
|
||||
printf '%s\n' "${map%%:*}"
|
||||
done
|
||||
;;
|
||||
esac
|
||||
done | sort -nu
|
||||
}
|
||||
|
||||
port_spec_contains() {
|
||||
local spec="$1"
|
||||
local wanted="$2"
|
||||
local token first last
|
||||
|
||||
for token in $(printf '%s' "$spec" | tr ',' ' '); do
|
||||
case "$token" in
|
||||
*-*)
|
||||
first="${token%%-*}"
|
||||
last="${token#*-}"
|
||||
[ "$wanted" -ge "$first" ] 2>/dev/null && [ "$wanted" -le "$last" ] 2>/dev/null && return 0
|
||||
;;
|
||||
*)
|
||||
[ "$token" = "$wanted" ] && return 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
set_uhttpd_list() {
|
||||
local option values current address
|
||||
option="$1"
|
||||
values="$2"
|
||||
current="$(uci -q get "uhttpd.main.$option" 2>/dev/null || true)"
|
||||
[ "$current" = "$values" ] && return 0
|
||||
|
||||
uci -q delete "uhttpd.main.$option" || true
|
||||
for address in $values; do
|
||||
uci add_list "uhttpd.main.$option=$address"
|
||||
done
|
||||
UHTTPD_CHANGED=1
|
||||
}
|
||||
|
||||
capture_uhttpd_bindings() {
|
||||
local saved_http saved_https address
|
||||
[ "$(uci_get main uhttpd_bind_owned 0)" != 1 ] || return 0
|
||||
|
||||
saved_http="$(uci -q get uhttpd.main.listen_http 2>/dev/null || true)"
|
||||
saved_https="$(uci -q get uhttpd.main.listen_https 2>/dev/null || true)"
|
||||
uci -q delete "$CONFIG.main.uhttpd_saved_listen_http" || true
|
||||
uci -q delete "$CONFIG.main.uhttpd_saved_listen_https" || true
|
||||
for address in $saved_http; do
|
||||
uci add_list "$CONFIG.main.uhttpd_saved_listen_http=$address"
|
||||
done
|
||||
for address in $saved_https; do
|
||||
uci add_list "$CONFIG.main.uhttpd_saved_listen_https=$address"
|
||||
done
|
||||
uci set "$CONFIG.main.uhttpd_bind_owned=1"
|
||||
uci commit "$CONFIG"
|
||||
}
|
||||
|
||||
restore_uhttpd_bindings() {
|
||||
[ "$(uci_get main uhttpd_bind_owned 0)" = 1 ] || return 0
|
||||
|
||||
set_uhttpd_list listen_http "$(uci_get main uhttpd_saved_listen_http)"
|
||||
set_uhttpd_list listen_https "$(uci_get main uhttpd_saved_listen_https)"
|
||||
uci -q delete "$CONFIG.main.uhttpd_saved_listen_http" || true
|
||||
uci -q delete "$CONFIG.main.uhttpd_saved_listen_https" || true
|
||||
uci -q delete "$CONFIG.main.uhttpd_bind_owned" || true
|
||||
uci commit "$CONFIG"
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/bin/sh
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
ZONE="$(uci_get main firewall_zone wan)"
|
||||
MANAGED="$(uci_get main manage_firewall 0)"
|
||||
MODE="$(uci_get main firewall_conflict_mode warn)"
|
||||
PORTS="$(required_ports)"
|
||||
WAN_POLICY=unknown
|
||||
HAS_MANAGED_RULE=0
|
||||
CONFLICTS=0
|
||||
|
||||
for zone in $(uci -q show firewall | sed -n 's/^firewall\.\(@zone\[[0-9][0-9]*\]\)=zone/\1/p'); do
|
||||
[ "$(uci -q get "firewall.$zone.name")" = "$ZONE" ] || continue
|
||||
WAN_POLICY="$(uci -q get "firewall.$zone.input" 2>/dev/null || echo REJECT)"
|
||||
break
|
||||
done
|
||||
|
||||
for rule in $(uci -q show firewall | sed -n 's/^firewall\.\(@rule\[[0-9][0-9]*\]\)=rule/\1/p'); do
|
||||
name="$(uci -q get "firewall.$rule.name" 2>/dev/null || true)"
|
||||
[ "$name" = "$FIREWALL_RULE_NAME" ] || continue
|
||||
HAS_MANAGED_RULE=1
|
||||
break
|
||||
done
|
||||
|
||||
printf 'enabled\t%s\n' "$MANAGED"
|
||||
printf 'zone\t%s\n' "$ZONE"
|
||||
printf 'policy\t%s\n' "$WAN_POLICY"
|
||||
printf 'mode\t%s\n' "$MODE"
|
||||
printf 'managed_rule\t%s\n' "$HAS_MANAGED_RULE"
|
||||
|
||||
for port in $PORTS; do
|
||||
printf 'port\t%s\n' "$port"
|
||||
done
|
||||
|
||||
for redirect in $(uci -q show firewall | sed -n 's/^firewall\.\(@redirect\[[0-9][0-9]*\]\)=redirect/\1/p'); do
|
||||
redirect_enabled="$(uci -q get "firewall.$redirect.enabled" 2>/dev/null || echo 1)"
|
||||
redirect_name="$(uci -q get "firewall.$redirect.name" 2>/dev/null || true)"
|
||||
if [ "$redirect_enabled" != 1 ]; then
|
||||
case "$redirect_name" in
|
||||
"$FIREWALL_DISABLED_PREFIX"*)
|
||||
[ "$MANAGED" = 1 ] && [ "$MODE" != disable ] || continue
|
||||
;;
|
||||
*) continue ;;
|
||||
esac
|
||||
fi
|
||||
[ "$(uci -q get "firewall.$redirect.src" 2>/dev/null || true)" = "$ZONE" ] || continue
|
||||
|
||||
proto="$(uci -q get "firewall.$redirect.proto" 2>/dev/null || echo 'tcp udp')"
|
||||
case " $proto " in *' tcp '*|*' tcpudp '*) ;; *) continue ;; esac
|
||||
|
||||
spec="$(uci -q get "firewall.$redirect.src_dport" 2>/dev/null || true)"
|
||||
[ -n "$spec" ] || continue
|
||||
|
||||
for port in $PORTS; do
|
||||
port_spec_contains "$spec" "$port" || continue
|
||||
name="${redirect_name:-$redirect}"
|
||||
dest_ip="$(uci -q get "firewall.$redirect.dest_ip" 2>/dev/null || true)"
|
||||
dest_port="$(uci -q get "firewall.$redirect.dest_port" 2>/dev/null || true)"
|
||||
printf 'conflict\t%s\t%s\t%s\t%s\t%s\n' "$redirect" "$name" "$spec" "$dest_ip" "$dest_port"
|
||||
CONFLICTS=$((CONFLICTS + 1))
|
||||
break
|
||||
done
|
||||
done
|
||||
|
||||
printf 'conflicts\t%s\n' "$CONFLICTS"
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
umask 077
|
||||
|
||||
CHECK_ONLY=0
|
||||
FORCE_DISABLED=0
|
||||
case "${1:-}" in
|
||||
'') ;;
|
||||
--check) CHECK_ONLY=1 ;;
|
||||
--disable) FORCE_DISABLED=1 ;;
|
||||
*) echo "Usage: firewall-sync [--check|--disable]" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
ZONE="$(uci_get main firewall_zone wan)"
|
||||
MODE="$(uci_get main firewall_conflict_mode warn)"
|
||||
MANAGED="$(uci_get main manage_firewall 0)"
|
||||
[ "$FORCE_DISABLED" != 1 ] || MANAGED=0
|
||||
PLAN=/tmp/haproxy-manager-firewall-plan.$$
|
||||
/usr/libexec/haproxy-manager/firewall-plan > "$PLAN"
|
||||
trap 'rm -f "$PLAN"' EXIT
|
||||
|
||||
CONFLICTS="$(sed -n 's/^conflicts[[:space:]]*//p' "$PLAN")"
|
||||
if [ "$MANAGED" = 1 ] && [ "${CONFLICTS:-0}" -gt 0 ] && [ "$MODE" != disable ]; then
|
||||
echo "Enabled firewall port forwards conflict with HAProxy listeners:" >&2
|
||||
awk -F '\t' '$1 == "conflict" { printf "- %s (WAN %s -> %s:%s)\n", $3, $4, $5, $6 }' "$PLAN" >&2
|
||||
echo "Choose automatic conflict resolution in Settings or disable the redirects manually." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
[ "$CHECK_ONLY" != 1 ] || { cat "$PLAN"; exit 0; }
|
||||
|
||||
PORTS=""
|
||||
[ "$MANAGED" != 1 ] || PORTS="$(sed -n 's/^port[[:space:]]*//p' "$PLAN")"
|
||||
CHANGED=0
|
||||
|
||||
for redirect in $(uci -q show firewall | sed -n 's/^firewall\.\(@redirect\[[0-9][0-9]*\]\)=redirect/\1/p'); do
|
||||
name="$(uci -q get "firewall.$redirect.name" 2>/dev/null || true)"
|
||||
case "$name" in
|
||||
"$FIREWALL_DISABLED_PREFIX"*) managed_disabled=1 ;;
|
||||
*) managed_disabled=0 ;;
|
||||
esac
|
||||
[ "$managed_disabled" = 1 ] || continue
|
||||
spec="$(uci -q get "firewall.$redirect.src_dport" 2>/dev/null || true)"
|
||||
still_needed=0
|
||||
for port in $PORTS; do
|
||||
port_spec_contains "$spec" "$port" || continue
|
||||
still_needed=1
|
||||
break
|
||||
done
|
||||
[ "$still_needed" = 1 ] && continue
|
||||
uci set "firewall.$redirect.enabled=1"
|
||||
CHANGED=1
|
||||
name="${name#"$FIREWALL_DISABLED_PREFIX"}"
|
||||
if [ -n "$name" ]; then
|
||||
uci set "firewall.$redirect.name=$name"
|
||||
else
|
||||
uci -q delete "firewall.$redirect.name" || true
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$MANAGED" = 1 ] && [ "$MODE" = disable ]; then
|
||||
for redirect in $(awk -F '\t' '$1 == "conflict" { print $2 }' "$PLAN"); do
|
||||
[ -n "$redirect" ] || continue
|
||||
name="$(uci -q get "firewall.$redirect.name" 2>/dev/null || true)"
|
||||
case "$name" in
|
||||
"$FIREWALL_DISABLED_PREFIX"*) ;;
|
||||
*) uci set "firewall.$redirect.name=$FIREWALL_DISABLED_PREFIX$name" ;;
|
||||
esac
|
||||
uci set "firewall.$redirect.enabled=0"
|
||||
CHANGED=1
|
||||
done
|
||||
fi
|
||||
|
||||
for index in $(uci -q show firewall | sed -n 's/^firewall\.@rule\[\([0-9][0-9]*\)\]=rule/\1/p' | sort -rn); do
|
||||
rule="@rule[$index]"
|
||||
name="$(uci -q get "firewall.$rule.name" 2>/dev/null || true)"
|
||||
if [ "$name" = "$FIREWALL_RULE_NAME" ]; then
|
||||
uci -q delete "firewall.$rule" || true
|
||||
CHANGED=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$PORTS" ]; then
|
||||
rule="$(uci add firewall rule)"
|
||||
uci set "firewall.$rule.name=$FIREWALL_RULE_NAME"
|
||||
uci set "firewall.$rule.src=$ZONE"
|
||||
uci set "firewall.$rule.proto=tcp"
|
||||
uci set "firewall.$rule.target=ACCEPT"
|
||||
uci set "firewall.$rule.family=ipv4"
|
||||
for port in $PORTS; do
|
||||
uci add_list "firewall.$rule.dest_port=$port"
|
||||
done
|
||||
CHANGED=1
|
||||
fi
|
||||
|
||||
if [ "$CHANGED" = 1 ]; then
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload
|
||||
fi
|
||||
|
||||
if [ "$MANAGED" = 1 ]; then
|
||||
echo "Firewall rules synchronized"
|
||||
else
|
||||
echo "Firewall automation disabled; managed rules removed"
|
||||
fi
|
||||
@@ -0,0 +1,234 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
umask 077
|
||||
|
||||
OUT="${1:-$TMP_CFG}"
|
||||
is_safe_temp_path "$OUT" || { echo "Refusing unsafe generated config path: $OUT" >&2; exit 1; }
|
||||
[ ! -d "$OUT" ] || { echo "Refusing generated config directory: $OUT" >&2; exit 1; }
|
||||
WORK_OUT="$OUT.new.$$"
|
||||
ENABLED="$(uci_get main enabled 0)"
|
||||
HTTP_PORT="$(uci_get main http_port 80)"
|
||||
HTTPS_PORT="$(uci_get main https_port 443)"
|
||||
WAN_IP="$(wan_ip || true)"
|
||||
|
||||
if [ "$ENABLED" != 1 ]; then
|
||||
echo "Managed HAProxy configuration is disabled." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
is_valid_port "$HTTP_PORT" || { echo "Invalid public HTTP port: $HTTP_PORT" >&2; exit 1; }
|
||||
is_valid_port "$HTTPS_PORT" || { echo "Invalid public HTTPS port: $HTTPS_PORT" >&2; exit 1; }
|
||||
|
||||
if [ -z "$WAN_IP" ]; then
|
||||
echo "Cannot detect WAN IPv4 address. Set the WAN bind address manually." >&2
|
||||
exit 1
|
||||
fi
|
||||
is_valid_ip_or_host "$WAN_IP" || { echo "Invalid WAN bind address: $WAN_IP" >&2; exit 1; }
|
||||
|
||||
TMP_HTTP_RULES=/tmp/haproxy-manager-http-rules.$$
|
||||
TMP_HTTP_BACKENDS=/tmp/haproxy-manager-http-backends.$$
|
||||
TMP_TLS_RULES=/tmp/haproxy-manager-tls-rules.$$
|
||||
TMP_TLS_BACKENDS=/tmp/haproxy-manager-tls-backends.$$
|
||||
TMP_TCP_FRONTENDS=/tmp/haproxy-manager-tcp-frontends.$$
|
||||
TMP_PORTS=/tmp/haproxy-manager-listen-ports.$$
|
||||
: > "$TMP_HTTP_RULES"
|
||||
: > "$TMP_HTTP_BACKENDS"
|
||||
: > "$TMP_TLS_RULES"
|
||||
: > "$TMP_TLS_BACKENDS"
|
||||
: > "$TMP_TCP_FRONTENDS"
|
||||
: > "$TMP_PORTS"
|
||||
trap 'rm -f "$WORK_OUT" "$TMP_HTTP_RULES" "$TMP_HTTP_BACKENDS" "$TMP_TLS_RULES" "$TMP_TLS_BACKENDS" "$TMP_TCP_FRONTENDS" "$TMP_PORTS"' EXIT
|
||||
|
||||
HAS_HTTP=0
|
||||
HAS_HTTPS=0
|
||||
HAS_TCP=0
|
||||
|
||||
claim_port() {
|
||||
local port="$1"
|
||||
local owner="$2"
|
||||
local existing
|
||||
|
||||
if grep -q "^${port}[[:space:]]" "$TMP_PORTS"; then
|
||||
existing="$(sed -n "s/^${port}[[:space:]]*//p" "$TMP_PORTS" | head -1)"
|
||||
echo "Public TCP port $port conflicts with ${existing:-another service}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\t%s\n' "$port" "$owner" >> "$TMP_PORTS"
|
||||
}
|
||||
|
||||
add_http_route() {
|
||||
local name="$1" host="$2" backend_host="$3" backend_port="$4"
|
||||
local address
|
||||
address="$(backend_address "$backend_host" "$backend_port")"
|
||||
|
||||
if [ "$HAS_HTTP" != 1 ]; then
|
||||
claim_port "$HTTP_PORT" "the shared Web HTTP listener"
|
||||
HAS_HTTP=1
|
||||
fi
|
||||
|
||||
{
|
||||
printf ' acl host_%s hdr(host),host_only -i %s\n' "$name" "$host"
|
||||
printf ' use_backend be_http_%s if host_%s\n' "$name" "$name"
|
||||
} >> "$TMP_HTTP_RULES"
|
||||
{
|
||||
printf 'backend be_http_%s\n' "$name"
|
||||
printf ' mode http\n'
|
||||
printf ' option httpchk GET /\n'
|
||||
printf ' server srv1 %s check\n\n' "$address"
|
||||
} >> "$TMP_HTTP_BACKENDS"
|
||||
}
|
||||
|
||||
add_https_route() {
|
||||
local name="$1" host="$2" backend_host="$3" backend_port="$4"
|
||||
local address
|
||||
address="$(backend_address "$backend_host" "$backend_port")"
|
||||
|
||||
if [ "$HAS_HTTPS" != 1 ]; then
|
||||
claim_port "$HTTPS_PORT" "the shared Web HTTPS listener"
|
||||
HAS_HTTPS=1
|
||||
fi
|
||||
|
||||
printf ' use_backend be_tls_%s if { req.ssl_sni -i %s }\n' "$name" "$host" >> "$TMP_TLS_RULES"
|
||||
{
|
||||
printf 'backend be_tls_%s\n' "$name"
|
||||
printf ' mode tcp\n'
|
||||
printf ' server srv1 %s check\n\n' "$address"
|
||||
} >> "$TMP_TLS_BACKENDS"
|
||||
}
|
||||
|
||||
add_tcp_route() {
|
||||
local name="$1" label="$2" backend_host="$3" public_port="$4" backend_port="$5"
|
||||
local address bind_address
|
||||
address="$(backend_address "$backend_host" "$backend_port")"
|
||||
bind_address="$(backend_address "$WAN_IP" "$public_port")"
|
||||
|
||||
claim_port "$public_port" "service $label"
|
||||
HAS_TCP=1
|
||||
{
|
||||
printf 'frontend fe_tcp_%s\n' "$name"
|
||||
printf ' bind %s\n' "$bind_address"
|
||||
printf ' mode tcp\n'
|
||||
printf ' default_backend be_tcp_%s\n\n' "$name"
|
||||
printf 'backend be_tcp_%s\n' "$name"
|
||||
printf ' mode tcp\n'
|
||||
printf ' server srv1 %s check\n\n' "$address"
|
||||
} >> "$TMP_TCP_FRONTENDS"
|
||||
}
|
||||
|
||||
for section in $(route_sections); do
|
||||
enabled="$(uci_get "$section" enabled 1)"
|
||||
[ "$enabled" = 1 ] || continue
|
||||
|
||||
kind="$(route_kind "$section")"
|
||||
label="$(uci_get "$section" name)"
|
||||
host="$(uci_get "$section" host)"
|
||||
backend_host="$(uci_get "$section" backend_host)"
|
||||
[ -n "$label" ] || label="$host"
|
||||
[ -n "$label" ] || label="$kind"
|
||||
name="$(route_name "${label}_${section}")"
|
||||
|
||||
is_valid_ip_or_host "$backend_host" || { echo "Invalid backend host for $label: $backend_host" >&2; exit 1; }
|
||||
|
||||
case "$kind" in
|
||||
web)
|
||||
is_valid_host "$host" || { echo "Invalid or missing domain for Web service $label: $host" >&2; exit 1; }
|
||||
http_enabled="$(uci_get "$section" web_http 1)"
|
||||
https_enabled="$(uci_get "$section" web_https 1)"
|
||||
[ "$http_enabled" = 1 ] || [ "$https_enabled" = 1 ] || { echo "Web service $label has no enabled protocol." >&2; exit 1; }
|
||||
|
||||
if [ "$http_enabled" = 1 ]; then
|
||||
backend_port="$(uci_get "$section" backend_http_port 80)"
|
||||
is_valid_port "$backend_port" || { echo "Invalid HTTP backend port for $label: $backend_port" >&2; exit 1; }
|
||||
add_http_route "$name" "$host" "$backend_host" "$backend_port"
|
||||
fi
|
||||
|
||||
if [ "$https_enabled" = 1 ]; then
|
||||
backend_port="$(uci_get "$section" backend_https_port 443)"
|
||||
is_valid_port "$backend_port" || { echo "Invalid HTTPS backend port for $label: $backend_port" >&2; exit 1; }
|
||||
add_https_route "$name" "$host" "$backend_host" "$backend_port"
|
||||
fi
|
||||
;;
|
||||
http|https|both)
|
||||
is_valid_host "$host" || { echo "Invalid or missing domain for legacy service $label: $host" >&2; exit 1; }
|
||||
backend_port="$(uci_get "$section" backend_port)"
|
||||
is_valid_port "$backend_port" || { echo "Invalid backend port for $label: $backend_port" >&2; exit 1; }
|
||||
case "$kind" in http|both) add_http_route "$name" "$host" "$backend_host" "$backend_port" ;; esac
|
||||
case "$kind" in https|both) add_https_route "$name" "$host" "$backend_host" "$backend_port" ;; esac
|
||||
;;
|
||||
ssh|rdp|custom|tcp)
|
||||
map_index=0
|
||||
maps="$(route_port_maps "$section" "$kind")"
|
||||
[ -n "$maps" ] || { echo "No TCP port mappings configured for $label." >&2; exit 1; }
|
||||
for map in $maps; do
|
||||
is_valid_port_map "$map" || { echo "Invalid TCP port mapping for $label: $map" >&2; exit 1; }
|
||||
public_port="${map%%:*}"
|
||||
backend_port="${map#*:}"
|
||||
map_index=$((map_index + 1))
|
||||
add_tcp_route "${name}_${map_index}" "$label" "$backend_host" "$public_port" "$backend_port"
|
||||
done
|
||||
;;
|
||||
*)
|
||||
echo "Invalid service type for $label: $kind" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$HAS_HTTP" != 1 ] && [ "$HAS_HTTPS" != 1 ] && [ "$HAS_TCP" != 1 ]; then
|
||||
echo "No enabled services define a HAProxy listener." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
{
|
||||
printf '# Generated by luci-app-haproxy-manager. Manual changes may be overwritten.\n'
|
||||
printf 'global\n'
|
||||
printf ' log /dev/log local0\n'
|
||||
printf ' maxconn 2048\n'
|
||||
printf ' daemon\n\n'
|
||||
printf 'defaults\n'
|
||||
printf ' log global\n'
|
||||
printf ' timeout connect 5s\n'
|
||||
printf ' timeout client 60s\n'
|
||||
printf ' timeout server 60s\n'
|
||||
printf ' option dontlognull\n\n'
|
||||
|
||||
if [ "$HAS_HTTP" = 1 ]; then
|
||||
http_bind="$(backend_address "$WAN_IP" "$HTTP_PORT")"
|
||||
printf 'frontend fe_web_http\n'
|
||||
printf ' bind %s\n' "$http_bind"
|
||||
printf ' mode http\n'
|
||||
printf ' option httplog\n'
|
||||
cat "$TMP_HTTP_RULES"
|
||||
printf ' default_backend be_not_found\n\n'
|
||||
cat "$TMP_HTTP_BACKENDS"
|
||||
fi
|
||||
|
||||
if [ "$HAS_HTTPS" = 1 ]; then
|
||||
https_bind="$(backend_address "$WAN_IP" "$HTTPS_PORT")"
|
||||
printf 'frontend fe_web_https\n'
|
||||
printf ' bind %s\n' "$https_bind"
|
||||
printf ' mode tcp\n'
|
||||
printf ' tcp-request inspect-delay 5s\n'
|
||||
printf ' tcp-request content accept if { req.ssl_hello_type 1 }\n'
|
||||
cat "$TMP_TLS_RULES"
|
||||
printf ' default_backend be_tls_not_found\n\n'
|
||||
cat "$TMP_TLS_BACKENDS"
|
||||
fi
|
||||
|
||||
cat "$TMP_TCP_FRONTENDS"
|
||||
|
||||
printf 'backend be_not_found\n'
|
||||
printf ' mode http\n'
|
||||
printf ' http-request return status 404 content-type text/plain string "No HAProxy route matched this host.\\n"\n\n'
|
||||
printf 'backend be_tls_not_found\n'
|
||||
printf ' mode tcp\n'
|
||||
printf ' tcp-request content reject\n'
|
||||
} > "$WORK_OUT"
|
||||
|
||||
# Replace the path itself so a stale symlink cannot redirect this root write.
|
||||
rm -f "$OUT"
|
||||
mv "$WORK_OUT" "$OUT"
|
||||
|
||||
printf '%s\n' "$OUT"
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
ID="${1:-}"
|
||||
is_valid_backup_id "$ID" || { echo "Invalid incident identifier" >&2; exit 1; }
|
||||
BASE="$(incident_dir)"
|
||||
[ ! -d "$BASE" ] || BASE="$(resolve_storage_dir "$BASE")" || exit 1
|
||||
FILE="$BASE/$ID/diagnostics.log"
|
||||
[ -f "$FILE" ] || { echo "Incident report not found: $ID" >&2; exit 1; }
|
||||
|
||||
# Keep the RPC response bounded even if a platform command produced excessive output.
|
||||
tail -c 65536 "$FILE"
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
BASE="$(incident_dir)"
|
||||
[ ! -d "$BASE" ] || BASE="$(resolve_storage_dir "$BASE")" || exit 1
|
||||
LAST=""
|
||||
[ ! -r "$BASE/LAST" ] || LAST="$(basename "$(sed -n '1p' "$BASE/LAST")")"
|
||||
|
||||
safe_field() {
|
||||
printf '%s' "$1" | tr '\t\r\n' ' '
|
||||
}
|
||||
|
||||
count=0
|
||||
for dir in $(find "$BASE" -maxdepth 1 -type d -name '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]' 2>/dev/null | sort -r); do
|
||||
[ -d "$dir" ] || continue
|
||||
id="$(basename "$dir")"
|
||||
latest=0
|
||||
[ "$id" != "$LAST" ] || latest=1
|
||||
result="$(sed -n '1p' "$dir/result" 2>/dev/null || echo detected)"
|
||||
action="$(sed -n 's/^action=//p' "$dir/meta" 2>/dev/null | head -1)"
|
||||
interface="$(sed -n 's/^interface=//p' "$dir/meta" 2>/dev/null | head -1)"
|
||||
reason="$(sed -n 's/^reason=//p' "$dir/meta" 2>/dev/null | head -1)"
|
||||
printf 'incident\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"$id" "$latest" "$(safe_field "$result")" "$(safe_field "$action")" \
|
||||
"$(safe_field "$interface")" "$(safe_field "$reason")"
|
||||
count=$((count + 1))
|
||||
[ "$count" -lt "$INCIDENT_LIMIT" ] || break
|
||||
done
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
migrate_firewall_markers() {
|
||||
local section marker name changed
|
||||
changed=0
|
||||
|
||||
for section in $(uci -q show firewall | sed -n 's/^firewall\.\(@redirect\[[0-9][0-9]*\]\)=redirect/\1/p'); do
|
||||
marker="$(uci -q get "firewall.$section.hm_disabled_by_haproxy_manager" 2>/dev/null || true)"
|
||||
[ -n "$marker" ] || continue
|
||||
if [ "$marker" = 1 ]; then
|
||||
name="$(uci -q get "firewall.$section.name" 2>/dev/null || true)"
|
||||
case "$name" in
|
||||
"$FIREWALL_DISABLED_PREFIX"*) ;;
|
||||
*) uci set "firewall.$section.name=$FIREWALL_DISABLED_PREFIX$name" ;;
|
||||
esac
|
||||
fi
|
||||
uci -q delete "firewall.$section.hm_disabled_by_haproxy_manager" || true
|
||||
changed=1
|
||||
done
|
||||
|
||||
for section in $(uci -q show firewall | sed -n 's/^firewall\.\(@rule\[[0-9][0-9]*\]\)=rule/\1/p'); do
|
||||
marker="$(uci -q get "firewall.$section.hm_managed" 2>/dev/null || true)"
|
||||
[ -n "$marker" ] || continue
|
||||
[ "$marker" != 1 ] || uci set "firewall.$section.name=$FIREWALL_RULE_NAME"
|
||||
uci -q delete "firewall.$section.hm_managed" || true
|
||||
changed=1
|
||||
done
|
||||
|
||||
[ "$changed" != 1 ] || uci commit firewall
|
||||
}
|
||||
|
||||
migrate_firewall_markers
|
||||
|
||||
SCHEMA_VERSION="$(uci_get main schema_version 1)"
|
||||
MIGRATED=0
|
||||
if [ "$SCHEMA_VERSION" -lt 2 ] 2>/dev/null; then
|
||||
|
||||
SECTIONS="$(route_sections)"
|
||||
[ -z "$SECTIONS" ] || /usr/libexec/haproxy-manager/backup >/dev/null
|
||||
|
||||
CONSUMED=/tmp/haproxy-manager-migrate-consumed.$$
|
||||
: > "$CONSUMED"
|
||||
trap 'rm -f "$CONSUMED"' EXIT
|
||||
|
||||
is_consumed() {
|
||||
grep -qx "$1" "$CONSUMED"
|
||||
}
|
||||
|
||||
to_web() {
|
||||
local section="$1" http_enabled="$2" https_enabled="$3" http_backend="$4" https_backend="$5"
|
||||
|
||||
uci set "$CONFIG.$section.kind=web"
|
||||
uci set "$CONFIG.$section.web_http=$http_enabled"
|
||||
uci set "$CONFIG.$section.web_https=$https_enabled"
|
||||
[ "$http_enabled" != 1 ] || uci set "$CONFIG.$section.backend_http_port=$http_backend"
|
||||
[ "$https_enabled" != 1 ] || uci set "$CONFIG.$section.backend_https_port=$https_backend"
|
||||
uci -q delete "$CONFIG.$section.protocol" || true
|
||||
uci -q delete "$CONFIG.$section.listen_port" || true
|
||||
uci -q delete "$CONFIG.$section.backend_port" || true
|
||||
}
|
||||
|
||||
clean_web_name() {
|
||||
local section="$1" host="$2" name clean
|
||||
name="$(uci_get "$section" name)"
|
||||
clean="$name"
|
||||
|
||||
case "$name" in
|
||||
"$host 80"|"$host:80"|"$host HTTP"|"$host http") clean="$host" ;;
|
||||
*" HTTP") clean="${name% HTTP}" ;;
|
||||
*" http") clean="${name% http}" ;;
|
||||
esac
|
||||
|
||||
[ -z "$clean" ] || [ "$clean" = "$name" ] || uci set "$CONFIG.$section.name=$clean"
|
||||
}
|
||||
|
||||
for section in $SECTIONS; do
|
||||
protocol="$(uci_get "$section" protocol)"
|
||||
case "$protocol" in
|
||||
http)
|
||||
host="$(uci_get "$section" host)"
|
||||
backend_host="$(uci_get "$section" backend_host)"
|
||||
enabled="$(uci_get "$section" enabled 1)"
|
||||
http_backend="$(uci_get "$section" backend_port 80)"
|
||||
match=""
|
||||
|
||||
for candidate in $SECTIONS; do
|
||||
is_consumed "$candidate" && continue
|
||||
[ "$(uci_get "$candidate" protocol)" = https ] || continue
|
||||
[ "$(uci_get "$candidate" host)" = "$host" ] || continue
|
||||
[ "$(uci_get "$candidate" backend_host)" = "$backend_host" ] || continue
|
||||
[ "$(uci_get "$candidate" enabled 1)" = "$enabled" ] || continue
|
||||
match="$candidate"
|
||||
break
|
||||
done
|
||||
|
||||
if [ -n "$match" ]; then
|
||||
clean_web_name "$section" "$host"
|
||||
to_web "$section" 1 1 "$http_backend" "$(uci_get "$match" backend_port 443)"
|
||||
printf '%s\n' "$match" >> "$CONSUMED"
|
||||
else
|
||||
clean_web_name "$section" "$host"
|
||||
to_web "$section" 1 0 "$http_backend" 443
|
||||
fi
|
||||
;;
|
||||
both)
|
||||
backend="$(uci_get "$section" backend_port 80)"
|
||||
clean_web_name "$section" "$(uci_get "$section" host)"
|
||||
to_web "$section" 1 1 "$backend" "$backend"
|
||||
;;
|
||||
tcp)
|
||||
public_port="$(uci_get "$section" listen_port "$(uci_get "$section" backend_port)")"
|
||||
backend_port="$(uci_get "$section" backend_port)"
|
||||
uci set "$CONFIG.$section.kind=custom"
|
||||
uci -q delete "$CONFIG.$section.port_map" || true
|
||||
uci add_list "$CONFIG.$section.port_map=$public_port:$backend_port"
|
||||
uci -q delete "$CONFIG.$section.protocol" || true
|
||||
uci -q delete "$CONFIG.$section.listen_port" || true
|
||||
uci -q delete "$CONFIG.$section.backend_port" || true
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
for section in $SECTIONS; do
|
||||
is_consumed "$section" && continue
|
||||
[ "$(uci_get "$section" protocol)" = https ] || continue
|
||||
to_web "$section" 0 1 80 "$(uci_get "$section" backend_port 443)"
|
||||
done
|
||||
|
||||
sed -n 's/^@route\[\([0-9][0-9]*\)\]$/\1/p' "$CONSUMED" | sort -rn | while read -r index; do
|
||||
uci -q delete "$CONFIG.@route[$index]" || true
|
||||
done
|
||||
MIGRATED=1
|
||||
|
||||
fi
|
||||
|
||||
if [ "$SCHEMA_VERSION" -lt 3 ] 2>/dev/null; then
|
||||
if [ "$(uci_get main enabled 0)" = 1 ] &&
|
||||
grep -q '^# Generated by luci-app-haproxy-manager\.' "$HAPROXY_CFG" 2>/dev/null; then
|
||||
uci set "$CONFIG.main.active_mode=generated"
|
||||
else
|
||||
uci set "$CONFIG.main.active_mode=none"
|
||||
fi
|
||||
uci set "$CONFIG.main.schema_version=3"
|
||||
uci commit "$CONFIG"
|
||||
MIGRATED=1
|
||||
fi
|
||||
|
||||
[ "$MIGRATED" != 1 ] || echo "Configuration schema upgraded to version 3"
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
[ "$(uci_get main webhook_enabled 0)" = 1 ] || exit 0
|
||||
URL="$(uci_get main webhook_url)"
|
||||
case "$URL" in
|
||||
http://*) AUTHORITY="${URL#http://}" ;;
|
||||
https://*) AUTHORITY="${URL#https://}" ;;
|
||||
*) echo "Webhook URL must use HTTP or HTTPS" >&2; exit 1 ;;
|
||||
esac
|
||||
[ -n "${AUTHORITY%%/*}" ] || { echo "Webhook URL must include a host" >&2; exit 1; }
|
||||
case "$URL" in
|
||||
*[![:graph:]]*) echo "Webhook URL contains invalid characters" >&2; exit 1 ;;
|
||||
esac
|
||||
if command -v uclient-fetch >/dev/null 2>&1; then
|
||||
FETCHER=uclient-fetch
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
FETCHER=wget
|
||||
else
|
||||
echo "A compatible wget implementation is required for webhook delivery" >&2
|
||||
exit 1
|
||||
fi
|
||||
[ -r /usr/share/libubox/jshn.sh ] || { echo "jshn.sh is not available" >&2; exit 1; }
|
||||
|
||||
INCIDENT_ID="$(printf '%s' "${1:-unknown}" | tr -cd '0-9-')"
|
||||
RESULT="$(printf '%s' "${2:-unknown}" | tr -cd 'A-Za-z0-9_.:-')"
|
||||
ACTION="$(printf '%s' "${3:-unknown}" | tr -cd 'A-Za-z0-9_.:-')"
|
||||
INTERFACE="$(printf '%s' "${4:-unknown}" | tr -cd 'A-Za-z0-9_.:-')"
|
||||
REASON="$(printf '%s' "${5:-unknown}" | tr -cd 'A-Za-z0-9_.:-')"
|
||||
|
||||
. /usr/share/libubox/jshn.sh
|
||||
json_init
|
||||
json_add_string event haproxy-manager-recovery
|
||||
json_add_string incident "$INCIDENT_ID"
|
||||
json_add_string result "$RESULT"
|
||||
json_add_string action "$ACTION"
|
||||
json_add_string interface "$INTERFACE"
|
||||
json_add_string reason "$REASON"
|
||||
PAYLOAD="$(json_dump)"
|
||||
|
||||
"$FETCHER" -q -T 10 -O /dev/null \
|
||||
--header='Content-Type: application/json' \
|
||||
--post-data="$PAYLOAD" "$URL"
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
required_ports
|
||||
@@ -0,0 +1,259 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
umask 077
|
||||
|
||||
ACTION="$(printf '%s' "${1:-network}" | tr -cd 'A-Za-z0-9_.:-')"
|
||||
INTERFACE="$(printf '%s' "${2:-unknown}" | tr -cd 'A-Za-z0-9_.:-')"
|
||||
RECOVER_LOCK=/var/lock/haproxy-manager.recover
|
||||
APPLY_LOCK=/var/lock/haproxy-manager.apply
|
||||
APPLY_LOCK_HELD=0
|
||||
TMP_CFG="/tmp/haproxy-manager-recover.$$"
|
||||
INCIDENT=""
|
||||
BASE=""
|
||||
DIAGNOSTICS=/dev/null
|
||||
|
||||
[ -n "$ACTION" ] || ACTION=network
|
||||
[ -n "$INTERFACE" ] || INTERFACE=unknown
|
||||
[ "$(uci_get main auto_recover 1)" = 1 ] || exit 0
|
||||
[ "$(uci_get main enabled 0)" = 1 ] || exit 0
|
||||
RECONCILE_GENERATED=0
|
||||
case "$ACTION" in
|
||||
ifup|ifupdate)
|
||||
[ "$INTERFACE" = "$(wan_interface)" ] || exit 0
|
||||
RECONCILE_GENERATED=1
|
||||
;;
|
||||
esac
|
||||
[ -x /etc/init.d/haproxy ] || exit 0
|
||||
/etc/init.d/haproxy enabled >/dev/null 2>&1 || exit 0
|
||||
|
||||
MODE="$(active_mode)"
|
||||
case "$MODE" in
|
||||
generated|raw) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
|
||||
cleanup() {
|
||||
rm -f "$TMP_CFG"
|
||||
[ "$APPLY_LOCK_HELD" != 1 ] || operation_lock_release "$APPLY_LOCK"
|
||||
operation_lock_release "$RECOVER_LOCK"
|
||||
}
|
||||
|
||||
trim_incidents() {
|
||||
local count old
|
||||
count=0
|
||||
for old in $(find "$BASE" -maxdepth 1 -type d -name '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]' 2>/dev/null | sort -r); do
|
||||
count=$((count + 1))
|
||||
[ "$count" -le "$INCIDENT_LIMIT" ] && continue
|
||||
case "$old" in
|
||||
"$BASE"/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9])
|
||||
rm -f "$old/meta" "$old/result" "$old/diagnostics.log"
|
||||
rmdir "$old" 2>/dev/null || true
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
create_incident() {
|
||||
local reason="$1" id attempt target_ip requested_base
|
||||
requested_base="$(incident_dir)" || return 1
|
||||
umask 077
|
||||
mkdir -p "$requested_base" || {
|
||||
logger -t haproxy-manager "Cannot create incident report directory: $requested_base"
|
||||
return 1
|
||||
}
|
||||
BASE="$(resolve_storage_dir "$requested_base")" || {
|
||||
logger -t haproxy-manager "Incident directory resolves outside /root or /mnt"
|
||||
return 1
|
||||
}
|
||||
id="$(date +%Y%m%d-%H%M%S)"
|
||||
INCIDENT="$BASE/$id"
|
||||
attempt=1
|
||||
while ! mkdir "$INCIDENT" 2>/dev/null; do
|
||||
[ "$attempt" -lt 5 ] || {
|
||||
logger -t haproxy-manager "Cannot create incident report in $BASE"
|
||||
return 1
|
||||
}
|
||||
sleep 1
|
||||
attempt=$((attempt + 1))
|
||||
id="$(date +%Y%m%d-%H%M%S)"
|
||||
INCIDENT="$BASE/$id"
|
||||
done
|
||||
chmod 700 "$BASE" "$INCIDENT" 2>/dev/null || true
|
||||
target_ip="$(wan_ip 2>/dev/null || echo unknown)"
|
||||
cat > "$INCIDENT/meta" <<EOF || return 1
|
||||
created=$id
|
||||
action=$ACTION
|
||||
interface=$INTERFACE
|
||||
reason=$reason
|
||||
target_ip=$target_ip
|
||||
EOF
|
||||
printf 'detected\n' > "$INCIDENT/result" || return 1
|
||||
printf '%s\n' "$id" > "$BASE/LAST" || return 1
|
||||
trim_incidents
|
||||
DIAGNOSTICS="$INCIDENT/diagnostics.log"
|
||||
if [ "$reason" = wan-address-change ]; then
|
||||
{
|
||||
printf 'HAProxy Manager WAN listener reconciliation\n'
|
||||
printf 'Incident: %s\nAction: %s\nInterface: %s\nReason: %s\nTarget WAN address: %s\n' \
|
||||
"$id" "$ACTION" "$INTERFACE" "$reason" "$target_ip"
|
||||
} > "$DIAGNOSTICS" 2>&1
|
||||
return 0
|
||||
fi
|
||||
{
|
||||
printf 'HAProxy Manager automatic recovery\n'
|
||||
printf 'Incident: %s\nAction: %s\nInterface: %s\nReason: %s\nTarget WAN address: %s\n\n' \
|
||||
"$id" "$ACTION" "$INTERFACE" "$reason" "$target_ip"
|
||||
printf '== Time and uptime ==\n'
|
||||
date || true
|
||||
uptime || true
|
||||
printf '\n== Memory ==\n'
|
||||
free || true
|
||||
printf '\n== IPv4 addresses ==\n'
|
||||
ip -4 addr show || true
|
||||
printf '\n== HAProxy procd state ==\n'
|
||||
ubus call service list '{"name":"haproxy","verbose":true}' || true
|
||||
printf '\n== Network interfaces ==\n'
|
||||
ubus call network.interface dump || true
|
||||
printf '\n== Recent HAProxy, procd, and OOM messages ==\n'
|
||||
logread | grep -iE 'haproxy|procd.*instance|out of memory|oom|killed process' | tail -n 160 || true
|
||||
printf '\n== Kernel OOM messages ==\n'
|
||||
dmesg | grep -Ei 'oom|out of memory|killed process|haproxy' | tail -n 80 || true
|
||||
} > "$DIAGNOSTICS" 2>&1
|
||||
}
|
||||
|
||||
set_result() {
|
||||
local result="$1" id reason
|
||||
if [ -z "$INCIDENT" ] || [ ! -d "$INCIDENT" ]; then
|
||||
logger -t haproxy-manager "Recovery result without incident report: $result"
|
||||
return 0
|
||||
fi
|
||||
if ! printf '%s\n' "$result" > "$INCIDENT/result"; then
|
||||
logger -t haproxy-manager "Could not update incident report with result: $result"
|
||||
INCIDENT=""
|
||||
DIAGNOSTICS=/dev/null
|
||||
return 0
|
||||
fi
|
||||
id="${INCIDENT##*/}"
|
||||
reason="$(sed -n 's/^reason=//p' "$INCIDENT/meta" 2>/dev/null | head -1)"
|
||||
(
|
||||
if ! /usr/libexec/haproxy-manager/notify "$id" "$result" "$ACTION" "$INTERFACE" "$reason"; then
|
||||
logger -t haproxy-manager "Webhook notification failed for incident $id"
|
||||
fi
|
||||
) >/dev/null 2>&1 &
|
||||
}
|
||||
|
||||
operation_lock_acquire "$RECOVER_LOCK" || exit 0
|
||||
trap cleanup EXIT
|
||||
|
||||
# Let netifd finish installing addresses and routes before comparing listeners.
|
||||
sleep 2
|
||||
operation_lock_acquire "$APPLY_LOCK" || exit 0
|
||||
APPLY_LOCK_HELD=1
|
||||
|
||||
CONFIG_CHANGED=0
|
||||
GENERATION_FAILED=0
|
||||
VALIDATION_FAILED=0
|
||||
if [ "$RECONCILE_GENERATED" = 1 ] && [ "$MODE" = generated ] &&
|
||||
[ "$(uci_get main wan_bind_ip auto)" = auto ]; then
|
||||
if /usr/libexec/haproxy-manager/generate "$TMP_CFG" >/dev/null 2>&1; then
|
||||
if /usr/libexec/haproxy-manager/validate "$TMP_CFG" >/dev/null 2>&1; then
|
||||
cmp -s "$TMP_CFG" "$HAPROXY_CFG" || CONFIG_CHANGED=1
|
||||
else
|
||||
VALIDATION_FAILED=1
|
||||
fi
|
||||
else
|
||||
GENERATION_FAILED=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$GENERATION_FAILED" != 1 ] && [ "$VALIDATION_FAILED" != 1 ] &&
|
||||
[ "$CONFIG_CHANGED" != 1 ] && /etc/init.d/haproxy status >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$GENERATION_FAILED" = 1 ]; then
|
||||
REASON=wan-address-unavailable
|
||||
elif [ "$VALIDATION_FAILED" = 1 ]; then
|
||||
REASON=invalid-generated-config
|
||||
elif [ "$CONFIG_CHANGED" = 1 ]; then
|
||||
REASON=wan-address-change
|
||||
else
|
||||
REASON=service-stopped
|
||||
fi
|
||||
if ! create_incident "$REASON"; then
|
||||
logger -t haproxy-manager "Incident report could not be stored; recovery will continue"
|
||||
INCIDENT=""
|
||||
DIAGNOSTICS=/dev/null
|
||||
fi
|
||||
logger -t haproxy-manager "HAProxy recovery started after $ACTION on $INTERFACE ($REASON)"
|
||||
|
||||
if [ "$GENERATION_FAILED" = 1 ]; then
|
||||
set_result address-unavailable
|
||||
logger -t haproxy-manager "Automatic recovery stopped: WAN address is unavailable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$VALIDATION_FAILED" = 1 ]; then
|
||||
set_result invalid-generated-config
|
||||
logger -t haproxy-manager "Automatic recovery stopped: generated configuration is invalid"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CONFIG_CHANGED" = 1 ]; then
|
||||
BACKUP_PATH=""
|
||||
if ! BACKUP_PATH="$(/usr/libexec/haproxy-manager/backup 2>> "$DIAGNOSTICS")"; then
|
||||
set_result backup-failed
|
||||
logger -t haproxy-manager "Automatic recovery stopped: snapshot creation failed"
|
||||
exit 1
|
||||
fi
|
||||
printf '\nRecovery point: %s\n' "${BACKUP_PATH##*/}" >> "$DIAGNOSTICS" || true
|
||||
if ! atomic_install_file "$TMP_CFG" "$HAPROXY_CFG"; then
|
||||
set_result install-failed
|
||||
logger -t haproxy-manager "Automatic recovery stopped: HAProxy configuration could not be installed"
|
||||
exit 1
|
||||
fi
|
||||
if /etc/init.d/haproxy restart >> "$DIAGNOSTICS" 2>&1 &&
|
||||
/etc/init.d/haproxy status >/dev/null 2>&1; then
|
||||
set_result reconciled
|
||||
logger -t haproxy-manager "HAProxy listeners reconciled with the current WAN address"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if /usr/libexec/haproxy-manager/rollback --sync "${BACKUP_PATH##*/}" >> "$DIAGNOSTICS" 2>&1; then
|
||||
set_result rolled-back
|
||||
logger -t haproxy-manager "WAN listener reconciliation failed and was rolled back"
|
||||
else
|
||||
set_result rollback-failed
|
||||
logger -t haproxy-manager "WAN listener reconciliation and rollback both failed"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! /etc/init.d/haproxy check >> "$DIAGNOSTICS" 2>&1; then
|
||||
set_result invalid-config
|
||||
logger -t haproxy-manager "Automatic recovery stopped: HAProxy configuration is invalid"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
attempt=1
|
||||
while [ "$attempt" -le 5 ]; do
|
||||
printf '\n== Start attempt %s ==\n' "$attempt" >> "$DIAGNOSTICS" || true
|
||||
/etc/init.d/haproxy restart >> "$DIAGNOSTICS" 2>&1 || true
|
||||
sleep $((attempt + 1))
|
||||
if /etc/init.d/haproxy status >/dev/null 2>&1; then
|
||||
set_result recovered
|
||||
{
|
||||
printf '\n== Recovered listeners ==\n'
|
||||
ps w | grep '[h]aproxy' || true
|
||||
netstat -lntp 2>/dev/null | grep haproxy || true
|
||||
} >> "$DIAGNOSTICS" 2>&1
|
||||
logger -t haproxy-manager "HAProxy recovered after $attempt attempt(s)"
|
||||
exit 0
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
set_result failed
|
||||
logger -t haproxy-manager "HAProxy automatic recovery failed after 5 attempts"
|
||||
exit 1
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
SYNC=0
|
||||
LOCK_DIR=/var/lock/haproxy-manager.apply
|
||||
LOCK_HELD=0
|
||||
|
||||
if [ "${1:-}" = --sync ]; then
|
||||
[ "$#" -eq 2 ] || { echo "Usage: rollback [--sync] [last|YYYYMMDD-HHMMSS]" >&2; exit 1; }
|
||||
SYNC=1
|
||||
TARGET="$2"
|
||||
else
|
||||
[ "$#" -le 1 ] || { echo "Usage: rollback [--sync] [last|YYYYMMDD-HHMMSS]" >&2; exit 1; }
|
||||
TARGET="${1:-last}"
|
||||
fi
|
||||
|
||||
release_lock() {
|
||||
[ "$LOCK_HELD" != 1 ] || operation_lock_release "$LOCK_DIR"
|
||||
}
|
||||
|
||||
if [ "$SYNC" != 1 ]; then
|
||||
attempt=0
|
||||
while ! operation_lock_acquire "$LOCK_DIR"; do
|
||||
[ "$attempt" -lt 15 ] || { echo "Another HAProxy Manager operation is still running." >&2; exit 1; }
|
||||
attempt=$((attempt + 1))
|
||||
sleep 1
|
||||
done
|
||||
LOCK_HELD=1
|
||||
trap 'status=$?; release_lock; exit "$status"' EXIT
|
||||
fi
|
||||
|
||||
BASE="$(backup_dir)"
|
||||
[ ! -d "$BASE" ] || BASE="$(resolve_storage_dir "$BASE")" || {
|
||||
echo "Recovery point directory resolves outside /root or /mnt" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ "$TARGET" = last ]; then
|
||||
[ -f "$BASE/LAST" ] || { echo "No LAST backup marker found" >&2; exit 1; }
|
||||
TARGET="$(basename "$(cat "$BASE/LAST")")"
|
||||
fi
|
||||
|
||||
is_valid_backup_id "$TARGET" || { echo "Invalid backup identifier" >&2; exit 1; }
|
||||
TARGET_DIR="$BASE/$TARGET"
|
||||
[ -d "$TARGET_DIR" ] || { echo "Backup not found: $TARGET" >&2; exit 1; }
|
||||
|
||||
firewall_changed=0
|
||||
uhttpd_changed=0
|
||||
state_failed=0
|
||||
|
||||
if [ -f "$TARGET_DIR/firewall" ] && ! cmp -s "$TARGET_DIR/firewall" /etc/config/firewall; then
|
||||
atomic_install_file "$TARGET_DIR/firewall" /etc/config/firewall
|
||||
firewall_changed=1
|
||||
fi
|
||||
if [ -f "$TARGET_DIR/uhttpd" ] && ! cmp -s "$TARGET_DIR/uhttpd" /etc/config/uhttpd; then
|
||||
atomic_install_file "$TARGET_DIR/uhttpd" /etc/config/uhttpd
|
||||
uhttpd_changed=1
|
||||
fi
|
||||
[ -f "$TARGET_DIR/haproxy_manager" ] && atomic_install_file "$TARGET_DIR/haproxy_manager" /etc/config/haproxy_manager
|
||||
[ -f "$TARGET_DIR/haproxy.cfg" ] && atomic_install_file "$TARGET_DIR/haproxy.cfg" "$HAPROXY_CFG"
|
||||
|
||||
if [ "$firewall_changed" = 1 ] && ! /etc/init.d/firewall restart >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Firewall restart failed while restoring $TARGET"
|
||||
state_failed=1
|
||||
fi
|
||||
|
||||
haproxy_enabled=1
|
||||
haproxy_running=1
|
||||
if [ -f "$TARGET_DIR/haproxy.state" ]; then
|
||||
haproxy_enabled="$(sed -n 's/^enabled=//p' "$TARGET_DIR/haproxy.state" | head -1)"
|
||||
haproxy_running="$(sed -n 's/^running=//p' "$TARGET_DIR/haproxy.state" | head -1)"
|
||||
fi
|
||||
|
||||
if [ "$haproxy_enabled" = 1 ]; then
|
||||
if ! /etc/init.d/haproxy enable >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Could not enable HAProxy while restoring $TARGET"
|
||||
state_failed=1
|
||||
fi
|
||||
else
|
||||
if ! /etc/init.d/haproxy disable >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Could not disable HAProxy while restoring $TARGET"
|
||||
state_failed=1
|
||||
fi
|
||||
fi
|
||||
|
||||
restore_services() {
|
||||
local failed="$state_failed"
|
||||
if [ "$uhttpd_changed" = 1 ] && ! /etc/init.d/uhttpd restart >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "LuCI restart failed while restoring $TARGET"
|
||||
failed=1
|
||||
fi
|
||||
if [ "$haproxy_running" = 1 ]; then
|
||||
if ! /etc/init.d/haproxy restart >/dev/null 2>&1 ||
|
||||
! /etc/init.d/haproxy status >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "HAProxy restart failed while restoring $TARGET"
|
||||
failed=1
|
||||
fi
|
||||
else
|
||||
if ! /etc/init.d/haproxy stop >/dev/null 2>&1 ||
|
||||
/etc/init.d/haproxy status >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "HAProxy stop failed while restoring $TARGET"
|
||||
failed=1
|
||||
fi
|
||||
fi
|
||||
return "$failed"
|
||||
}
|
||||
|
||||
if [ "$SYNC" = 1 ]; then
|
||||
restore_services || exit 1
|
||||
else
|
||||
LOCK_HELD=0
|
||||
trap - EXIT
|
||||
(
|
||||
sleep 2
|
||||
if ! restore_services; then
|
||||
logger -t haproxy-manager "Service restoration failed for recovery point $TARGET"
|
||||
fi
|
||||
operation_lock_release "$LOCK_DIR"
|
||||
) >/dev/null 2>&1 &
|
||||
operation_lock_set_pid "$LOCK_DIR" "$!"
|
||||
fi
|
||||
|
||||
echo "Restored backup $TARGET. Affected services will reload shortly."
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/bin/sh
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
enabled="$(uci_get main enabled 0)"
|
||||
wan="$(wan_ip 2>/dev/null || echo unknown)"
|
||||
version="not-available"
|
||||
service="stopped"
|
||||
backup_count=0
|
||||
auto_recovery="$(uci_get main auto_recover 1)"
|
||||
mode="$(active_mode)"
|
||||
last_incident=""
|
||||
last_incident_result="none"
|
||||
last_incident_interface=""
|
||||
|
||||
if command -v haproxy >/dev/null 2>&1; then
|
||||
version="$(haproxy -v 2>/dev/null | sed -n '1s/^HAProxy version \([^ ]*\).*/\1/p')"
|
||||
[ -n "$version" ] || version="unknown"
|
||||
fi
|
||||
|
||||
if [ -x /etc/init.d/haproxy ] && /etc/init.d/haproxy status >/dev/null 2>&1; then
|
||||
service="running"
|
||||
fi
|
||||
|
||||
backup_count="$(/usr/libexec/haproxy-manager/backups 2>/dev/null | wc -l | tr -d ' ')"
|
||||
|
||||
INCIDENT_BASE="$(incident_dir)"
|
||||
if [ -d "$INCIDENT_BASE" ]; then
|
||||
INCIDENT_BASE="$(resolve_storage_dir "$INCIDENT_BASE")" || INCIDENT_BASE=""
|
||||
fi
|
||||
if [ -f "$INCIDENT_BASE/LAST" ]; then
|
||||
last_incident="$(sed -n '1p' "$INCIDENT_BASE/LAST")"
|
||||
if is_valid_backup_id "$last_incident" && [ -d "$INCIDENT_BASE/$last_incident" ]; then
|
||||
last_incident_result="$(sed -n '1p' "$INCIDENT_BASE/$last_incident/result" 2>/dev/null || echo detected)"
|
||||
last_incident_interface="$(sed -n 's/^interface=//p' "$INCIDENT_BASE/$last_incident/meta" 2>/dev/null | head -1)"
|
||||
else
|
||||
last_incident=""
|
||||
last_incident_result="none"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf 'enabled\t%s\n' "$enabled"
|
||||
printf 'wan_ip\t%s\n' "$wan"
|
||||
printf 'version\t%s\n' "$version"
|
||||
printf 'service\t%s\n' "$service"
|
||||
printf 'backup_count\t%s\n' "$backup_count"
|
||||
printf 'auto_recovery\t%s\n' "$auto_recovery"
|
||||
printf 'active_mode\t%s\n' "$mode"
|
||||
printf 'last_incident\t%s\n' "$last_incident"
|
||||
printf 'last_incident_result\t%s\n' "$last_incident_result"
|
||||
printf 'last_incident_interface\t%s\n' "$last_incident_interface"
|
||||
|
||||
netstat -lntp 2>/dev/null | awk '
|
||||
NR > 2 && $6 == "LISTEN" && ($7 ~ /haproxy/ || $7 ~ /uhttpd/) {
|
||||
printf "listener\t%s\t%s\t%s\n", $1, $4, $7
|
||||
}
|
||||
'
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/bin/sh
|
||||
set -u
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
LOCK_DIR=/var/lock/haproxy-manager.apply
|
||||
LOCK_HELD=0
|
||||
UHTTPD_CHANGED=0
|
||||
MODE="$(active_mode)"
|
||||
FAILED=0
|
||||
|
||||
attempt=0
|
||||
while ! operation_lock_acquire "$LOCK_DIR"; do
|
||||
[ "$attempt" -lt 15 ] || {
|
||||
logger -t haproxy-manager "Package removal could not acquire the operation lock"
|
||||
exit 1
|
||||
}
|
||||
attempt=$((attempt + 1))
|
||||
sleep 1
|
||||
done
|
||||
LOCK_HELD=1
|
||||
trap '[ "$LOCK_HELD" != 1 ] || operation_lock_release "$LOCK_DIR"' EXIT
|
||||
|
||||
/usr/libexec/haproxy-manager/backup >/dev/null 2>&1 ||
|
||||
logger -t haproxy-manager "Could not create a final recovery point before package removal"
|
||||
|
||||
if ! restore_uhttpd_bindings; then
|
||||
logger -t haproxy-manager "Could not restore the original LuCI listener bindings"
|
||||
FAILED=1
|
||||
fi
|
||||
if [ "$UHTTPD_CHANGED" = 1 ] && ! uci commit uhttpd; then
|
||||
logger -t haproxy-manager "Could not commit the restored LuCI listener bindings"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
if ! /usr/libexec/haproxy-manager/firewall-sync --disable >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Could not remove owned firewall state"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
case "$MODE" in
|
||||
generated|raw)
|
||||
if ! /etc/init.d/haproxy stop >/dev/null 2>&1 ||
|
||||
/etc/init.d/haproxy status >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Could not stop HAProxy during package removal"
|
||||
FAILED=1
|
||||
fi
|
||||
if ! /etc/init.d/haproxy disable >/dev/null 2>&1; then
|
||||
logger -t haproxy-manager "Could not disable HAProxy during package removal"
|
||||
FAILED=1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$UHTTPD_CHANGED" = 1 ]; then
|
||||
/etc/init.d/uhttpd restart >/dev/null 2>&1 ||
|
||||
{ logger -t haproxy-manager "Could not restart LuCI after restoring its listeners"; FAILED=1; }
|
||||
fi
|
||||
|
||||
if [ "$FAILED" != 0 ]; then
|
||||
logger -t haproxy-manager "Package runtime cleanup is incomplete; removal was stopped"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
uci set "$CONFIG.main.active_mode=none"
|
||||
uci commit "$CONFIG"
|
||||
|
||||
LOCK_HELD=0
|
||||
operation_lock_release "$LOCK_DIR"
|
||||
logger -t haproxy-manager "Removed package-owned HAProxy, firewall, and LuCI runtime state"
|
||||
exit 0
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
. /usr/libexec/haproxy-manager/common.sh
|
||||
|
||||
CFG="${1:-/etc/haproxy.cfg}"
|
||||
if [ "$CFG" != "$HAPROXY_CFG" ]; then
|
||||
is_safe_temp_path "$CFG" || { echo "Refusing unsafe HAProxy config path: $CFG" >&2; exit 1; }
|
||||
fi
|
||||
if ! command -v haproxy >/dev/null 2>&1; then
|
||||
echo "haproxy binary is not installed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
haproxy -c -f "$CFG"
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"admin/services/haproxy-manager": {
|
||||
"title": "HAProxy",
|
||||
"i18n": "haproxy-manager",
|
||||
"order": 60,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-haproxy-manager" ]
|
||||
}
|
||||
},
|
||||
"admin/services/haproxy-manager/status": {
|
||||
"title": "Status",
|
||||
"i18n": "haproxy-manager",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "haproxy-manager/status"
|
||||
}
|
||||
},
|
||||
"admin/services/haproxy-manager/routes": {
|
||||
"title": "Services",
|
||||
"i18n": "haproxy-manager",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "haproxy-manager/routes"
|
||||
}
|
||||
},
|
||||
"admin/services/haproxy-manager/settings": {
|
||||
"title": "Settings",
|
||||
"i18n": "haproxy-manager",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "haproxy-manager/settings"
|
||||
}
|
||||
},
|
||||
"admin/services/haproxy-manager/raw": {
|
||||
"title": "Raw Config",
|
||||
"i18n": "haproxy-manager",
|
||||
"order": 40,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "haproxy-manager/raw"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"luci-app-haproxy-manager": {
|
||||
"description": "Grant access to LuCI HAProxy Manager",
|
||||
"read": {
|
||||
"uci": [ "haproxy_manager" ],
|
||||
"file": {
|
||||
"/etc/haproxy.cfg": [ "read" ],
|
||||
"/tmp/haproxy-manager-*": [ "read" ]
|
||||
},
|
||||
"ubus": {
|
||||
"file": [ "read", "stat" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "haproxy_manager" ],
|
||||
"file": {
|
||||
"/usr/libexec/haproxy-manager/status": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/generate": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/apply": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/backup": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/backups": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/incidents": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/incident": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/firewall-plan": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/rollback": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/validate": [ "exec" ],
|
||||
"/usr/libexec/haproxy-manager/apply-raw-file": [ "exec" ],
|
||||
"/tmp/haproxy-manager-*": [ "write" ]
|
||||
},
|
||||
"ubus": {
|
||||
"file": [ "exec", "write" ],
|
||||
"uci": [ "commit" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user