#!/usr/bin/env bash # Copyright (c) 2021-2025 community-scripts ORG # Author: MickLesk (Canbiz) && Desert_Gamer # License: MIT function header_info { clear cat <<"EOF" ___ ____ _____ |_ _| _ \ _ |_ _|_ _ __ _ | || |_) (_) | |/ _` |/ _` | | || __/ _ | | (_| | (_| | |___|_| (_) |_|\__,_|\__, | |___/ EOF } clear header_info APP="IP-Tag" hostname=$(hostname) # Color variables YW=$(echo "\033[33m") GN=$(echo "\033[1;92m") RD=$(echo "\033[01;31m") CL=$(echo "\033[m") BFR="\\r\\033[K" HOLD=" " CM=" ✔️ ${CL}" CROSS=" ✖️ ${CL}" # Error handler for displaying error messages error_handler() { if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID >/dev/null; then kill $SPINNER_PID >/dev/null fi printf "\e[?25h" local exit_code="$?" local line_number="$1" local command="$2" local error_message="${RD}[ERROR]${CL} in line ${RD}$line_number${CL}: exit code ${RD}$exit_code${CL}: while executing command ${YW}$command${CL}" echo -e "\n$error_message\n" } # Spinner for progress indication spinner() { local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') local spin_i=0 local interval=0.1 printf "\e[?25l" local color="${YWB}" while true; do printf "\r ${color}%s${CL}" "${frames[spin_i]}" spin_i=$(((spin_i + 1) % ${#frames[@]})) sleep "$interval" done } # Info message msg_info() { local msg="$1" echo -ne "${TAB}${YW}${HOLD}${msg}${HOLD}" spinner & SPINNER_PID=$! } # Success message msg_ok() { if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID >/dev/null; then kill $SPINNER_PID >/dev/null fi printf "\e[?25h" local msg="$1" echo -e "${BFR}${CM}${GN}${msg}${CL}" } # Error message msg_error() { if [ -n "$SPINNER_PID" ] && ps -p $SPINNER_PID >/dev/null; then kill $SPINNER_PID >/dev/null fi printf "\e[?25h" local msg="$1" echo -e "${BFR}${CROSS}${RD}${msg}${CL}" } # Check if service exists check_service_exists() { if systemctl is-active --quiet iptag.service; then return 0 else return 1 fi } # Migrate configuration from old path to new migrate_config() { local old_config="/opt/lxc-iptag" local new_config="/opt/iptag/iptag.conf" if [[ -f "$old_config" ]]; then msg_info "Migrating configuration from old path" if cp "$old_config" "$new_config" &>/dev/null; then rm -rf "$old_config" &>/dev/null msg_ok "Configuration migrated and old config removed" else msg_error "Failed to migrate configuration" fi fi } # Update existing installation 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 # 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 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 "Creating manual run command" cat <<'EOF' >/usr/local/bin/iptag-run #!/usr/bin/env bash CONFIG_FILE="/opt/iptag/iptag.conf" SCRIPT_FILE="/opt/iptag/iptag" if [[ ! -f "$SCRIPT_FILE" ]]; then echo "❌ Main script not found: $SCRIPT_FILE" exit 1 fi export FORCE_SINGLE_RUN=true exec "$SCRIPT_FILE" EOF chmod +x /usr/local/bin/iptag-run msg_ok "Created iptag-run executable - You can execute this manually by entering “iptag-run” in the Proxmox host, so the script is executed by hand." 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" 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#*.*.}" ;; *) echo "$ip" ;; esac } # Check if IP is in any CIDRs ip_in_cidrs() { local ip="$1" cidrs="$2" [[ -z "$cidrs" ]] && return 1 local IFS=' ' 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 } # 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 multiple methods with performance optimizations get_vm_ips() { 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 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 for container or VM update_tags() { local type="$1" vmid="$2" local current_ips_full if [[ "$type" == "lxc" ]]; then current_ips_full=$(get_lxc_ips "${vmid}") while IFS= read -r line; do [[ "$line" == tags:* ]] && current_tags_raw="${line#tags: }" && break done < <(pct config "$vmid" 2>/dev/null) 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 local current_tags=() next_tags=() current_ip_tags=() if [[ -n "$current_tags_raw" ]]; then mapfile -t current_tags < <(echo "$current_tags_raw" | sed 's/;/\n/g') fi # 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}") else next_tags+=("${tag}") fi done # 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[*]}" # 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 new IP tags for new_ip in "${formatted_ips[@]}"; do next_tags+=("$new_ip") done # 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 # 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_changed() { local type="$1" current case "$type" in "lxc") current=$(pct list 2>/dev/null | grep -v VMID) ;; "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 eval "$last_var='$current'" return 0 } # Main check function check() { local current_time changes_detected=false current_time=$(date +%s) local update_lxc=false local update_vm=false # 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 # 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 update_lxc=true log_warning "LXC status changes detected" 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 last_vm_status_check_time=$current_time if check_status_changed "vm"; then update_vm=true log_warning "VM status changes detected" 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 update_lxc=true update_vm=true log_warning "Network interface changes detected" fi fi # Force update if interval exceeded 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 if [[ "$type" == "lxc" ]]; then update_lxc=true log_info "Scheduled LXC update (every $((FORCE_UPDATE_INTERVAL / 60)) minutes)" else update_vm=true log_info "Scheduled VM update (every $((FORCE_UPDATE_INTERVAL / 60)) minutes)" fi eval "${last_update_var}=${current_time}" fi done # Final execution $update_lxc && update_all_tags "lxc" $update_vm && update_all_tags "vm" } # 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 last_cleanup_time=0 # Main loop main() { # 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" if [[ "$FORCE_SINGLE_RUN" == "true" ]]; then check exit 0 fi while true; do check sleep "${LOOP_INTERVAL:-300}" done } # 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:-2} 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" && "${LXC_ALLOW_FORCED_COMMANDS:-true}" == "true" ]]; 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" && "${LXC_ALLOW_FORCED_COMMANDS:-true}" == "true" ]]; 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 } # Main installation process if check_service_exists; then while true; do read -p "IP-Tag service is already installed. Do you want to update it? (y/n): " yn case $yn in [Yy]*) update_installation exit 0 ;; [Nn]*) msg_error "Installation cancelled." exit 0 ;; *) msg_error "Please answer yes or no." ;; esac done fi while true; do read -p "This will install ${APP} on ${hostname}. Proceed? (y/n): " yn case $yn in [Yy]*) break ;; [Nn]*) msg_error "Installation cancelled." exit ;; *) msg_error "Please answer yes or no." ;; esac done if ! pveversion | grep -Eq "pve-manager/8\.[0-4](\.[0-9]+)*"; then msg_error "This version of Proxmox Virtual Environment is not supported" msg_error "⚠️ Requires Proxmox Virtual Environment Version 8.0 or later." msg_error "Exiting..." sleep 2 exit fi FILE_PATH="/usr/local/bin/iptag" if [[ -f "$FILE_PATH" ]]; then msg_info "The file already exists: '$FILE_PATH'. Skipping installation." exit 0 fi msg_info "Installing Dependencies" apt-get update &>/dev/null apt-get install -y ipcalc net-tools &>/dev/null msg_ok "Installed Dependencies" msg_info "Setting up IP-Tag Scripts" mkdir -p /opt/iptag msg_ok "Setup IP-Tag Scripts" # Migrate config if needed migrate_config msg_info "Setup Default Config" if [[ ! -f /opt/iptag/iptag.conf ]]; then generate_config >/opt/iptag/iptag.conf msg_ok "Setup default config" else msg_ok "Default config already exists" fi msg_info "Setup Main Function" if [[ ! -f /opt/iptag/iptag ]]; then generate_main_script >/opt/iptag/iptag chmod +x /opt/iptag/iptag msg_ok "Setup Main Function" else msg_ok "Main Function already exists" fi msg_info "Creating Service" if [[ ! -f /lib/systemd/system/iptag.service ]]; then generate_service >/lib/systemd/system/iptag.service msg_ok "Created Service" else msg_ok "Service already exists." fi msg_ok "Setup IP-Tag Scripts" 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" msg_info "Creating manual run command" cat <<'EOF' >/usr/local/bin/iptag-run #!/usr/bin/env bash CONFIG_FILE="/opt/iptag/iptag.conf" SCRIPT_FILE="/opt/iptag/iptag" if [[ ! -f "$SCRIPT_FILE" ]]; then echo "❌ Main script not found: $SCRIPT_FILE" exit 1 fi export FORCE_SINGLE_RUN=true exec "$SCRIPT_FILE" EOF chmod +x /usr/local/bin/iptag-run msg_ok "Created iptag-run executable - You can execute this manually by entering “iptag-run” in the Proxmox host, so the script is executed by hand." SPINNER_PID="" echo -e "\n${APP} installation completed successfully! ${CL}\n" # Proper script termination exit 0