From 0e87c4fe34e3bffd4a66f7a1d8cf263124fcb773 Mon Sep 17 00:00:00 2001 From: Desert Gamer <44316028+DesertGamer@users.noreply.github.com> Date: Fri, 4 Jul 2025 11:38:05 +0300 Subject: [PATCH] Update Iptag (#5677) --- tools/pve/add-iptag.sh | 1484 ++++++++++++++++++++++++++-------------- 1 file changed, 988 insertions(+), 496 deletions(-) diff --git a/tools/pve/add-iptag.sh b/tools/pve/add-iptag.sh index ba81b838e..8a0f42306 100644 --- a/tools/pve/add-iptag.sh +++ b/tools/pve/add-iptag.sh @@ -3,7 +3,6 @@ # Copyright (c) 2021-2025 community-scripts ORG # Author: MickLesk (Canbiz) && Desert_Gamer # License: MIT -# Source: https://github.com/gitsang/iptag function header_info { clear @@ -22,7 +21,7 @@ header_info APP="IP-Tag" hostname=$(hostname) -# Farbvariablen +# Color variables YW=$(echo "\033[33m") GN=$(echo "\033[1;92m") RD=$(echo "\033[01;31m") @@ -32,13 +31,7 @@ HOLD=" " CM=" ✔️ ${CL}" CROSS=" ✖️ ${CL}" -# This function enables error handling in the script by setting options and defining a trap for the ERR signal. -catch_errors() { - set -Eeuo pipefail - trap 'error_handler $LINENO "$BASH_COMMAND"' ERR -} - -# This function is called when an error occurs. It receives the exit code, line number, and command that caused the error, and displays an error message. +# Error handler for displaying error messages error_handler() { if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID >/dev/null; then kill $SPINNER_PID >/dev/null @@ -51,7 +44,7 @@ error_handler() { echo -e "\n$error_message\n" } -# This function displays a spinner. +# Spinner for progress indication spinner() { local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') local spin_i=0 @@ -67,7 +60,7 @@ spinner() { done } -# This function displays an informational message with a yellow color. +# Info message msg_info() { local msg="$1" echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}" @@ -75,7 +68,7 @@ msg_info() { SPINNER_PID=$! } -# This function displays a success message with a green color. +# Success message msg_ok() { if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID >/dev/null; then kill $SPINNER_PID >/dev/null @@ -85,7 +78,7 @@ msg_ok() { echo -e "${BFR}${CM}${GN}${msg}${CL}" } -# This function displays a error message with a red color. +# Error message msg_error() { if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID >/dev/null; then kill $SPINNER_PID >/dev/null @@ -124,17 +117,122 @@ migrate_config() { update_installation() { msg_info "Updating IP-Tag Scripts" systemctl stop iptag.service &>/dev/null + msg_ok "Stopped IP-Tag service" # Create directory if it doesn't exist if [[ ! -d "/opt/iptag" ]]; then mkdir -p /opt/iptag fi - # Migrate config if needed - migrate_config + # Create new config file (check if exists and ask user) + if [[ -f "/opt/iptag/iptag.conf" ]]; then + echo -e "\n${YW}Configuration file already exists.${CL}" + while true; do + read -p "Do you want to replace it with defaults? (y/n): " yn + case $yn in + [Yy]*) + msg_info "Replacing configuration file" + generate_config >/opt/iptag/iptag.conf + msg_ok "Configuration file replaced with defaults" + break + ;; + [Nn]*) + echo -e "${GN}✔️ Keeping existing configuration file${CL}" + break + ;; + *) + echo -e "${RD}Please answer yes or no.${CL}" + ;; + esac + done + else + msg_info "Creating new configuration file" + generate_config >/opt/iptag/iptag.conf + msg_ok "Created new configuration file at /opt/iptag/iptag.conf" + fi # Update main script - cat <<'EOF' >/opt/iptag/iptag + msg_info "Updating main script" + generate_main_script >/opt/iptag/iptag + chmod +x /opt/iptag/iptag + msg_ok "Updated main script" + + # Update service file + msg_info "Updating service file" + generate_service >/lib/systemd/system/iptag.service + msg_ok "Updated service file" + + msg_info "Restarting service" + systemctl daemon-reload &>/dev/null + systemctl enable -q --now iptag.service &>/dev/null + msg_ok "Updated IP-Tag Scripts" +} + +# Generate configuration file content +generate_config() { + cat <&2 + fi +} + +# Color constants +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[0;33m' +readonly BLUE='\033[0;34m' +readonly PURPLE='\033[0;35m' +readonly CYAN='\033[0;36m' +readonly WHITE='\033[1;37m' +readonly GRAY='\033[0;37m' +readonly NC='\033[0m' # No Color + +# Logging functions with colors +log_success() { + echo -e "${GREEN}✓${NC} $*" +} + +log_info() { + echo -e "${BLUE}ℹ${NC} $*" +} + +log_warning() { + echo -e "${YELLOW}⚠${NC} $*" +} + +log_error() { + echo -e "${RED}✗${NC} $*" +} + +log_change() { + echo -e "${CYAN}~${NC} $*" +} + +log_unchanged() { + echo -e "${GRAY}=${NC} $*" } # Check if IP is in CIDR ip_in_cidr() { local ip="$1" cidr="$2" - ipcalc -c "$ip" "$cidr" >/dev/null 2>&1 || return 1 - - local network prefix ip_parts net_parts - network=$(echo "$cidr" | cut -d/ -f1) - prefix=$(echo "$cidr" | cut -d/ -f2) - IFS=. read -r -a ip_parts <<< "$ip" - IFS=. read -r -a net_parts <<< "$network" - - case $prefix in - 8) [[ "${ip_parts[0]}" == "${net_parts[0]}" ]] ;; - 16) [[ "${ip_parts[0]}.${ip_parts[1]}" == "${net_parts[0]}.${net_parts[1]}" ]] ;; - 24) [[ "${ip_parts[0]}.${ip_parts[1]}.${ip_parts[2]}" == "${net_parts[0]}.${net_parts[1]}.${net_parts[2]}" ]] ;; - 32) [[ "$ip" == "$network" ]] ;; - *) return 1 ;; - esac + debug_log "ip_in_cidr: checking '$ip' against '$cidr'" + + # Manual CIDR check - более надёжный метод + debug_log "ip_in_cidr: using manual check (bypassing ipcalc)" + local network prefix + IFS='/' read -r network prefix <<< "$cidr" + + # Convert IP and network to integers for comparison + local ip_int net_int mask + IFS='.' read -r a b c d <<< "$ip" + ip_int=$(( (a << 24) + (b << 16) + (c << 8) + d )) + + IFS='.' read -r a b c d <<< "$network" + net_int=$(( (a << 24) + (b << 16) + (c << 8) + d )) + + # Create subnet mask + mask=$(( 0xFFFFFFFF << (32 - prefix) )) + + # Apply mask and compare + local ip_masked=$((ip_int & mask)) + local net_masked=$((net_int & mask)) + + debug_log "ip_in_cidr: IP=$ip ($ip_int), Network=$network ($net_int), Prefix=$prefix" + debug_log "ip_in_cidr: Mask=$mask (hex: $(printf '0x%08x' $mask))" + debug_log "ip_in_cidr: IP&Mask=$ip_masked ($(printf '%d.%d.%d.%d' $((ip_masked>>24&255)) $((ip_masked>>16&255)) $((ip_masked>>8&255)) $((ip_masked&255))))" + debug_log "ip_in_cidr: Net&Mask=$net_masked ($(printf '%d.%d.%d.%d' $((net_masked>>24&255)) $((net_masked>>16&255)) $((net_masked>>8&255)) $((net_masked&255))))" + + if (( ip_masked == net_masked )); then + debug_log "ip_in_cidr: manual check PASSED - IP is in CIDR" + return 0 + else + debug_log "ip_in_cidr: manual check FAILED - IP is NOT in CIDR" + return 1 + fi } # Format IP address according to the configuration format_ip_tag() { local ip="$1" + [[ -z "$ip" ]] && return local format="${TAG_FORMAT:-$DEFAULT_TAG_FORMAT}" - case "$format" in "last_octet") echo "${ip##*.}" ;; "last_two_octets") echo "${ip#*.*.}" ;; @@ -192,7 +347,17 @@ ip_in_cidrs() { local ip="$1" cidrs="$2" [[ -z "$cidrs" ]] && return 1 local IFS=' ' - for cidr in $cidrs; do ip_in_cidr "$ip" "$cidr" && return 0; done + debug_log "Checking IP '$ip' against CIDRs: '$cidrs'" + for cidr in $cidrs; do + debug_log "Testing IP '$ip' against CIDR '$cidr'" + if ip_in_cidr "$ip" "$cidr"; then + debug_log "IP '$ip' matches CIDR '$cidr' - PASSED" + return 0 + else + debug_log "IP '$ip' does not match CIDR '$cidr'" + fi + done + debug_log "IP '$ip' failed all CIDR checks" return 1 } @@ -209,76 +374,223 @@ is_valid_ipv4() { return 0 } -lxc_status_changed() { - current_lxc_status=$(pct list 2>/dev/null) - if [ "${last_lxc_status}" == "${current_lxc_status}" ]; then - return 1 - else - last_lxc_status="${current_lxc_status}" - return 0 - fi -} - -vm_status_changed() { - current_vm_status=$(qm list 2>/dev/null) - if [ "${last_vm_status}" == "${current_vm_status}" ]; then - return 1 - else - last_vm_status="${current_vm_status}" - return 0 - fi -} - -fw_net_interface_changed() { - current_net_interface=$(ifconfig | grep "^fw") - if [ "${last_net_interface}" == "${current_net_interface}" ]; then - return 1 - else - last_net_interface="${current_net_interface}" - return 0 - fi -} - -# Get VM IPs using MAC addresses and ARP table +# Get VM IPs using multiple methods with performance optimizations get_vm_ips() { - local vmid=$1 ips="" macs found_ip=false - qm status "$vmid" 2>/dev/null | grep -q "status: running" || return - - macs=$(qm config "$vmid" 2>/dev/null | grep -E 'net[0-9]+' | grep -oE '[a-fA-F0-9]{2}(:[a-fA-F0-9]{2}){5}') - [[ -z "$macs" ]] && return - - for mac in $macs; do - local ip - ip=$(arp -an 2>/dev/null | grep -i "$mac" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}') - [[ -n "$ip" ]] && { ips+="$ip "; found_ip=true; } - done - - if ! $found_ip; then - local agent_ip - agent_ip=$(qm agent "$vmid" network-get-interfaces 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' || true) - [[ -n "$agent_ip" ]] && ips+="$agent_ip " + local vmid=$1 ips="" + local vm_config="/etc/pve/qemu-server/${vmid}.conf" + [[ ! -f "$vm_config" ]] && return + + debug_log "vm $vmid: starting optimized IP detection" + + # Check if VM is running first (avoid expensive operations for stopped VMs) + local vm_status="" + if command -v qm >/dev/null 2>&1; then + vm_status=$(qm status "$vmid" 2>/dev/null | awk '{print $2}') fi - - echo "${ips% }" + + if [[ "$vm_status" != "running" ]]; then + debug_log "vm $vmid: not running (status: $vm_status), skipping expensive detection" + return + fi + + # Cache for this execution + local cache_file="/tmp/iptag_vm_${vmid}_cache" + local cache_ttl=60 # 60 seconds cache + + # Check cache first + if [[ -f "$cache_file" ]] && [[ $(($(date +%s) - $(stat -c %Y "$cache_file" 2>/dev/null || echo 0))) -lt $cache_ttl ]]; then + local cached_ips=$(cat "$cache_file" 2>/dev/null) + if [[ -n "$cached_ips" ]]; then + debug_log "vm $vmid: using cached IPs: $cached_ips" + echo "$cached_ips" + return + fi + fi + + # Method 1: Quick ARP table lookup (fastest) + local mac_addresses=$(grep -E "^net[0-9]+:" "$vm_config" | grep -oE "([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}" | head -3) + debug_log "vm $vmid: found MACs: $mac_addresses" + + # Quick ARP check without forced refresh (most common case) + for mac in $mac_addresses; do + local mac_lower=$(echo "$mac" | tr '[:upper:]' '[:lower:]') + local ip=$(ip neighbor show | grep "$mac_lower" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1) + if [[ -n "$ip" && "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + debug_log "vm $vmid: found IP $ip via quick ARP for MAC $mac_lower" + ips+="$ip " + fi + done + + # Early exit if we found IPs via ARP + if [[ -n "$ips" ]]; then + local unique_ips=$(echo "$ips" | tr ' ' '\n' | sort -u | tr '\n' ' ') + unique_ips="${unique_ips% }" + debug_log "vm $vmid: early exit with IPs: '$unique_ips'" + echo "$unique_ips" > "$cache_file" + echo "$unique_ips" + return + fi + + # Method 2: QM guest agent (fast if available) + if command -v qm >/dev/null 2>&1; then + local qm_ips=$(timeout 3 qm guest cmd "$vmid" network-get-interfaces 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | grep -v "127.0.0.1" | head -2) + for qm_ip in $qm_ips; do + if [[ "$qm_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + debug_log "vm $vmid: found IP $qm_ip via qm guest cmd" + ips+="$qm_ip " + fi + done + fi + + # Early exit if we found IPs via QM + if [[ -n "$ips" ]]; then + local unique_ips=$(echo "$ips" | tr ' ' '\n' | sort -u | tr '\n' ' ') + unique_ips="${unique_ips% }" + debug_log "vm $vmid: early exit with QM IPs: '$unique_ips'" + echo "$unique_ips" > "$cache_file" + echo "$unique_ips" + return + fi + + # Method 3: DHCP leases check (medium cost) + for mac in $mac_addresses; do + local mac_lower=$(echo "$mac" | tr '[:upper:]' '[:lower:]') + + for dhcp_file in "/var/lib/dhcp/dhcpd.leases" "/var/lib/dhcpcd5/dhcpcd.leases" "/tmp/dhcp.leases"; do + if [[ -f "$dhcp_file" ]]; then + local dhcp_ip=$(timeout 2 grep -A 10 "ethernet $mac_lower" "$dhcp_file" 2>/dev/null | grep "binding state active" -A 5 | grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}" | head -1) + if [[ -n "$dhcp_ip" && "$dhcp_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + debug_log "vm $vmid: found IP $dhcp_ip via DHCP leases for MAC $mac_lower" + ips+="$dhcp_ip " + break 2 + fi + fi + done + done + + # Early exit if we found IPs via DHCP + if [[ -n "$ips" ]]; then + local unique_ips=$(echo "$ips" | tr ' ' '\n' | sort -u | tr '\n' ' ') + unique_ips="${unique_ips% }" + debug_log "vm $vmid: early exit with DHCP IPs: '$unique_ips'" + echo "$unique_ips" > "$cache_file" + echo "$unique_ips" + return + fi + + # Method 4: Limited network discovery (expensive - only if really needed) + debug_log "vm $vmid: falling back to limited network discovery" + + for mac in $mac_addresses; do + local mac_lower=$(echo "$mac" | tr '[:upper:]' '[:lower:]') + + # Get bridge interfaces + local bridges=$(grep -E "^net[0-9]+:" "$vm_config" | grep -oE "bridge=\w+" | cut -d= -f2 | head -1) + for bridge in $bridges; do + if [[ -n "$bridge" && -d "/sys/class/net/$bridge" ]]; then + # Get bridge IP range + local bridge_ip=$(ip addr show "$bridge" 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]+' | head -1) + if [[ -n "$bridge_ip" ]]; then + local network=$(echo "$bridge_ip" | cut -d'/' -f1) + debug_log "vm $vmid: limited scan on bridge $bridge network $bridge_ip" + + # Force ARP refresh with broadcast ping (limited) + IFS='.' read -r a b c d <<< "$network" + local broadcast="$a.$b.$c.255" + timeout 1 ping -c 1 -b "$broadcast" >/dev/null 2>&1 || true + + # Check ARP again after refresh + sleep 0.5 + local ip=$(ip neighbor show | grep "$mac_lower" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1) + if [[ -n "$ip" && "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + debug_log "vm $vmid: found IP $ip via ARP after broadcast for MAC $mac_lower" + ips+="$ip " + break 2 + fi + + # Only do very limited ping scan (reduced range) + IFS='.' read -r a b c d <<< "$network" + local base_net="$a.$b.$c" + + # Try only most common ranges (much smaller than before) + for last_octet in {100..105} {200..205}; do + local test_ip="$base_net.$last_octet" + + # Very quick ping test (reduced timeout) + if timeout 0.2 ping -c 1 -W 1 "$test_ip" >/dev/null 2>&1; then + # Check if this IP corresponds to our MAC + sleep 0.1 + local found_mac=$(ip neighbor show "$test_ip" 2>/dev/null | grep -oE "([0-9a-f]{2}:){5}[0-9a-f]{2}") + if [[ "$found_mac" == "$mac_lower" ]]; then + debug_log "vm $vmid: found IP $test_ip via limited ping scan for MAC $mac_lower" + ips+="$test_ip " + break 2 + fi + fi + done + + # Skip extended scanning entirely (too expensive) + debug_log "vm $vmid: skipping extended scan to preserve CPU" + fi + fi + done + done + + # Method 5: Static configuration check (fast) + if [[ -z "$ips" ]]; then + debug_log "vm $vmid: checking for static IP configuration" + + # Check cloud-init configuration if exists + local cloudinit_file="/var/lib/vz/snippets/${vmid}-cloud-init.yml" + if [[ -f "$cloudinit_file" ]]; then + local static_ip=$(grep -E "addresses?:" "$cloudinit_file" 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1) + if [[ -n "$static_ip" && "$static_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + debug_log "vm $vmid: found static IP $static_ip in cloud-init config" + ips+="$static_ip " + fi + fi + + # Check VM config for any IP hints + local config_ip=$(grep -E "(ip=|gw=)" "$vm_config" 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1) + if [[ -n "$config_ip" && "$config_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + debug_log "vm $vmid: found IP hint $config_ip in VM config" + ips+="$config_ip " + fi + fi + + # Remove duplicates and cache result + local unique_ips=$(echo "$ips" | tr ' ' '\n' | sort -u | tr '\n' ' ') + unique_ips="${unique_ips% }" + + # Cache the result (even if empty) + echo "$unique_ips" > "$cache_file" + + debug_log "vm $vmid: final optimized IPs: '$unique_ips'" + echo "$unique_ips" } -# Update tags +# Update tags for container or VM update_tags() { - local type="$1" vmid="$2" config_cmd="pct" - [[ "$type" == "vm" ]] && config_cmd="qm" - + local type="$1" vmid="$2" local current_ips_full + if [[ "$type" == "lxc" ]]; then - current_ips_full=$(lxc-info -n "${vmid}" -i 2>/dev/null | grep -E "^IP:" | awk '{print $2}') + current_ips_full=$(get_lxc_ips "${vmid}") + local current_tags_raw=$(pct config "${vmid}" 2>/dev/null | grep tags | awk '{print $2}') else current_ips_full=$(get_vm_ips "${vmid}") + local vm_config="/etc/pve/qemu-server/${vmid}.conf" + if [[ -f "$vm_config" ]]; then + local current_tags_raw=$(grep "^tags:" "$vm_config" 2>/dev/null | cut -d: -f2 | sed 's/^[[:space:]]*//') + fi fi - [[ -z "$current_ips_full" ]] && return local current_tags=() next_tags=() current_ip_tags=() - mapfile -t current_tags < <($config_cmd config "${vmid}" 2>/dev/null | grep tags | awk '{print $2}' | sed 's/;/\n/g') + if [[ -n "$current_tags_raw" ]]; then + mapfile -t current_tags < <(echo "$current_tags_raw" | sed 's/;/\n/g') + fi - # Separate IP and non-IP tags + # Separate IP/numeric and user tags for tag in "${current_tags[@]}"; do if is_valid_ipv4 "${tag}" || [[ "$tag" =~ ^[0-9]+(\.[0-9]+)*$ ]]; then current_ip_tags+=("${tag}") @@ -287,48 +599,185 @@ update_tags() { fi done - local formatted_ips=() needs_update=false added_ips=() - for ip in ${current_ips_full}; do - if is_valid_ipv4 "$ip" && ip_in_cidrs "$ip" "${CIDR_LIST[*]}"; then - local formatted_ip=$(format_ip_tag "$ip") - formatted_ips+=("$formatted_ip") - if [[ ! " ${current_ip_tags[*]} " =~ " ${formatted_ip} " ]]; then - needs_update=true - added_ips+=("$formatted_ip") - next_tags+=("$formatted_ip") + # Generate new IP tags from current IPs + local formatted_ips=() + debug_log "$type $vmid current_ips_full: '$current_ips_full'" + debug_log "$type $vmid CIDR_LIST: ${CIDR_LIST[*]}" + for ip in $current_ips_full; do + [[ -z "$ip" ]] && continue + debug_log "$type $vmid processing IP: '$ip'" + if is_valid_ipv4 "$ip"; then + debug_log "$type $vmid IP '$ip' is valid" + if ip_in_cidrs "$ip" "${CIDR_LIST[*]}"; then + debug_log "$type $vmid IP '$ip' passed CIDR check" + local formatted_ip=$(format_ip_tag "$ip") + debug_log "$type $vmid formatted '$ip' -> '$formatted_ip'" + [[ -n "$formatted_ip" ]] && formatted_ips+=("$formatted_ip") + else + debug_log "$type $vmid IP '$ip' failed CIDR check" fi + else + debug_log "$type $vmid IP '$ip' is invalid" fi done + debug_log "$type $vmid final formatted_ips: ${formatted_ips[*]}" - [[ ${#formatted_ips[@]} -eq 0 ]] && return + # If LXC and no IPs detected, do not touch tags at all + if [[ "$type" == "lxc" && ${#formatted_ips[@]} -eq 0 ]]; then + log_unchanged "LXC ${GRAY}${vmid}${NC}: No IP detected, tags unchanged" + return + fi - # Add existing IP tags that are still valid - for tag in "${current_ip_tags[@]}"; do - if [[ " ${formatted_ips[*]} " =~ " ${tag} " ]]; then - if [[ ! " ${next_tags[*]} " =~ " ${tag} " ]]; then - next_tags+=("$tag") - fi - fi + # Add new IP tags + for new_ip in "${formatted_ips[@]}"; do + next_tags+=("$new_ip") done - if [[ "$needs_update" == true ]]; then - echo "${type^} ${vmid}: adding IP tags: ${added_ips[*]}" - $config_cmd set "${vmid}" -tags "$(IFS=';'; echo "${next_tags[*]}")" &>/dev/null - elif [[ ${#current_ip_tags[@]} -gt 0 ]]; then - echo "${type^} ${vmid}: IP tags already set: ${current_ip_tags[*]}" + # Update tags if there are changes + local old_tags_str=$(IFS=';'; echo "${current_tags[*]}") + local new_tags_str=$(IFS=';'; echo "${next_tags[*]}") + + debug_log "$type $vmid old_tags: '$old_tags_str'" + debug_log "$type $vmid new_tags: '$new_tags_str'" + debug_log "$type $vmid tags_equal: $([[ "$old_tags_str" == "$new_tags_str" ]] && echo true || echo false)" + + if [[ "$old_tags_str" != "$new_tags_str" ]]; then + # Determine what changed + local old_ip_tags_count=${#current_ip_tags[@]} + local new_ip_tags_count=${#formatted_ips[@]} + + # Build detailed change message + local change_details="" + + if [[ $old_ip_tags_count -eq 0 ]]; then + change_details="added ${new_ip_tags_count} IP tag(s): [${GREEN}${formatted_ips[*]}${NC}]" + else + # Compare old and new IP tags + local added_tags=() removed_tags=() common_tags=() + + # Find removed tags + for old_tag in "${current_ip_tags[@]}"; do + local found=false + for new_tag in "${formatted_ips[@]}"; do + if [[ "$old_tag" == "$new_tag" ]]; then + found=true + break + fi + done + if [[ "$found" == false ]]; then + removed_tags+=("$old_tag") + else + common_tags+=("$old_tag") + fi + done + + # Find added tags + for new_tag in "${formatted_ips[@]}"; do + local found=false + for old_tag in "${current_ip_tags[@]}"; do + if [[ "$new_tag" == "$old_tag" ]]; then + found=true + break + fi + done + if [[ "$found" == false ]]; then + added_tags+=("$new_tag") + fi + done + + # Build change message + local change_parts=() + if [[ ${#added_tags[@]} -gt 0 ]]; then + change_parts+=("added [${GREEN}${added_tags[*]}${NC}]") + fi + if [[ ${#removed_tags[@]} -gt 0 ]]; then + change_parts+=("removed [${YELLOW}${removed_tags[*]}${NC}]") + fi + if [[ ${#common_tags[@]} -gt 0 ]]; then + change_parts+=("kept [${GRAY}${common_tags[*]}${NC}]") + fi + + change_details=$(IFS=', '; echo "${change_parts[*]}") + fi + + log_change "${type^^} ${CYAN}${vmid}${NC}: ${change_details}" + + if [[ "$type" == "lxc" ]]; then + pct set "${vmid}" -tags "$(IFS=';'; echo "${next_tags[*]}")" &>/dev/null + else + local vm_config="/etc/pve/qemu-server/${vmid}.conf" + if [[ -f "$vm_config" ]]; then + sed -i '/^tags:/d' "$vm_config" + if [[ ${#next_tags[@]} -gt 0 ]]; then + echo "tags: $(IFS=';'; echo "${next_tags[*]}")" >> "$vm_config" + fi + fi + fi else - echo "${type^} ${vmid}: setting initial IP tags: ${formatted_ips[*]}" - $config_cmd set "${vmid}" -tags "$(IFS=';'; echo "${formatted_ips[*]}")" &>/dev/null + # Tags unchanged + local ip_count=${#formatted_ips[@]} + local status_msg="" + + if [[ $ip_count -eq 0 ]]; then + status_msg="No IPs detected" + elif [[ $ip_count -eq 1 ]]; then + status_msg="IP tag [${GRAY}${formatted_ips[0]}${NC}] unchanged" + else + status_msg="${ip_count} IP tags [${GRAY}${formatted_ips[*]}${NC}] unchanged" + fi + + log_unchanged "${type^^} ${GRAY}${vmid}${NC}: ${status_msg}" + fi +} + +# Update all instances of specified type +update_all_tags() { + local type="$1" vmids count=0 + + if [[ "$type" == "lxc" ]]; then + vmids=($(pct list 2>/dev/null | grep -v VMID | awk '{print $1}')) + else + local all_vm_configs=($(ls /etc/pve/qemu-server/*.conf 2>/dev/null | sed 's/.*\/\([0-9]*\)\.conf/\1/' | sort -n)) + vmids=("${all_vm_configs[@]}") + fi + + count=${#vmids[@]} + [[ $count -eq 0 ]] && return + + # Display processing header with color + if [[ "$type" == "lxc" ]]; then + log_info "Processing ${WHITE}${count}${NC} LXC container(s) in parallel" + + # Clean up old cache files before processing LXC + cleanup_vm_cache + + # Process LXC containers in parallel for better performance + process_lxc_parallel "${vmids[@]}" + else + log_info "Processing ${WHITE}${count}${NC} virtual machine(s) in parallel" + + # Clean up old cache files before processing VMs + cleanup_vm_cache + + # Process VMs in parallel for better performance + process_vms_parallel "${vmids[@]}" + fi + + # Add completion message + if [[ "$type" == "lxc" ]]; then + log_success "Completed processing LXC containers" + else + log_success "Completed processing virtual machines" fi } # Check if status changed -check_status() { +check_status_changed() { local type="$1" current case "$type" in "lxc") current=$(pct list 2>/dev/null | grep -v VMID) ;; - "vm") current=$(qm list 2>/dev/null | grep -v VMID) ;; - "fw") current=$(ifconfig 2>/dev/null | grep "^fw") ;; + "vm") current=$(ls -la /etc/pve/qemu-server/*.conf 2>/dev/null) ;; + "fw") current=$(ip link show type bridge 2>/dev/null) ;; esac local last_var="last_${type}_status" [[ "${!last_var}" == "$current" ]] && return 1 @@ -336,101 +785,431 @@ check_status() { return 0 } -# Update all instances -update_all() { - local type="$1" list_cmd="pct" vmids count=0 - [[ "$type" == "vm" ]] && list_cmd="qm" - - vmids=$($list_cmd list 2>/dev/null | grep -v VMID | awk '{print $1}') - for vmid in $vmids; do ((count++)); done - - echo "Found ${count} running ${type}s" - [[ $count -eq 0 ]] && return - - for vmid in $vmids; do - update_tags "$type" "$vmid" - done -} - # Main check function check() { local current_time changes_detected=false current_time=$(date +%s) + + # Periodic cache cleanup (every 10 minutes) + local time_since_last_cleanup=$((current_time - ${last_cleanup_time:-0})) + if [[ $time_since_last_cleanup -ge 600 ]]; then + cleanup_vm_cache + last_cleanup_time=$current_time + debug_log "Performed periodic cache cleanup" + fi - for type in "lxc" "vm"; do - local interval_var="${type^^}_STATUS_CHECK_INTERVAL" - local last_check_var="last_${type}_check_time" - local last_update_var="last_update_${type}_time" - - if [[ "${!interval_var}" -gt 0 ]] && (( current_time - ${!last_check_var} >= ${!interval_var} )); then - echo "Checking ${type^^} status..." - eval "${last_check_var}=\$current_time" - if check_status "$type"; then - changes_detected=true - update_all "$type" - eval "${last_update_var}=\$current_time" - fi - fi - - if (( current_time - ${!last_update_var} >= FORCE_UPDATE_INTERVAL )); then - echo "Force updating ${type} tags..." + # Check LXC status + local time_since_last_lxc_check=$((current_time - last_lxc_status_check_time)) + if [[ "${LXC_STATUS_CHECK_INTERVAL:-60}" -gt 0 ]] && \ + [[ "${time_since_last_lxc_check}" -ge "${LXC_STATUS_CHECK_INTERVAL:-60}" ]]; then + last_lxc_status_check_time=${current_time} + if check_status_changed "lxc"; then changes_detected=true - update_all "$type" - eval "${last_update_var}=\$current_time" - fi - done - - if [[ "${FW_NET_INTERFACE_CHECK_INTERVAL}" -gt 0 ]] && \ - (( current_time - last_fw_check_time >= FW_NET_INTERFACE_CHECK_INTERVAL )); then - echo "Checking network interfaces..." - last_fw_check_time=$current_time - if check_status "fw"; then - changes_detected=true - update_all "lxc" - update_all "vm" - last_update_lxc_time=$current_time - last_update_vm_time=$current_time + log_warning "LXC status changes detected, updating tags" + update_all_tags "lxc" + last_update_lxc_time=${current_time} fi fi - $changes_detected || echo "No changes detected in system status" + # Check VM status + local time_since_last_vm_check=$((current_time - last_vm_status_check_time)) + if [[ "${VM_STATUS_CHECK_INTERVAL:-60}" -gt 0 ]] && \ + [[ "${time_since_last_vm_check}" -ge "${VM_STATUS_CHECK_INTERVAL:-60}" ]]; then + last_vm_status_check_time=${current_time} + if check_status_changed "vm"; then + changes_detected=true + log_warning "VM status changes detected, updating tags" + update_all_tags "vm" + last_update_vm_time=${current_time} + fi + fi + + # Check network interface changes + local time_since_last_fw_check=$((current_time - last_fw_net_interface_check_time)) + if [[ "${FW_NET_INTERFACE_CHECK_INTERVAL:-60}" -gt 0 ]] && \ + [[ "${time_since_last_fw_check}" -ge "${FW_NET_INTERFACE_CHECK_INTERVAL:-60}" ]]; then + last_fw_net_interface_check_time=${current_time} + if check_status_changed "fw"; then + changes_detected=true + log_warning "Network interface changes detected, updating all tags" + update_all_tags "lxc" + update_all_tags "vm" + last_update_lxc_time=${current_time} + last_update_vm_time=${current_time} + fi + fi + + # Force update if needed + for type in "lxc" "vm"; do + local last_update_var="last_update_${type}_time" + local time_since_last_update=$((current_time - ${!last_update_var})) + if [[ ${time_since_last_update} -ge ${FORCE_UPDATE_INTERVAL:-1800} ]]; then + changes_detected=true + local minutes=$((${FORCE_UPDATE_INTERVAL:-1800} / 60)) + if [[ "$type" == "lxc" ]]; then + log_info "Scheduled LXC update (every ${minutes} minutes)" + else + log_info "Scheduled VM update (every ${minutes} minutes)" + fi + update_all_tags "$type" + eval "${last_update_var}=${current_time}" + fi + done } # Initialize time variables declare -g last_lxc_status="" last_vm_status="" last_fw_status="" -declare -g last_lxc_check_time=0 last_vm_check_time=0 last_fw_check_time=0 -declare -g last_update_lxc_time=0 last_update_vm_time=0 +declare -g last_lxc_status_check_time=0 last_vm_status_check_time=0 last_fw_net_interface_check_time=0 +declare -g last_update_lxc_time=0 last_update_vm_time=0 last_cleanup_time=0 # Main loop main() { - while true; do - check - sleep "${LOOP_INTERVAL:-$DEFAULT_CHECK_INTERVAL}" + # Display startup message + echo -e "\n${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + log_success "IP-Tag service started successfully" + echo -e "${BLUE}ℹ${NC} Loop interval: ${WHITE}${LOOP_INTERVAL:-$DEFAULT_CHECK_INTERVAL}${NC} seconds" + echo -e "${BLUE}ℹ${NC} Debug mode: ${WHITE}${DEBUG:-false}${NC}" + echo -e "${BLUE}ℹ${NC} Tag format: ${WHITE}${TAG_FORMAT:-$DEFAULT_TAG_FORMAT}${NC}" + echo -e "${BLUE}ℹ${NC} Allowed CIDRs: ${WHITE}${CIDR_LIST[*]}${NC}" + echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" + check +} + +# Cache cleanup function +cleanup_vm_cache() { + local cache_dir="/tmp" + local vm_cache_ttl=${VM_IP_CACHE_TTL:-120} + local lxc_cache_ttl=${LXC_IP_CACHE_TTL:-120} + local status_cache_ttl=${LXC_STATUS_CACHE_TTL:-30} + local current_time=$(date +%s) + + debug_log "Starting extreme cache cleanup" + + # Clean VM cache files + for cache_file in "$cache_dir"/iptag_vm_*_cache; do + if [[ -f "$cache_file" ]]; then + local file_time=$(stat -c %Y "$cache_file" 2>/dev/null || echo 0) + if [[ $((current_time - file_time)) -gt $vm_cache_ttl ]]; then + rm -f "$cache_file" 2>/dev/null + debug_log "Cleaned up expired VM cache file: $cache_file" + fi + fi done + + # Clean LXC IP cache files + for cache_file in "$cache_dir"/iptag_lxc_*_cache; do + if [[ -f "$cache_file" ]]; then + local file_time=$(stat -c %Y "$cache_file" 2>/dev/null || echo 0) + if [[ $((current_time - file_time)) -gt $lxc_cache_ttl ]]; then + rm -f "$cache_file" 2>/dev/null + # Also clean meta files + rm -f "${cache_file}.meta" 2>/dev/null + debug_log "Cleaned up expired LXC cache file: $cache_file" + fi + fi + done + + # Clean LXC status cache files (shorter TTL) + for cache_file in "$cache_dir"/iptag_lxc_status_*_cache; do + if [[ -f "$cache_file" ]]; then + local file_time=$(stat -c %Y "$cache_file" 2>/dev/null || echo 0) + if [[ $((current_time - file_time)) -gt $status_cache_ttl ]]; then + rm -f "$cache_file" 2>/dev/null + debug_log "Cleaned up expired LXC status cache: $cache_file" + fi + fi + done + + # Clean LXC PID cache files (60 second TTL) + for cache_file in "$cache_dir"/iptag_lxc_pid_*_cache; do + if [[ -f "$cache_file" ]]; then + local file_time=$(stat -c %Y "$cache_file" 2>/dev/null || echo 0) + if [[ $((current_time - file_time)) -gt 60 ]]; then + rm -f "$cache_file" 2>/dev/null + debug_log "Cleaned up expired LXC PID cache: $cache_file" + fi + fi + done + + # Clean any orphaned meta files + for meta_file in "$cache_dir"/iptag_*.meta; do + if [[ -f "$meta_file" ]]; then + local base_file="${meta_file%.meta}" + if [[ ! -f "$base_file" ]]; then + rm -f "$meta_file" 2>/dev/null + debug_log "Cleaned up orphaned meta file: $meta_file" + fi + fi + done + + debug_log "Completed extreme cache cleanup" +} + +# Parallel VM processing function +process_vms_parallel() { + local vm_list=("$@") + local max_parallel=${MAX_PARALLEL_VM_CHECKS:-5} + local job_count=0 + local pids=() + local pid_start_times=() + + for vmid in "${vm_list[@]}"; do + if [[ $job_count -ge $max_parallel ]]; then + local pid_to_wait="${pids[0]}" + local start_time="${pid_start_times[0]}" + local waited=0 + while kill -0 "$pid_to_wait" 2>/dev/null && [[ $waited -lt 10 ]]; do + sleep 1 + ((waited++)) + done + if kill -0 "$pid_to_wait" 2>/dev/null; then + kill -9 "$pid_to_wait" 2>/dev/null + log_warning "VM parallel: killed stuck process $pid_to_wait after 10s timeout" + else + wait "$pid_to_wait" + fi + pids=("${pids[@]:1}") + pid_start_times=("${pid_start_times[@]:1}") + ((job_count--)) + fi + # Start background job + (update_tags "vm" "$vmid") & + pids+=($!) + pid_start_times+=("$(date +%s)") + ((job_count++)) + done + for i in "${!pids[@]}"; do + local pid="${pids[$i]}" + local waited=0 + while kill -0 "$pid" 2>/dev/null && [[ $waited -lt 10 ]]; do + sleep 1 + ((waited++)) + done + if kill -0 "$pid" 2>/dev/null; then + kill -9 "$pid" 2>/dev/null + log_warning "VM parallel: killed stuck process $pid after 10s timeout" + else + wait "$pid" + fi + done +} + +# Parallel LXC processing function +process_lxc_parallel() { + local lxc_list=("$@") + local max_parallel=${MAX_PARALLEL_LXC_CHECKS:-7} + local batch_size=${LXC_BATCH_SIZE:-20} + local job_count=0 + local pids=() + local pid_start_times=() + + debug_log "Starting parallel LXC processing: ${#lxc_list[@]} containers, max_parallel=$max_parallel" + + if [[ ${#lxc_list[@]} -gt 5 ]]; then + debug_log "Pre-loading LXC statuses for ${#lxc_list[@]} containers" + local all_statuses=$(pct list 2>/dev/null) + for vmid in "${lxc_list[@]}"; do + local status=$(echo "$all_statuses" | grep "^$vmid" | awk '{print $2}') + if [[ -n "$status" ]]; then + local status_cache_file="/tmp/iptag_lxc_status_${vmid}_cache" + echo "$status" > "$status_cache_file" 2>/dev/null & + fi + done + wait + debug_log "Completed batch status pre-loading" + fi + for vmid in "${lxc_list[@]}"; do + if [[ $job_count -ge $max_parallel ]]; then + local pid_to_wait="${pids[0]}" + local start_time="${pid_start_times[0]}" + local waited=0 + while kill -0 "$pid_to_wait" 2>/dev/null && [[ $waited -lt 10 ]]; do + sleep 1 + ((waited++)) + done + if kill -0 "$pid_to_wait" 2>/dev/null; then + kill -9 "$pid_to_wait" 2>/dev/null + log_warning "LXC parallel: killed stuck process $pid_to_wait after 10s timeout" + else + wait "$pid_to_wait" + fi + pids=("${pids[@]:1}") + pid_start_times=("${pid_start_times[@]:1}") + ((job_count--)) + fi + # Start background job with higher priority + (update_tags "lxc" "$vmid") & + pids+=($!) + pid_start_times+=("$(date +%s)") + ((job_count++)) + done + for i in "${!pids[@]}"; do + local pid="${pids[$i]}" + local waited=0 + while kill -0 "$pid" 2>/dev/null && [[ $waited -lt 10 ]]; do + sleep 1 + ((waited++)) + done + if kill -0 "$pid" 2>/dev/null; then + kill -9 "$pid" 2>/dev/null + log_warning "LXC parallel: killed stuck process $pid after 10s timeout" + else + wait "$pid" + fi + done + debug_log "Completed parallel LXC processing" +} + +# Optimized LXC IP detection with caching and alternative methods +get_lxc_ips() { + local vmid=$1 + local status_cache_file="/tmp/iptag_lxc_status_${vmid}_cache" + local status_cache_ttl=${LXC_STATUS_CACHE_TTL:-30} + + debug_log "lxc $vmid: starting extreme optimized IP detection" + + # Check status cache first (avoid expensive pct status calls) + local lxc_status="" + if [[ -f "$status_cache_file" ]] && [[ $(($(date +%s) - $(stat -c %Y "$status_cache_file" 2>/dev/null || echo 0))) -lt $status_cache_ttl ]]; then + lxc_status=$(cat "$status_cache_file" 2>/dev/null) + debug_log "lxc $vmid: using cached status: $lxc_status" + else + lxc_status=$(pct status "${vmid}" 2>/dev/null | awk '{print $2}') + echo "$lxc_status" > "$status_cache_file" 2>/dev/null + debug_log "lxc $vmid: fetched fresh status: $lxc_status" + fi + + if [[ "$lxc_status" != "running" ]]; then + debug_log "lxc $vmid: not running (status: $lxc_status)" + return + fi + + local ips="" + local method_used="" + + # EXTREME Method 1: Direct Proxmox config inspection (super fast) + debug_log "lxc $vmid: trying direct Proxmox config inspection" + local pve_lxc_config="/etc/pve/lxc/${vmid}.conf" + if [[ -f "$pve_lxc_config" ]]; then + local static_ip=$(grep -E "^net[0-9]+:" "$pve_lxc_config" 2>/dev/null | grep -oE 'ip=([0-9]{1,3}\.){3}[0-9]{1,3}' | cut -d'=' -f2 | head -1) + debug_log "lxc $vmid: [CONFIG] static_ip='$static_ip' (from $pve_lxc_config)" + if [[ -n "$static_ip" && "$static_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + debug_log "lxc $vmid: found static IP $static_ip in Proxmox config" + ips="$static_ip" + method_used="proxmox_config" + fi + else + debug_log "lxc $vmid: [CONFIG] config file not found: $pve_lxc_config" + fi + + # EXTREME Method 2: Direct network namespace inspection (fastest dynamic) + if [[ -z "$ips" ]]; then + debug_log "lxc $vmid: trying optimized namespace inspection" + local ns_file="/var/lib/lxc/${vmid}/rootfs/proc/net/fib_trie" + if [[ -f "$ns_file" ]]; then + local ns_ip=$(timeout 1 grep -m1 -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' "$ns_file" 2>/dev/null | grep -v '127.0.0.1' | head -1) + debug_log "lxc $vmid: [NAMESPACE] ns_ip='$ns_ip'" + if [[ -n "$ns_ip" ]] && is_valid_ipv4 "$ns_ip"; then + debug_log "lxc $vmid: found IP $ns_ip via namespace inspection" + ips="$ns_ip" + method_used="namespace" + fi + else + debug_log "lxc $vmid: [NAMESPACE] ns_file not found: $ns_file" + fi + fi + + # EXTREME Method 3: Batch ARP table lookup (if namespace failed) + if [[ -z "$ips" ]]; then + debug_log "lxc $vmid: trying batch ARP lookup" + local bridge_name=""; local mac_addr="" + if [[ -f "$pve_lxc_config" ]]; then + bridge_name=$(grep -Eo 'bridge=[^,]+' "$pve_lxc_config" | head -1 | cut -d'=' -f2) + mac_addr=$(grep -Eo 'hwaddr=([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' "$pve_lxc_config" | head -1 | cut -d'=' -f2) + debug_log "lxc $vmid: [ARP] bridge_name='$bridge_name' mac_addr='$mac_addr' (from $pve_lxc_config)" + fi + if [[ -z "$bridge_name" || -z "$mac_addr" ]]; then + local lxc_config="/var/lib/lxc/${vmid}/config" + if [[ -f "$lxc_config" ]]; then + [[ -z "$bridge_name" ]] && bridge_name=$(grep "lxc.net.0.link" "$lxc_config" 2>/dev/null | cut -d'=' -f2 | tr -d ' ') + [[ -z "$mac_addr" ]] && mac_addr=$(grep "lxc.net.0.hwaddr" "$lxc_config" 2>/dev/null | cut -d'=' -f2 | tr -d ' ') + debug_log "lxc $vmid: [ARP] bridge_name='$bridge_name' mac_addr='$mac_addr' (from $lxc_config)" + else + debug_log "lxc $vmid: [ARP] lxc config not found: $lxc_config" + fi + fi + if [[ -n "$bridge_name" && -n "$mac_addr" ]]; then + local bridge_ip=$(ip neighbor show dev "$bridge_name" 2>/dev/null | grep "$mac_addr" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1) + debug_log "lxc $vmid: [ARP] bridge_ip='$bridge_ip'" + if [[ -n "$bridge_ip" && "$bridge_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then + debug_log "lxc $vmid: found IP $bridge_ip via ARP table" + ips="$bridge_ip" + method_used="arp_table" + fi + fi + fi + + # EXTREME Method 4: Fast process namespace (if ARP failed) + if [[ -z "$ips" ]] && [[ "${LXC_SKIP_SLOW_METHODS:-true}" != "true" ]]; then + debug_log "lxc $vmid: trying fast process namespace" + local pid_cache_file="/tmp/iptag_lxc_pid_${vmid}_cache" + local container_pid="" + if [[ -f "$pid_cache_file" ]] && [[ $(($(date +%s) - $(stat -c %Y "$pid_cache_file" 2>/dev/null || echo 0))) -lt 60 ]]; then + container_pid=$(cat "$pid_cache_file" 2>/dev/null) + else + container_pid=$(pct list 2>/dev/null | grep "^$vmid" | awk '{print $3}') + [[ -n "$container_pid" && "$container_pid" != "-" ]] && echo "$container_pid" > "$pid_cache_file" + fi + debug_log "lxc $vmid: [PROCESS_NS] container_pid='$container_pid'" + if [[ -n "$container_pid" && "$container_pid" != "-" ]]; then + local ns_ip=$(timeout 1 nsenter -t "$container_pid" -n ip -4 addr show 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | grep -v '127.0.0.1' | head -1) + debug_log "lxc $vmid: [PROCESS_NS] ns_ip='$ns_ip'" + if [[ -n "$ns_ip" ]] && is_valid_ipv4 "$ns_ip"; then + debug_log "lxc $vmid: found IP $ns_ip via process namespace" + ips="$ns_ip" + method_used="process_ns" + fi + fi + fi + + # Fallback: always do lxc-attach/pct exec with timeout if nothing found + if [[ -z "$ips" ]]; then + debug_log "lxc $vmid: trying fallback lxc-attach (forced)" + local attach_ip="" + attach_ip=$(timeout 7s lxc-attach -n "$vmid" -- ip -4 addr show 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | grep -v '127.0.0.1' | head -1) + local attach_status=$? + debug_log "lxc $vmid: [LXC_ATTACH] attach_ip='$attach_ip' status=$attach_status" + if [[ $attach_status -eq 124 ]]; then + debug_log "lxc $vmid: lxc-attach timed out after 7s" + fi + if [[ -n "$attach_ip" ]] && is_valid_ipv4 "$attach_ip"; then + debug_log "lxc $vmid: found IP $attach_ip via lxc-attach (forced)" + ips="$attach_ip" + method_used="lxc_attach_forced" + fi + fi + if [[ -z "$ips" ]]; then + debug_log "lxc $vmid: trying fallback pct exec (forced)" + local pct_ip="" + pct_ip=$(timeout 7s pct exec "$vmid" -- ip -4 addr show 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | grep -v '127.0.0.1' | head -1) + local pct_status=$? + debug_log "lxc $vmid: [PCT_EXEC] pct_ip='$pct_ip' status=$pct_status" + if [[ $pct_status -eq 124 ]]; then + debug_log "lxc $vmid: pct exec timed out after 7s" + fi + if [[ -n "$pct_ip" ]] && is_valid_ipv4 "$pct_ip"; then + debug_log "lxc $vmid: found IP $pct_ip via pct exec (forced)" + ips="$pct_ip" + method_used="pct_exec_forced" + fi + fi + + debug_log "lxc $vmid: [RESULT] ips='$ips' method='$method_used'" + echo "$ips" } main EOF - chmod +x /opt/iptag/iptag - - # Update service file - cat </lib/systemd/system/iptag.service -[Unit] -Description=IP-Tag service -After=network.target - -[Service] -Type=simple -ExecStart=/opt/iptag/iptag -Restart=always - -[Install] -WantedBy=multi-user.target -EOF - - systemctl daemon-reload &>/dev/null - systemctl enable -q --now iptag.service &>/dev/null - msg_ok "Updated IP-Tag Scripts" } # Main installation process @@ -497,30 +1276,7 @@ migrate_config msg_info "Setup Default Config" if [[ ! -f /opt/iptag/iptag.conf ]]; then - cat </opt/iptag/iptag.conf -# Configuration file for LXC IP tagging - -# List of allowed CIDRs -CIDR_LIST=( - 192.168.0.0/16 - 172.16.0.0/12 - 10.0.0.0/8 - 100.64.0.0/10 -) - -# Tag format options: -# - "full": full IP address (e.g., 192.168.0.100) -# - "last_octet": only the last octet (e.g., 100) -# - "last_two_octets": last two octets (e.g., 0.100) -TAG_FORMAT="full" - -# Interval settings (in seconds) -LOOP_INTERVAL=60 -VM_STATUS_CHECK_INTERVAL=60 -FW_NET_INTERFACE_CHECK_INTERVAL=60 -LXC_STATUS_CHECK_INTERVAL=60 -FORCE_UPDATE_INTERVAL=1800 -EOF + generate_config >/opt/iptag/iptag.conf msg_ok "Setup default config" else msg_ok "Default config already exists" @@ -528,288 +1284,16 @@ fi msg_info "Setup Main Function" if [[ ! -f /opt/iptag/iptag ]]; then - cat <<'EOF' >/opt/iptag/iptag -#!/bin/bash -# =============== CONFIGURATION =============== # -readonly CONFIG_FILE="/opt/iptag/iptag.conf" -readonly DEFAULT_TAG_FORMAT="full" -readonly DEFAULT_CHECK_INTERVAL=60 - -# Load the configuration file if it exists -if [ -f "$CONFIG_FILE" ]; then - # shellcheck source=./iptag.conf - source "$CONFIG_FILE" -fi - -# Convert IP to integer for comparison -ip_to_int() { - local ip="$1" - local a b c d - IFS=. read -r a b c d <<< "${ip}" - echo "$((a << 24 | b << 16 | c << 8 | d))" -} - -# Check if IP is in CIDR -ip_in_cidr() { - local ip="$1" cidr="$2" - ipcalc -c "$ip" "$cidr" >/dev/null 2>&1 || return 1 - - local network prefix ip_parts net_parts - network=$(echo "$cidr" | cut -d/ -f1) - prefix=$(echo "$cidr" | cut -d/ -f2) - IFS=. read -r -a ip_parts <<< "$ip" - IFS=. read -r -a net_parts <<< "$network" - - case $prefix in - 8) [[ "${ip_parts[0]}" == "${net_parts[0]}" ]] ;; - 16) [[ "${ip_parts[0]}.${ip_parts[1]}" == "${net_parts[0]}.${net_parts[1]}" ]] ;; - 24) [[ "${ip_parts[0]}.${ip_parts[1]}.${ip_parts[2]}" == "${net_parts[0]}.${net_parts[1]}.${net_parts[2]}" ]] ;; - 32) [[ "$ip" == "$network" ]] ;; - *) return 1 ;; - esac -} - -# Format IP address according to the configuration -format_ip_tag() { - local ip="$1" - local format="${TAG_FORMAT:-$DEFAULT_TAG_FORMAT}" - - case "$format" in - "last_octet") echo "${ip##*.}" ;; - "last_two_octets") echo "${ip#*.*.}" ;; - *) echo "$ip" ;; - esac -} - -# Check if IP is in any CIDRs -ip_in_cidrs() { - local ip="$1" cidrs="$2" - [[ -z "$cidrs" ]] && return 1 - local IFS=' ' - for cidr in $cidrs; do - ip_in_cidr "$ip" "$cidr" && return 0 - done - return 1 -} - -# Check if IP is valid -is_valid_ipv4() { - local ip="$1" - [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1 - - local IFS='.' parts - read -ra parts <<< "$ip" - for part in "${parts[@]}"; do - (( part >= 0 && part <= 255 )) || return 1 - done - return 0 -} - -# Get VM IPs using MAC addresses and ARP table -get_vm_ips() { - local vmid=$1 ips="" macs found_ip=false - qm status "$vmid" 2>/dev/null | grep -q "status: running" || return - - macs=$(qm config "$vmid" 2>/dev/null | grep -E 'net[0-9]+' | grep -oE '[a-fA-F0-9]{2}(:[a-fA-F0-9]{2}){5}') - [[ -z "$macs" ]] && return - - for mac in $macs; do - local ip - ip=$(arp -an 2>/dev/null | grep -i "$mac" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}') - [[ -n "$ip" ]] && { ips+="$ip "; found_ip=true; } - done - - if ! $found_ip; then - local agent_ip - agent_ip=$(qm agent "$vmid" network-get-interfaces 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' || true) - [[ -n "$agent_ip" ]] && ips+="$agent_ip " - fi - - echo "${ips% }" -} - -# Update tags for container or VM -update_tags() { - local type="$1" vmid="$2" config_cmd="pct" - [[ "$type" == "vm" ]] && config_cmd="qm" - - local current_ips_full - if [[ "$type" == "lxc" ]]; then - current_ips_full=$(lxc-info -n "${vmid}" -i 2>/dev/null | grep -E "^IP:" | awk '{print $2}') - else - current_ips_full=$(get_vm_ips "${vmid}") - fi - [[ -z "$current_ips_full" ]] && return - - local current_tags=() next_tags=() current_ip_tags=() - mapfile -t current_tags < <($config_cmd config "${vmid}" 2>/dev/null | grep tags | awk '{print $2}' | sed 's/;/\n/g') - - # Separate IP and non-IP tags - for tag in "${current_tags[@]}"; do - if is_valid_ipv4 "${tag}" || [[ "$tag" =~ ^[0-9]+(\.[0-9]+)*$ ]]; then - current_ip_tags+=("${tag}") - else - next_tags+=("${tag}") - fi - done - - local formatted_ips=() needs_update=false added_ips=() - for ip in ${current_ips_full}; do - if is_valid_ipv4 "$ip" && ip_in_cidrs "$ip" "${CIDR_LIST[*]}"; then - local formatted_ip=$(format_ip_tag "$ip") - formatted_ips+=("$formatted_ip") - if [[ ! " ${current_ip_tags[*]} " =~ " ${formatted_ip} " ]]; then - needs_update=true - added_ips+=("$formatted_ip") - next_tags+=("$formatted_ip") - fi - fi - done - - [[ ${#formatted_ips[@]} -eq 0 ]] && return - - # Add existing IP tags that are still valid - for tag in "${current_ip_tags[@]}"; do - if [[ " ${formatted_ips[*]} " =~ " ${tag} " ]]; then - if [[ ! " ${next_tags[*]} " =~ " ${tag} " ]]; then - next_tags+=("$tag") - fi - fi - done - - if [[ "$needs_update" == true ]]; then - echo "${type^} ${vmid}: adding IP tags: ${added_ips[*]}" - $config_cmd set "${vmid}" -tags "$(IFS=';'; echo "${next_tags[*]}")" &>/dev/null - fi -} - -# Update all instances of specified type -update_all_tags() { - local type="$1" list_cmd="pct" vmids count=0 - [[ "$type" == "vm" ]] && list_cmd="qm" - - vmids=$($list_cmd list 2>/dev/null | grep -v VMID | awk '{print $1}') - for vmid in $vmids; do ((count++)); done - - echo "Found ${count} running ${type}s" - [[ $count -eq 0 ]] && return - - for vmid in $vmids; do - update_tags "$type" "$vmid" - done -} - -# Check if status changed -check_status_changed() { - local type="$1" current - case "$type" in - "lxc") current=$(pct list 2>/dev/null | grep -v VMID) ;; - "vm") current=$(qm list 2>/dev/null | grep -v VMID) ;; - "fw") current=$(ifconfig 2>/dev/null | grep "^fw") ;; - esac - local last_var="last_${type}_status" - [[ "${!last_var}" == "$current" ]] && return 1 - eval "$last_var='$current'" - return 0 -} - -# Main check function -check() { - local current_time changes_detected=false - current_time=$(date +%s) - - # Check LXC status - local time_since_last_lxc_check=$((current_time - last_lxc_status_check_time)) - if [[ "${LXC_STATUS_CHECK_INTERVAL:-60}" -gt 0 ]] && \ - [[ "${time_since_last_lxc_check}" -ge "${LXC_STATUS_CHECK_INTERVAL:-60}" ]]; then - echo "Checking LXC status..." - last_lxc_status_check_time=${current_time} - if check_status_changed "lxc"; then - changes_detected=true - update_all_tags "lxc" - last_update_lxc_time=${current_time} - fi - fi - - # Check VM status - local time_since_last_vm_check=$((current_time - last_vm_status_check_time)) - if [[ "${VM_STATUS_CHECK_INTERVAL:-60}" -gt 0 ]] && \ - [[ "${time_since_last_vm_check}" -ge "${VM_STATUS_CHECK_INTERVAL:-60}" ]]; then - echo "Checking VM status..." - last_vm_status_check_time=${current_time} - if check_status_changed "vm"; then - changes_detected=true - update_all_tags "vm" - last_update_vm_time=${current_time} - fi - fi - - # Check network interface changes - local time_since_last_fw_check=$((current_time - last_fw_net_interface_check_time)) - if [[ "${FW_NET_INTERFACE_CHECK_INTERVAL:-60}" -gt 0 ]] && \ - [[ "${time_since_last_fw_check}" -ge "${FW_NET_INTERFACE_CHECK_INTERVAL:-60}" ]]; then - echo "Checking network interfaces..." - last_fw_net_interface_check_time=${current_time} - if check_status_changed "fw"; then - changes_detected=true - update_all_tags "lxc" - update_all_tags "vm" - last_update_lxc_time=${current_time} - last_update_vm_time=${current_time} - fi - fi - - # Force update if needed - for type in "lxc" "vm"; do - local last_update_var="last_update_${type}_time" - local time_since_last_update=$((current_time - ${!last_update_var})) - if [[ ${time_since_last_update} -ge ${FORCE_UPDATE_INTERVAL:-1800} ]]; then - echo "Force updating ${type} tags..." - changes_detected=true - update_all_tags "$type" - eval "${last_update_var}=${current_time}" - fi - done - - $changes_detected || echo "No changes detected in system status" -} - -# Initialize time variables -declare -g last_lxc_status="" last_vm_status="" last_fw_status="" -declare -g last_lxc_status_check_time=0 last_vm_status_check_time=0 last_fw_net_interface_check_time=0 -declare -g last_update_lxc_time=0 last_update_vm_time=0 - -# Main loop -main() { - while true; do - check - sleep "${LOOP_INTERVAL:-$DEFAULT_CHECK_INTERVAL}" - done -} - -main -EOF + generate_main_script >/opt/iptag/iptag + chmod +x /opt/iptag/iptag msg_ok "Setup Main Function" else msg_ok "Main Function already exists" fi -chmod +x /opt/iptag/iptag msg_info "Creating Service" if [[ ! -f /lib/systemd/system/iptag.service ]]; then - cat </lib/systemd/system/iptag.service -[Unit] -Description=IP-Tag service -After=network.target - -[Service] -Type=simple -ExecStart=/opt/iptag/iptag -Restart=always - -[Install] -WantedBy=multi-user.target -EOF + generate_service >/lib/systemd/system/iptag.service msg_ok "Created Service" else msg_ok "Service already exists." @@ -821,5 +1305,13 @@ msg_info "Starting Service" systemctl daemon-reload &>/dev/null systemctl enable -q --now iptag.service &>/dev/null msg_ok "Started Service" + +msg_info "Restarting Service with optimizations" +systemctl restart iptag.service &>/dev/null +msg_ok "Service restarted with CPU optimizations" + SPINNER_PID="" echo -e "\n${APP} installation completed successfully! ${CL}\n" + +# Proper script termination +exit 0